본문 바로가기
Back-end/java spring

[JPA] 영속성 컨텍스트에 대한 정리

by kkkdh 2023. 1. 21.
728x90

JPA가 내부에서 어떻게 돌아가는지에 대한 이론적 배경을 공부하기 위해 영속성 컨텍스트라는 개념을 공부해 보자.

 

JPA에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스를 매핑하기 (Object Relational Mapping)
  2. 영속성 컨텍스트 (JPA의 내부 동작과 연관된 개념)

이번에는 영속성 컨텍스트에 대한 개념을 정리해 보자.


영속성 컨텍스트

  • JPA를 이해하는데 가장 중요한 용어로
  • "엔티티를 영구 저장하는 환경"이라는 뜻이다.
  • EntityManager.persist(entity object);
    • 영속성 컨텍스트를 이용해 entity를 영속화함을 의미한다.

EntityManger와 영속성 컨텍스트를 정리하자면,

  • 영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다.
  • EntityManger를 통해 영속성 컨텍스트에 접근할 수 있다.

라고 할 수 있다.


엔티티의 생명 주기

엔티티 생명주기를 설명하는 대표적인 다이어그램이라고 한다.

  • 비영속 (new / transient)
    • 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태이다. (그냥 일반 객체나 다름이 없다)
  • 영속 (managed)
    • 영속성 컨텍스트에 관리되는 상태
  • 준영속 (detached)
    • 영속성 컨텍스트에 저장되었다가, 분리된 상태
  • 삭제 (removed)
    • 삭제된 상태

비영속 상태의 객체
EntityManager의 persist method 사용 이후에야 영속 상태가 된다.

영속성 컨텍스트에 엔티티를 영속화하는 시점에 DB에 query가 날라가지 않고, transaction이 commit되는 시점에 DB에 저장된다. 그러니까 영속성 컨텍스트에 영속화되는 것이 DB에 바로 저장되는 것을 의미하지 않는다고 볼 수 있다.

 

그러니깐 영속성 컨텍스트는 애플리케이션과 DB 사이에 있는 하나의 계층이라고 볼 수 있다. 따라서 중간에 계층을 둠으로써 버퍼링이나 캐싱을 할 수 있는 이점들을 취할 수 있게 된다.


영속성 컨텍스트의 이점

  • 1차 캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  • 변경 감지 (Dirty Checking)
  • 지연 로딩 (Lazy Loading)

 

<엔티티 조회, 1차 캐시>

 

EntityManager의 find method를 이용해 entity 객체를 찾는 경우, DB가 아닌 1차 캐시를 먼저 탐색한다.

 

1차 캐시는 다음과 같이 Identifier와 Entity 객체의 Map 형태로 구성되어 있다.

1차 캐시는 EntityManager 내부에 존재

만약에 영속성 컨텍스트의 1차 캐시에 없는 entity를 조회하려 하는 경우 DB에서 entity를 찾고, 1차 캐시에 저장한 이후 반환한다.

1차 캐시에 없는 entity를 조회하는 경우

1차 캐시에 마찬가지로 식별자와 entity 객체의 쌍으로 저장된다.

 

다만, 보통 데이터베이스 트랜잭션 이후에 영속성 컨텍스트가 닫히며 1차 캐시도 같이 소멸되기 때문에, 캐싱의 효과가 미미하다. (여러 고객을 대상으로 한 캐싱 서비스가 아니다. 찰나의 순간에 대한 캐싱 효과)

 

 

<영속 엔티티의 동일성 보장>

예시 코드

위 예시코드를 보자

 

persist method 전후로 문자열을 통해 영속성 컨텍스트에 영속화할 때, DB에 저장되는지를 확인하고자 하였고, 영속화된 entity를 find method로 조회해서 출력하는 간단한 코드이다.

select query는 안날라가고, insert query만 날라감

실행 결과를 봤을 때, "=== BEFORE ===", "=== AFTER ===" 문자열 전후로 insert query가 날라가지 않는 결과와 함께 조회한 entity가 select query 없이 조회되었음을 확인할 수 있다.

 

