✔️ 영속성 컨텍스트
- JPA 를 이해하는데 가장 중요한 용어
- "엔티티를 영구 저장하는 환경" 이라는 뜻
- EntityManager 를 통해서 영속성 컨텍스트에 접근
- EntityManager 를 생성하면, 그 안에 PersistenceContext(영속성 컨텍스트)라는 눈에 보이지 않는 논리적 개념의 공간이 생긴다.
- EntityManager 를 통해 persist(..) 와 같은 작업을 한다고 해서 곧바로 INSERT 쿼리가 DB에 전달되지 않는다.
- 해당 엔티티 객체는 영속성 컨텍스트의 관리 대상이 되는 상태인 영속 상태가 된다.
✔️ 엔티티의 생명주기
- 비영속 (new/transient)
- 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속 (managed)
- 준영속 (detached)
- 삭제 (removed)
🤔 왜 이런 매커니즘이 있는걸까?
영속성 컨텍스트를 가짐으로 해서 생기는 이점이 있기 때문이다.
✔️ 영속성 컨텍스트의 이점
- 엔티티 조회 - 1차 캐시
- 엔티티 조회 - 동일성(identity) 보장
- 엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연(Transactional write-behind)
- 엔티티 수정 - 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
👉 하나씩 알아보자.
엔티티 조회 - 1차 캐시
- 영속성 컨텍스트는 내부에 1차 캐시라는 것을 들고있다. (Map 형태)
- 1차 캐시 내부에는 ( 엔티티 객체의 PK 정보, 엔티티 객체, 최초 1차 캐시에 들어온 시점의 엔티티 정보 ) 등이 있다.
- 이렇게 되면, 조회시 이점이 있다.
em.find(..)
를 통해 조회시, JPA는 영속성 컨텍스트 내부 1차 캐시 영역을 조회한다.
- 1차 캐시에 값이 존재하면, 캐시에 있는 값으로 조회한다.
- 1차 캐시에 값이 없으면, DB에서 조회를 하고, 조회된 값을 1차 캐시에 저장한다. 그리고 그 저장된 값을 반환한다.
- 참고) EntityManager 는 클라이언트 요청에 해당하는 트랜잭션 종료시 close 된다. 따라서, 1차 캐시는 하나의 트랜잭션 안에서만 적용된다.
엔티티 조회 - 동일성(identity) 보장
- 한 트랜잭션 안에서 동일한 객체를 조회할 경우 반환되는 객체(findMember1, findMember2)가 서로 같은 것(==)을 말한다. ( 1차 캐시에서 조회하기 때문에 가능한 것이다. )
- 참고) 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연
- 영속성 컨텍스트 안에는 1차 캐시 외에도 쓰기 지연 SQL 저장소라는 것이 존재한다.
em.persist(memberA);
라고 하면, 먼저 memberA 가 1차 캐시에 들어간다. 그러면서 JPA는 memberA 엔티티를 분석해서 INSERT SQL를 생성한다. 그리고 쓰기 지연 SQL 저장소라는 곳에 쌓아둔다.
em.persist(memberB);
라고 하면, 먼저 memberB 가 1차 캐시에 들어간다. 그러면서 JPA는 memberB 엔티티를 분석해서 INSERT SQL를 생성한다. 그리고 쓰기 지연 SQL 저장소라는 곳에 쌓아둔다.
tx.commit();
을 통해 트랜잭션을 커밋하는 시점에, 쓰기 지연 SQL 저장소에 쌓인 쿼리가 데이터베이스에 전송된다. 그리고 트랜잭션이 커밋된다.
엔티티 수정 - 변경 감지(Dirty Checking)
- 엔티티의 값을 변경하기 위해서는
em.update(..)
같은 코드가 있어야할 것 같지만, 마치 자바 컬렉션에서 값을 변경하는 것 처럼, 그런 코드가 없어도 DB에 값이 변경된다. 어떻게 가능할까? 비밀은 영속성 컨텍스트에 있다.
- 트랜잭션이 커밋되는 시점에 내부적으로 JPA 는 다음과 같이 동작한다.
- 트랜잭션이 커밋되는 시점에 내부적으로 flush() 가 호출된다.
- 플러시 발생시, 1차 캐시 내부에 엔티티와 스냅샷(최초로 1차 캐시에 값이 들어온 시점의 상태)을 비교한다. (변경 감지)
- 비교한 값이 일치하지 않은 경우(=변경된 경우), UPDATE SQL 을 생성하여 쓰기 지연 SQL 저장소에 만들어둔다. 그리고 이를 데이터베이스에 전달하여 반영하고 커밋한다.
- 참고) 결과 로그
✔️ 플러시
- 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것을 말한다.
- 트랜잭션이 커밋되면 플러시가 자동으로 발생한다.
- 플러시 발생시 일어나는 일들
- 변경 감지(Dirty Checking)
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
- 참고) 쓰기 지연 SQL 저장소에는 등록, 수정, 삭제 쿼리 등이 담겨있다.
- 영속성 컨텍스트를 플러시하는 방법
em.flush()
: 직접 호출
트랜잭션 커밋
: 플러시 자동 호출
JPQL 쿼리 실행
: 플러시 자동 호출
- 참고)
flush()
가 발생한다고 해서 데이터베이스 트랜잭션이 커밋되는 것은 아니다.
- 참고)
flush()
가 발생한다고 해서 1차 캐시가 지워지지 않는다. ( 1차 캐시에는 아무런 영향을 주지 않는다. )
- 참고) 플러시 모드 옵션
FlushMode.AUTO
: 커밋이나 쿼리를 실행할 때 플러시 (기본값)
FlushMode.COMMIT
: 커밋할 때만 플러시
✔️ 준영속 상태
- 영속 -> 준영속
- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
- 영속성 컨텍스트가 제공하는 기능(Dirty Checking, .. 등등)을 사용 못함
- 준영속 상태로 만드는 방법
em.detach(entity)
: 특정 엔티티만 준영속 상태로 전환
em.clear()
: 영속성 컨텍스트를 완전히 초기화
em.close()
: 영속성 컨텍스트를 종료
강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.