JPA 영속성 컨텍스트를 이해할 때는 “캐시가 있다”는 설명보다 현재 엔티티가 관리 대상인지, 변경이 언제 데이터베이스와 동기화되는지를 먼저 보면 된다. 엔티티 상태와 flush, 트랜잭션 commit의 경계를 함께 놓으면 변경 감지와 준영속 상태도 자연스럽게 연결된다.
이 글은 JPA 공부 순서에서 영속성 관리 부분만 떼어, Jakarta Persistence 3.2 사양과 Hibernate 문서를 기준으로 다시 정리한 학습 노트다.
영속성 컨텍스트는 관리 중인 엔티티의 작업 공간이다
영속성 컨텍스트는 엔티티 인스턴스와 그 생명주기를 관리하는 환경이다. 하나의 영속성 컨텍스트 안에서는 같은 영속 식별자에 대응하는 관리 엔티티 인스턴스가 하나만 존재한다. 이 성질 때문에 같은 식별자를 반복 조회할 때 객체 동일성을 유지하고, 관리 중인 객체의 변화를 추적할 수 있다.
이를 흔히 1차 캐시라고도 부르지만, 조회 성능만을 위한 캐시로 이해하면 범위가 좁다. 더 중요한 역할은 다음과 같다.
- 엔티티의 현재 생명주기 상태를 관리한다.
- 관리 엔티티의 변경을 추적한다.
- 변경 작업을 모아
flush할 때 데이터베이스와 동기화한다. - 같은 영속 식별자에 대한 객체 동일성을 유지한다.
엔티티의 네 가지 상태
상태는 “이 객체가 지금 특정 영속성 컨텍스트와 어떤 관계인가”로 구분한다.
| 상태 | 영속성 컨텍스트와의 관계 | 대표적인 전환 |
|---|---|---|
| 비영속(new) | 아직 연결되지 않은 새 객체 | new Member() |
| 관리(managed) | 현재 컨텍스트가 추적하는 객체 | persist(), find(), merge()의 반환값 |
| 분리(detached) | 영속 식별자는 있지만 현재 컨텍스트가 관리하지 않음 | detach(), clear(), 컨텍스트 종료 |
| 삭제(removed) | 관리 중이며 삭제 대상으로 예약됨 | remove() |
persist()는 새 엔티티를 관리 상태로 만든다. 이 호출이 항상 그 자리에서 INSERT를 실행한다는 뜻은 아니다. 사양상 데이터베이스 반영은 늦어도 flush 또는 트랜잭션 커밋 과정에서 이루어지며, 식별자 생성 전략과 구현체에 따라 SQL 실행 시점은 달라질 수 있다.
merge()도 주의해야 한다. 전달한 분리 객체 자체를 다시 관리 상태로 바꾸는 메서드가 아니다. 그 상태를 관리 인스턴스에 복사하고 관리 상태인 별도의 반환값을 돌려준다. 이후 변경을 이어 갈 객체는 merge()의 인자보다 반환값이다.
변경 감지는 관리 상태에서만 동작한다
Hibernate는 관리 엔티티를 처음 읽었을 때의 상태와 현재 상태를 비교해 변경을 감지한다. 트랜잭션 안에서 관리 엔티티의 필드를 바꾸면, 별도 update() 호출 없이도 flush 과정에서 필요한 UPDATE가 만들어질 수 있다.
엔티티 조회
→ 관리 상태에서 값 변경
→ flush 시 변경 감지
→ UPDATE SQL 실행
→ 트랜잭션 commit 또는 rollback
반대로 분리 상태의 객체는 현재 영속성 컨텍스트가 추적하지 않는다. 값을 바꿨다는 이유만으로 변경 감지가 일어나지 않는다. detach(entity)는 한 엔티티를 분리하고, clear()는 컨텍스트 안의 관리 엔티티를 모두 분리한다.
flush와 commit은 같은 동작이 아니다
flush는 영속성 컨텍스트의 보류된 변경을 데이터베이스와 동기화하는 과정이다. 이때 INSERT, UPDATE, DELETE SQL이 실행될 수 있지만 트랜잭션이 끝난 것은 아니다.
commit은 트랜잭션을 성공으로 완료한다. 그 전에 필요한 flush가 일어날 수 있다. 이미 flush로 SQL을 보냈더라도 이후 트랜잭션이 rollback되면 해당 변경은 커밋되지 않는다.
| 동작 | 영속성 컨텍스트 | 데이터베이스 SQL | 트랜잭션 |
|---|---|---|---|
flush() |
유지됨 | 보류된 변경을 동기화 | 계속 진행 중 |
clear() |
관리 엔티티를 모두 분리 | 자체로 flush하지 않음 | 계속 진행 중 |
commit() |
환경 구성에 따라 범위 종료 가능 | 필요한 flush 뒤 커밋 | 완료 |
rollback() |
객체 상태를 자동으로 원상 복구한다고 볼 수 없음 | 변경을 커밋하지 않음 | 취소 |
기본 FlushModeType.AUTO에서는 커밋 전뿐 아니라, 아직 반영하지 않은 변경이 쿼리 결과에 영향을 줄 수 있을 때 쿼리 실행 전에 flush가 필요하다. FlushModeType.COMMIT은 동기화를 커밋 시점까지 늦추려는 모드지만, 쿼리가 메모리의 변경을 어떻게 보게 될지는 별도 주의가 필요하다.
실습할 때 확인할 세 가지
영속성 컨텍스트는 설명만 읽기보다 작은 트랜잭션에서 SQL 로그와 객체 상태를 함께 보면 이해가 빠르다.
- 같은 트랜잭션에서 같은 식별자를 두 번
find()했을 때 객체 동일성이 유지되는지 본다. - 관리 엔티티의 값을 바꾼 뒤
flush()전후에 SQL이 언제 실행되는지 본다. clear()뒤 다시 조회했을 때 새 SQL과 새 관리 인스턴스가 만들어지는지 본다.
여기서 SQL 로그에 문장이 보였다는 사실과 트랜잭션이 최종 커밋됐다는 사실은 구분해야 한다. 테스트가 rollback으로 끝나는지도 함께 확인해야 한다.
정리
영속성 컨텍스트를 읽는 질문은 세 개면 충분하다.
- 이 엔티티는 지금 관리 상태인가?
- 변경은 어느 flush에서 SQL로 동기화되는가?
- 그 SQL이 속한 트랜잭션은 실제로 commit됐는가?
이 순서로 보면 1차 캐시, 변경 감지, 쓰기 지연, 준영속 상태가 서로 떨어진 기능이 아니라 하나의 상태 관리 흐름으로 보인다. 프록시와 지연 로딩까지 이어서 볼 때는 JPA 프록시와 연관관계 정리가 다음 단계다.
참고 자료
'배움과 성장 > 백엔드·데이터' 카테고리의 다른 글
| JPA 엔티티에 protected 기본 생성자가 필요한 이유 (0) | 2023.01.30 |
|---|---|
| MongoDB macOS 설치와 로컬 실행: Homebrew·mongosh·보안 확인 (0) | 2023.01.03 |
| JPA 값 타입 정리: @Embeddable과 @ElementCollection의 경계 (2) | 2022.12.28 |
| JPA 프록시·지연 로딩·cascade·orphanRemoval: 헷갈리는 경계 (0) | 2022.12.28 |
| JPA 공부 순서: ORM·영속성 컨텍스트부터 Spring Data JPA까지 (0) | 2022.12.28 |
댓글