이를 통해 다시 한번

  1. 영속성 컨텍스트에 영속화되는 시점이 DB에 바로 저장되는 것을 의미하지 않는다.
  2. entity를 조회할 시에 영속성 컨텍스트 안의 1차 캐시를 먼저 탐색하고, 없는 경우 DB에 접근한다.

두 가지 사실을 확인할 수 있었다.

 

이번에는 코드를 조금 바꿔서

같은 entity 객체를 두 번 조회하는 예시 코드를 짜봤다.

이번에는 한 번의 select query로 두 개의 entity 객체를 조회하는 방식을 확인할 수 있었다. (두 번째 entity 객체는 1차 캐시에서 조회될 것이다)

 

두 entity가 동일함을 확인 가능

위의 결과처럼 동일성이 보장된다. (마치 자바 collection을 이용해 같은 객체를 조회하듯이)

 

1차 캐시로 반복 가능한 읽기(repeatable read) 등급의 transaction 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다 - 강의 자료

이렇게 어렵게 설명할 수 있는데, 이는 결국 같은 트랜잭션 안에서 동일한 entity를 조회하는 경우 entity의 동일성을 DB가 아닌 애플리케이션의 차원에서 보장해 줌을 의미하는 것 같다.

 

 

<트랜잭션을 지원하는 쓰기 지연>

 

이 개념은 앞선 예제에서 확인한 바가 있다.

 

개념적으로는 다음 그림과 같이 영속성 컨텍스트 안에 쓰기 지연 SQL 저장소라는 것 덕분에 가능한 기능이라고 한다.

영속화된 entity가 쓰기 지연 SQL 저장소에 commit 시점까지 쌓인다.

쓰기 지연 SQL 저장소 덕분에 entity가 commit 시점에 한 번에 DB에 반영되어 저장되는 구조를 가질 수 있다. 

DB에 저장되는 시점은 transaction을 commit method 호출 상황에서 flush(JPA에서) 후에 commit 되며 저장된다고 한다.

JPA에서는 DB에 저장할 때를 flush라는 용어로 설명한다고 함

(버퍼의 데이터를 처리하듯이 쓰기 지연 저장소의 entity를 모두 처리하기 때문에 flush라고 하는 게 아닐까 싶다)

그래도 예시 코드를 한 번더 짜보자.

예상대로 된다면, 구분선 이후에 query가 날라갈 것이다. 

선이 그어지고 insert query가 날라간다

이때, 버퍼링이라는 기능이 도입된다. (모아서 DB에 한 번에 전송되는 방식)

 

이것을 JDBC batch 처리라고 한다. (모아서 한 번에 처리하는 기능)

 

 

<변경 감지 - 엔티티 수정>

 

영속 상태인 entity를 수정한 다음에 persist method를 한 번 더 호출해야 영속성 컨텍스트에 반영되지 않을까??라는 오해를 하시 십상이다.

 

하지만, 이전에도 살펴본 바와 같이 JPA를 사용하는 목적은 entity를 자바 컬랙션 다루듯이 다루기 위함이었다.

 

따라서 컬랙션에서 가져온 객체를 다룰 때처럼 영속 상태의 객체는 JPA에서 관리하고 있기 때문에, 필드 값을 수정한다 하더라도 알아서 반영이 된다.

그냥 조회하고, 변경해 보았다.
persist 없이도 알아서 update query를 날려준다!!

이는 JPA에서 제공하는 변경 감지 덕분이다!!

 

변경 감지의 동작 방식을 그림과 함께 정리해 보자.

  1. flush method가 호출될 때, 스냅샷과 Entity를 내부적으로 비교한다. (일일이 전부 비교한다)
  2. 변경 사항을 쓰기 지연 SQL 저장소에 update query로 만들어준다.
  3. flush 과정에서 update query가 DB에 반영되고
  4. commit 되며 완료된다.
