Hibernate 내부에서 영속성 컨텍스트는 어떻게 동작할까?🤔
Published:
목차
- 들어가며..
- Hibernate에서의 영속성 컨텍스트
- Hibernate 내부 아키텍처
- Hibernate 핵심 내부 클래스
- 결론
N+1기
계좌가 많아질 경우, 어떻게 설계하면 좋을까?🤔
N+1은 왜 발생할까?🤔(Fetch Join 적용 전후 성능 비교)
N+1은 왜 JPA에서 발생할까?🤔(JPQL)
N+1은 메모리에 어떤 영향을 미칠까?🤔(JVM)
영속성 컨텍스트는 N+1과 어떤 관계가 있을까?🤔(Hibernate)
Fetch Join은 영속성 컨텍스트에서 어떻게 동작할까?🤔
Hibernate 내부에서 영속성 컨텍스트는 어떻게 동작할까?🤔
들어가며..
스프링은 직접 데이터베이스와 통신하지 않고, 다음 단계로 통신한다.
개발자 코드 → JPA(ORM을 위한 표준 인터페이스) → Hibernate(JPA 구현체) → JDBC(DB 연결 드라이버) → Database(H2 / MySQL 등)
Hibernate(ORM)가 자동으로 H2 데이터베이스에 테이블을 생성하고 쿼리를 실행해주는 관계이다.
영속성 컨텍스트
영속성 컨텍스트는 DB에서 조회되거나 저장된 엔티티를 캐싱하고 변경 사항을 추적하는 역할을 한다. 이곳에 저장된 객체는 Entity Manger가 관리하는 “영속 상태(Persistent State)”가 되며, JPA는 해당 객체들의 상태를 추적한다.
Hibernate에서 영속성 컨텍스트는 1차 캐시 역할을 하며 DB와 애플리케이션 사이에서 엔티티 상태를 관리힌다.
- DB에서 조회된 엔티티는 영속성 컨텍스트에 저장
- 동일 PK 엔티티는 항상 하나의 객체 인스턴스만 존재
- 엔티티 변경을 dirty checking으로 추적
Application
↓
Persistence Context (1st Level Cache)
↓
Database
Hibernate에서의 영속성 컨텍스트
Hibernate에서는 영속성 컨텍스트 역할을 Session이 수행한다.
- JPA
- EntityManager = 영속성 컨텍스트를 관리하는 인터페이스
- Hibernate
- Session = JPA의 EntityManager 구현체
- 내부적으로 PersistenceContext 구현체를 가지고 있음
EntityManager (JPA)
↓
Hibernate Session
↓
PersistenceContext (1차 캐시)
Hibernate 내부 구조
- Hibernate Session 내부
- PersistenceContext
- entitiesByKey
- entityEntries
- ActionQueue
- DirtyChecking 메커니즘
- 핵심 클래스
- org.hibernate.engine.spi.PersistenceContext
- org.hibernate.internal.SessionImp
트랜잭션과 영속성 컨텍스트
Spring + Hibernate 환경에서는 @Transactional 단위로 영속성 컨텍스트가 생성되고 종료된다. 이것이 트랜잭션 단위 영속성 컨텍스트 (Transaction-scoped)이다.
Hibernate 내부 아키텍처
애플리케이션은 @Transactional로 정의된 트랜잭션 범위 안에서 EntityManager를 사용해 엔티티를 조회하고 변경한다. 이때 EntityManager는 JPA 표준 인터페이스일 뿐이고, 실제 동작은 Hibernate의 Session이 담당한다.
Session은 내부적으로 PersistenceContext(영속성 컨텍스트) 를 관리하며, 여기서 엔티티를 1차 캐시에 저장해 동일성(identity)을 보장하고 변경 사항을 추적한다. 엔티티를 수정하더라도 즉시 DB에 반영하지 않고, 변경 내용은 ActionQueue에 쌓인다.
이후 트랜잭션이 종료되거나 flush 시점이 되면, ActionQueue에 모인 SQL이 한 번에 실행되고, JDBC를 통해 최종적으로 데이터베이스에 반영된다.
[Application]
↓
@Transactional
↓
EntityManager (JPA)
↓
Session (Hibernate)
↓
-----------------------------------
| PersistenceContext |
| - entitiesByKey (1차 캐시) |
| - entityEntries (스냅샷) |
-----------------------------------
↓
ActionQueue (쓰기 지연 SQL 저장소)
↓
JDBC → DB
엔티티 조회 시 내부 동작
find() 호출
내부 흐름
// find() 호출
Member member = em.find(Member.class, 1L);
- Session.find()
- PersistenceContext 조회
├─ entitiesByKey 에 key 존재?
│ ├─ YES → 바로 반환 (DB 안감)
│ └─ NO → DB 조회
DB에서 row 조회
엔티티 객체 생성
PersistenceContext 저장
├─ entitiesByKey 에 저장
├─ entityEntries 에 스냅샷 저장
- 반환
1차 캐시
같은 트랜잭션에서 동일 객체 보장하므로 ‘member1 == member2’ 가 true이다.
entitiesByKey
------------------------------
| (Member, 1L) → Member@A1 |
| (Member, 2L) → Member@B2 |
------------------------------
변경 감지(Dirty Checking)
엔티티 로딩 시 Hibernate는 스냅샷을 저장한다.
entityEntries
----------------------------------------
| Member@A1 → [id=1, name="old"] |
----------------------------------------
member.setName("new");
Flush 시점 내부 동작
- 현재 엔티티 값 조회
- 스냅샷과 비교
- 변경됨 감지
- update SQL 생성
- ActionQueue에 저장
쓰기 지연 (Write Behind)
아래의 명령어 실행 시에도 바로 INSERT 안 나간다.
em.persist(member);
내부 구조
PersistenceContext
↓
ActionQueue
---------------------------------
| InsertAction(Member@A1) |
| UpdateAction(Member@B2) |
---------------------------------
Flush 발생 시 반영된다.
- commit 직전
- JPQL 실행 직전
- em.flush() 호출 시
ActionQueue.execute()
↓
JDBC batch
↓
DB
flush 전체 흐름
Hibernate는 foreign key 순서를 고려해서 SQL을 정렬한다.
flush()
↓
1. Dirty Checking
2. SQL 생성
3. ActionQueue 정렬
4. JDBC batch 실행
5. DB 반영
지연 로딩 내부
실제 저장 구조
Order
|
└── member → MemberProxy
접근 시
order.getMember().getName()
↓
프록시가 Session 통해 DB 조회
↓
PersistenceContext 저장
↓
실제 객체로 교체
트랜잭션 종료
commit
↓
flush
↓
DB 반영
↓
PersistenceContext clear
↓
모든 엔티티 Detached
정리
Hibernate 영속성 컨텍스트는 다음과 같은 순서로 동작한다.
- 1차 캐시(Map 구조)
- 스냅샷 기반 Dirty Checking
- ActionQueue 기반 쓰기 지연
- Flush 시점에 SQL 실행
- 트랜잭션 단위 생명주기
┌────────────────────┐
│ Application │
└─────────┬──────────┘
↓
┌────────────────────┐
│ Session │
└─────────┬──────────┘
↓
┌─────────────────────────────────┐
│ PersistenceContext │
│ ┌──────────────────────────┐ │
│ │ entitiesByKey (1차 캐시) │ │
│ └──────────────────────────┘ │
│ ┌──────────────────────────┐ │
│ │ entityEntries (스냅샷) │ │
│ └──────────────────────────┘ │
└───────────────┬─────────────────┘
↓
┌────────────────┐
│ ActionQueue │
└────────┬───────┘
↓
JDBC
↓
DB
Hibernate 핵심 내부 클래스
실제 코드에서 중요한 것들은 다음과 같다. 이 네 개는 Hibernate의 엔티티 관리 핵심 구조이다.
- org.hibernate.internal.SessionImpl
- org.hibernate.engine.spi.PersistenceContext
- org.hibernate.engine.spi.ActionQueue
- org.hibernate.engine.spi.EntityEntry
아키텍처 관계
SessionImpl
│
├── PersistenceContext
│ └── EntityEntry
│
└── ActionQueue
SessionImpl
Hibernate의 핵심 엔진이며 Hibernate Session의 실제 구현체이다. 실제 모든 동작은 SessionImpl에서 수행된다.
JPA를 사용할 때 구조는 다음과 같다.
Application
↓
EntityManager (JPA 인터페이스)
↓
Hibernate EntityManager 구현체
↓
SessionImpl
주요 역할
- 영속성 컨텍스트 관리
- 엔티티 조회
- flush 관리
- SQL 실행
- 트랜잭션 연동
실제 내부 호출 흐름
EntityManager.find()
↓
SessionImpl.find()
↓
PersistenceContext 조회
↓
없으면 DB 조회
내부 구조
SessionImpl
├─ PersistenceContext (엔티티 관리)
└─ ActionQueue (SQL 실행 대기)
PersistenceContext
영속성 컨텍스트의 실제 구현이다.
핵심 역할
- 엔티티 1차 캐시 관리
- 엔티티 상태 관리
- Dirty Checking 지원
내부 구조
대표적으로 이런 Map들이 있다.
PersistenceContext
├── entitiesByKey
├── entityEntries
├── proxiesByKey
entitiesByKey(1차 캐시)
Map<EntityKey, Object>
같은 트랜잭션에서 true가 된다.
(Member,1) → Member@123
(Member,2) → Member@456
member1 == member2
entityEntries
엔티티 메타 정보 저장소이다.
Map<Object, EntityEntry>
엔티티 객체는 관리 정보가 된다.
Member@123 → EntityEntry
EntityEntry
엔티티 상태 메타데이터다. Hibernate가 엔티티를 관리하기 위해 필요한 정보가 들어 있다.
대표 필드
EntityEntry
├─ Status status
├─ Object[] loadedState
├─ Object id
├─ EntityPersister persister
└─ LockMode lockMode
status
엔티티 상태를 나타낸다.
- MANAGED
- DELETED
- DETACHED
- LOADING
loadedState
Dirty Checking의 핵심이다. 변경 감지를 수행한다.
loadedState = ["id=1", "name=old"]
// flush 시 비교
현재값 vs loadedState
Dirty Checking 과정
EntityEntry.loadedState
│
▼
현재 엔티티 값 비교
│
▼
다르면 UPDATE SQL 생성
ActionQueue
SQL 실행 대기 큐다. Hibernate의 쓰기 지연 전략(write-behind)을 담당한다. 각각 SQL 작업 리스트다.
내부 구조
ActionQueue
├─ insertions
├─ updates
├─ deletions
├─ collectionUpdates
persist 시 동작
SessionImpl.persist() // INSERT SQL
↓
PersistenceContext 저장 // ActionQueue에 저장
↓
ActionQueue.insertions 추가 // flush 때 실행
전체 동작 연결
Hibernate 내부 핵심 흐름을 합치면 다음과 같다.
SessionImpl
│
├── PersistenceContext
│ │
│ ├── entitiesByKey (1차 캐시)
│ └── entityEntries
│ └── EntityEntry
│
└── ActionQueue
├── insert
├── update
└── delete
실제 조회 흐름
find()
↓
SessionImpl
↓
PersistenceContext.entitiesByKey 조회
↓
없으면 DB 조회
↓
엔티티 생성
↓
EntityEntry 생성
↓
PersistenceContext 저장
실제 update 흐름
member.setName("new")
↓
flush
↓
EntityEntry.loadedState 비교
↓
변경 감지
↓
ActionQueue update 추가
↓
SQL 실행
결론
Hibernate 영속성 컨텍스트는 Map(entitiesByKey) + Snapshot(EntityEntry) + SQL Queue(ActionQueue) 구조로 동작한다.
참고
Gemini
ChatGPT
Claude.ai
[JPA] 영속성 컨텍스트의 동작원리와 이점
Hibernate ORM User Guide
hibernate-orm
SessionImpl (Hibernate JavaDocs)
Chapter 3. Persistence Contexts
EntityEntry (Hibernate JavaDocs)
ActionQueue (Hibernate JavaDocs)
