1. 영속성 컨텍스트 (Persistence Context)
1.1 영속성 컨텍스트란?
- Persistence Context는 JPA(EntityManager), Hibernate(Session)에서 엔티티 상태와 변경사항을 관리하는 1차 캐시 역할 수행
- 엔티티를 조회하면 해당 엔티티는 영속성 컨텍스트에 등록되어 엔티티 식별자를 키로 하는 Map에 저장됨
- 같은 트랜잭션 내에서 동일 엔티티를 여러 번 조회해도, 영속성 컨텍스트에서 캐싱된 객체를 반환
- 데이터베이스를 조회하지 않고 캐시를 조회하기 때문에 빠름
1.2 EntityManager와 Session
- JPA의 EntityManager와 Hibernate의 Session은 영속성 컨텍스트의 API 역할을 하며, 기능적으로 거의 동일
- Hibernate 5.2부터는 Session이 EntityManager를 확장하여 두 인터페이스의 경계가 더욱 모호해짐
1.3 엔티티 상태 전이
- Transient/New: 새로 생성, 아직 DB에 저장되지 않은 상태
- Managed: persist() 호출 등으로 영속성 컨텍스트에 등록되어 관리되는 상태
- Detached: 영속성 컨텍스트에서 분리된 상태 (evict, close 등)
- Removed: remove() 호출로 삭제 예약
- 상태 변화는 다음과 같음
- persist(): INSERT 예약
- remove(): DELETE 예약
- 엔티티 필드 변경 (Managed 상태): UPDATE 예약
1.4 Write-Behind Cache와 Flushing
- 영속성 컨텍스트는 트랜잭션 write-behind 캐시로 동작함
- 엔티티 변경 (INSERT/UPDATE/DELETE)은 즉시 DB에 반영되지 않고, 일단 영속성 컨텍스트에 저장됨
- flush 시점에만 실제 DML (INSERT/UPDATE/DELETE)이 DB에 반영됨
- flush 시, 영속성 컨텍스트는 엔티티가 로드된 이후 변경되었는지 감지하여 UPDATE 쿼리를 생성
- 이러한 행위를 Dirty Checking이라고 지칭
1.5 Flushing의 목적과 모드
- flush()는 Persistence Context와 DB를 동기화하는 작업
- flush가 필요한 이유는 다음과 같음
- 트랜잭션 commit 시 변경사항을 안전하게 저장
- 쿼리 실행 전, in-memory 변경사항이 쿼리 결과에 반영 (read-your-writes 일관성)
- 수동(flush()), 자동(FlushMode) 플러싱 모두 지원
- JPA FlushModeType은 두 가지가 있음
- AUTO (default 값): 쿼리 실행 전, 트랜잭션 커밋 전 자동 flush
- COMMIT: 트랜잭션 커밋 시에만 flush. 쿼리 실행 전에는 flush 하지 않아 변경사항이 쿼리 결과에 반영되지 않을 수 있음
- Hibernate FlushMode는 네 가지가 있음
- AUTO: 트랜잭션 커밋 시 flush (default 값), 쿼리 실행 전에는 반드시 flush하지 않음
- ALWAYS: 모든 쿼리 실행 전 + 커밋 시 flush
- COMMIT: 커밋 시에만 flush
- MANUAL: 수동 flush만 가능 (자동 flush 없음)
1.6 실무 적용 팁
- 영속성 컨텍스트는 1차 캐시이자, 엔티티 상태/변경 추적의 핵심
- flush 모드에 따라 다르지만 기본적으로 flush()는 트랜잭션 경계뿐 아니라, 쿼리 실행 전에도 자주 일어남
- Dirty Checking으로 인해 자동 UPDATE, INSERT, DELETE가 생성되므로 애플리케이션에서는 엔티티 상태 변화만 신경 쓰면 됨 (DML SQL 직접 작성 불필요)
- FlushMode를 COMMIT으로 설정하면 성능은 좋아질 수 있지만, 쿼리 결과가 in-memory 변경사항을 반영하지 않을 수 있으니 주의 필요
2. Hibernate Action Queue
2.1 Hibernate 이벤트와 이벤트 리스너
- Hibernate에서 엔티티 상태 변경 (persist, merge, delete 등)이 발생할 때마다 내부적으로 이벤트가 생성됨
- i.g. PersistEvent, MergeEvent, DeleteEvent 등
- 각 이벤트는 기본 EventListener 구현체에 의해 처리됨
- i.g. DefaultPersistEventListener, DefaultMergeEventListener 등
- 개발자는 사용자 정의 이벤트 리스너로 기본 동작을 대체할 수 있음
2.2 Action Queue란?
- 이벤트 리스너가 엔티티 상태 전환을 감지하면, 엔티티 액션 (Insert, Update, Delete 등)을 Action Queue에 등록
- 영속성 컨텍스트의 flush() 시점에 Action Queue에 쌓인 모든 엔티티 액션이 실제 SQL DML (INSERT, UPDATE, DELETE)로 실행됨
- 즉, Action Queue는 write-behind (지연 쓰기) 캐시의 핵심 구현체
2.3 Flush 작업 실행 순서
- Hibernate는 영속성 컨텍스트가 flush 될 때 Action Queue에 쌓인 액션을 아래와 같은 엄격한 순서로 실행
- OrphanRemovalAction: 고아 객체 (연관관계에서 제거된 자식)의 삭제 처리
- EntityInsertAction / EntityIdentityInsertAction: 새로 추가된 엔티티에 대한 INSERT
- EntityUpdateAction: 변경된 엔티티 (Dirty Checking) UPDATE
- CollectionRemoveAction: 컬렉션 (OneToMany, ManyToMany 등)에서 삭제된 요소 처리
- CollectionUpdateAction: 컬렉션에 추가/수정된 요소 UPDATE
- CollectionRecreateAction: 컬렉션이 새로 생성된 경우(i.g. Bag 컬렉션 등) INSERT
- EntityDeleteAction: 삭제 예약된 엔티티에 대한 DELETE
- 위 순서가 중요한 이유는 다음과 같음
- 데이터 무결성 (외래키, 고유키 등) 위반 방지
- i.g. 고유 제약조건을 가진 엔티티를 삭제 후 같은 값으로 다시 삽입하려면, DELETE가 INSERT보다 먼저 실행되어야 함
2.4 주의할 점
- Action Queue의 실행 순서는 개발자가 persist, remove, update 등 메서드를 호출한 순서와 다를 수 있음
- i.g. 코드에서는 remove → persist 했지만, flush에서는 persist → remove가 실행되어 제약조건 위반될 수 있음
- 삭제 후 곧바로 삽입할 때는, 삭제 직후에 수동 flush()를 호출해 DELETE를 먼저 실행시키는 것을 권장
- 동일한 비즈니스 키나 자연 식별자를 가진 엔티티를 새로 추가하려고 기존 엔티티를 삭제/삽입하는 것보다 UPDATE로 값을 변경하는 것이 더 효율적이고 안전함
3. AUTO Flush Mode (JPA vs Hibernate)
3.1 JPA와 Hibernate의 AUTO Flush Mode 차이
- JPA의 FlushModeType.AUTO
- JPQL 또는 네이티브 SQL 쿼리 실행 전과 트랜잭션 커밋 전 항상 flush를 수행
- 즉, 쿼리 실행 전에 항상 영속성 컨텍스트의 변경 내용이 데이터베이스에 반영됨
- Hibernate의 FlushMode.AUTO
- 트랜잭션 커밋 시에는 항상 flush
- 쿼리 실행 전에는 “스마트 플러싱 (smart flushing)"
- 쿼리가 스캔하는 테이블에 대기 중인 엔티티 변경이 있을 때만 flush를 트리거
- flush 호출 횟수를 줄이고 1차 캐시 동기화를 가능한 한 늦춤 (성능 최적화)
3.2 Hibernate FlushMode.AUTO의 동작 원리와 한계
- JPQL/HQL 쿼리: Hibernate는 쿼리에서 사용되는 테이블을 파악해, 해당 테이블에 변경 대기 중인 엔티티가 있을 때만 flush
- 반면, Native SQL Query 호출 시
- Hibernate는 DBMS별 SQL 파서가 없기 때문에, 쿼리가 어떤 테이블에 영향을 미치는지 알 수 없음
- 따라서 네이티브 SQL 쿼리 실행 전에는 자동 flush를 트리거하지 않음
3.3 Native SQL Query 일관성 문제
- Native SQL 쿼리 실행 전 flush가 일어나지 않으면, 영속성 컨텍스트에 남아 있는 변경사항 (INSERT/UPDATE/DELETE)이
쿼리 결과에 반영되지 않는 읽기-쓰기 불일치(read-your-writes inconsistency) 발생 가능- i.g. 엔티티를 persist 한 후, 바로 네이티브 SQL로 count(*) 조회 시 DB에는 아직 INSERT가 반영되지 않아 count=0으로 나올 수 있음
3.4 Native SQL Query 일관성 문제 해결책
- FlushMode.ALWAYS로 쿼리/세션 플러시 강제
- 쿼리 실행 직전에 항상 flush를 발생시켜 일관성 보장
- 세션 전체에 대해 설정할 수도 있음
- addSynchronizedEntityClass/addSynchronizedQuerySpace 사용
- 네이티브 쿼리에 영향을 받는 엔티티 또는 테이블을 명시적으로 지정
- Hibernate는 해당 정보를 바탕으로 flush 여부 결정
4. Hibernate Dirty Checking 메커니즘
4.1 기본 Dirty Checking(변경 감지) 메커니즘
- 엔티티가 transient → managed로 전이되면 SQL INSERT가 실행되며 removed로 표시되면 SQL DELETE가 실행됨.
- SQL UPDATE는 별도의 상태 전이가 없고, 엔티티가 managed 상태일 때 속성이 변경되면 영속성 컨텍스트가 이를 추적함
- flush 시점에 Hibernate가 엔티티의 속성 변경을 감지 (dirty checking)해 SQL UPDATE를 생성함
4.2 Dirty Checking의 내부 동작 원리
- flush 시 Hibernate는 세션에 관리되는 모든 엔티티에 대해 flush-entity 이벤트를 발생시키고 각 엔티티의 persister.findDirty()를 호출해 엔티티의 현재 속성 값과 로딩 시점의 값을 비교
- 하나라도 값이 다르면 해당 필드를 UPDATE 대상으로 기록
- dirty check의 총 횟수 = 엔티티 수 * 각 엔티티의 프로퍼티 수
4.3 영속성 컨텍스트 크기와 성능 이슈
- 영속성 컨텍스트는 엔티티의 현재 상태와 로딩 시점의 상태 (loaded state, hydrated state) 모두를 저장하므로 메모리 2배 소모
- 모든 managed 엔티티를 dirty checking 하므로, 엔티티 수가 많아지면 CPU 리소스 소모가 매우 커짐
- 특히 batch 처리, 대용량 트랜잭션에서 성능 저하가 심각
4.4 읽기 전용 엔티티로 Dirty Checking 최적화
- 엔티티를 read-only 모드로 로딩하면, Dirty Checking을 하지 않아 메모리와 CPU를 대폭 절감할 수 있음
- 읽기 전용으로 로드된 엔티티는 flush 시점에 UPDATE 대상이 아님
4.5 배치 처리와 flush-clear-commit 패턴
- 배치 작업 시 영속성 컨텍스트 크기 관리가 매우 중요
- 너무 많은 엔티티가 한 컨텍스트에 쌓이면 dirty checking, GC, flush 등에서 성능 저하·메모리 부족 발생
- flush-clear-commit 패턴은 다음과 같음
- 일정 배치 크기마다 flush() → clear() → commit()
- flush(): 변경사항을 DB에 반영
- clear(): 영속성 컨텍스트를 비워 메모리/CPU 사용량 최소화
- commit(): 트랜잭션을 주기적으로 커밋해 롤백 리스크 최소화
- 이렇게 하면 롱런 트랜잭션, OOM, 전체 롤백 등 위험도 줄고, 성능도 보장할 수 있음
- 일정 배치 크기마다 flush() → clear() → commit()
5. Hibernate Bytecode Enhancement Dirty Checking
5.1 Bytecode Enhancement Dirty Checking이란
- Hibernate의 Bytecode Enhancement는 엔티티 클래스의 바이트코드를 빌드 시점에 계측하여 Dirty 상태를 자동으로 추적하는 기능
- 기존 Hibernate의 Dirty Checking은 flush 시점마다 모든 Managed 엔티티의 모든 속성을 일일이 비교하므로 느렸음
- Bytecode Enhancement를 활성화하면, 필드가 실제로 변경될 때마다 Dirty 상태를 즉시 기록하므로 상대적으로 빠름
- flush 시점에는 실제로 변경된 속성만 UPDATE 대상으로 처리
5.2 설정 방법
- Maven 플러그인(hbm-enhance-maven-plugin) 활용
- enableDirtyTracking 옵션을 true로 지정해야 함
5.3 실제 동작 방식
- 바이트코드 향상 후, 엔티티의 모든 setter/getter, 필드 변경 로직에 dirty tracker가 자동 삽입됨
- i.g. setName() 호출 시, 값이 실제로 변경되면 DirtyTracker에 해당 속성 이름이 기록됨
- flush 시점에 Hibernate는 DirtyTracker가 기록해 둔 변경된 속성만 UPDATE 대상으로 처리
5.4 한계와 주의점
- 영속성 컨텍스트가 작을 때는 성능 차이가 크지 않을 수 있음
- Bytecode Enhancement를 써도, 로딩 시점의 엔티티 스냅샷은 여전히 유지함 (2차 캐시 활용, 등)
- 결국 Dirty Checking 방식과 무관하게 영속성 컨텍스트 크기를 적절히 유지하는 것이 핵심 포인트
참고
인프런 - 고성능 JPA & Hibernate (High-Performance Java Persistence)
반응형
'DB > JPA' 카테고리의 다른 글
[Hibernate/JPA] Batching (0) | 2025.06.04 |
---|---|
[Hibernate/JPA] Statement (0) | 2025.06.04 |
[Hibernate/JPA] 상속 (0) | 2025.06.02 |
[Hibernate/JPA] 관계 (0) | 2025.06.02 |
[Hibernate/JPA] 식별자 생성 최적화 전략 (0) | 2025.05.30 |