스냅샷: entity가 영속성 컨텍스트에 들어왔을 때의 최초 상태

사실 영속성 컨텍스트 내부에 있는 1차 캐시에는 스냅샷이라는 필드가 하나 더 있었고, 스냅샷을 이용해 변경 감지를 해서 update query를 알아서 만들어주는 구조인 것이다.

 

 

<엔티티 삭제>

 

이것 또한 변경 감지를 통한 entity 수정과 동일한 메커니즘으로 동작하며, 단지 entity가 삭제되는 것이 차이점일 뿐이다.

 

결론: entity 수정과 삭제의 상황에서는 persist method 사용하지 않는 것이 정답이며, 사용하지 않아도 JPA에 의해서 transaction commit 시점에서 변경 사항이 반영된다. (이를 변경 감지라고 부른다.)


플러시 (Flush)

영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 작업을 의미한다.

 

변경 사항과 데이터베이스의 상태를 맞춰주는 작업이라고 이해할 수 있다.

 

플러시 발생

  • 변경 감지
  • 수정된 엔티티에 대한 query가 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 query를 데이터베이스에 전송 (등록, 수정, 삭제 query)

영속성 컨텍스트를 플러시 하는 방법

  • em.flush() - 직접 호출하는 방법
    • 쓸 일이 거의 없지만, 알아두어야 테스트 같은 상황에서 사용할 수 있다.
  • transaction commit - 플러시 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동 호출

이해가 안 되는 부분은 일단 알아두고 넘어가자.

flush는 쓰기 지연 SQL 저장소에 작성된 query들을 날릴 뿐, 1차 캐시를 비우지는 않는다.
따라서 em.flush() method 호출한다고 해서 1차 캐시가 지워지지는 않는다.

위와 같은 상황에서 flush가 안된다면, JPQL의 실행 결과로 조회되는 entity가 아무도 없을 것이다.

 

이런 경우 여러 가지 문제가 생길 수도 있다고 하는데, 이를 방지하기 위해 JPA에서는 기본적으로 JPQL 실행 시 flush를 자동으로 호출한 뒤에 JPQL을 실행한다고 함.

 

플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)

//FlushModeType.AUTO (default)
//FlushModeType.COMMIT

기본값은 AUTO로 flush 호출하고 query를 실행하는 방식이고, COMMIT은 commit 시점에서만 플러시가 된다.

 

COMMIT 모드는 현재 영속화한 entity와 관계없는 entity를 조회하는 등의 작업 상황에서는 간혹 가다 도움이 된다고 한다.

(사실 크게 도움 되지 않는다고 한다..)

 

플러시는!!

  • 영속성 컨텍스트를 비우지 않는다.
  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화한다
  • Transaction이라는 작업 단위 개념이 있기 때문에, 존재하며 이 작업 단위가 중요하다. → transaction의 커밋 직전에만 동기화하면 되기 때문이다.

준영속 상태

지금 배워도 크게 이해가지 않는다고 한다. 일단은 정리해 두자.

 

일단 entity가 영속 상태가 되는 경우는 크게 두 가지이다.

 

바로 EntityManager의 persist method로 새로 등록하는 경우와 조회를 통해 영속성 컨텍스트에 없는 entity를 DB에서 가져오는 경우이다.

 

영속 상태 → 준영속 상태

  • 영속 상태의 entity가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용하지 못하게 된다. (변경감지 등..)

준영속 상태로 만드는 방법

  • em.detach(entity)
    • 특정 entity만 준영속 상태로 전환
  • em.clear()
    • 영속성 컨텍스트를 완전히 초기화 → 1차 캐시가 초기화 됨
  • em.close()
    • 영속성 컨텍스트를 종료 → 당연히 종료하면 영속 객체들은 준영속 객체가 될 것

Entity가 준영속 상태가 된다면, 더 이상 JPA에 의해 관리되지 않으며, 그에 따른 부가 기능들을 모두 사용할 수 없게 된다. (변경 감지같은 기능들)

728x90

댓글