계좌가 많아질 경우, 어떻게 설계하면 좋을까?🤔
N+1은 왜 발생할까?🤔(Fetch Join 적용 전후 성능 비교)
N+1은 왜 JPA에서 발생할까?🤔(JPQL)
N+1은 메모리에 어떤 영향을 미칠까?🤔(JVM)
영속성 컨텍스트는 N+1과 어떤 관계가 있을까?🤔(Hibernate)
Fetch Join은 영속성 컨텍스트에서 어떻게 동작할까?🤔
Hibernate 내부에서 영속성 컨텍스트는 어떻게 동작할까?🤔
N+1은 GC를 왜 자주 발생시킬까?🤔
스프링은 직접 데이터베이스와 통신하지 않고, 다음 단계로 통신한다.
개발자 코드 → JPA(ORM을 위한 표준 인터페이스) → Hibernate(JPA 구현체) → JDBC(DB 연결 드라이버) → Database(H2 / MySQL 등)
Hibernate(ORM)가 자동으로 H2 데이터베이스에 테이블을 생성하고 쿼리를 실행해주는 관계이다.
영속성 컨텍스트는 DB에서 조회되거나 저장된 엔티티를 캐싱하고 변경 사항을 추적하는 역할을 한다. 이곳에 저장된 객체는 Entity Manger가 관리하는 “영속 상태(Persistent State)”가 되며, JPA는 해당 객체들의 상태를 추적한다.
Hibernate에서 영속성 컨텍스트는 1차 캐시 역할을 하며 DB와 애플리케이션 사이에서 엔티티 상태를 관리힌다.
Application
↓
Persistence Context (1st Level Cache)
↓
Database
Hibernate에서는 영속성 컨텍스트 역할을 Session이 수행한다.
EntityManager (JPA)
↓
Hibernate Session
↓
PersistenceContext (1차 캐시)
Spring + Hibernate 환경에서는 @Transactional 단위로 영속성 컨텍스트가 생성되고 종료된다. 이것이 트랜잭션 단위 영속성 컨텍스트 (Transaction-scoped)이다.
애플리케이션은 @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() 호출
Member member = em.find(Member.class, 1L);
├─ entitiesByKey 에 key 존재?
│ ├─ YES → 바로 반환 (DB 안감)
│ └─ NO → DB 조회
DB에서 row 조회
엔티티 객체 생성
PersistenceContext 저장
├─ entitiesByKey 에 저장
├─ entityEntries 에 스냅샷 저장
같은 트랜잭션에서 동일 객체 보장하므로 ‘member1 == member2’ 가 true이다.
entitiesByKey
------------------------------
| (Member, 1L) → Member@A1 |
| (Member, 2L) → Member@B2 |
------------------------------
엔티티 로딩 시 Hibernate는 스냅샷을 저장한다.
entityEntries
----------------------------------------
| Member@A1 → [id=1, name="old"] |
----------------------------------------
member.setName("new");
Flush 시점 내부 동작
아래의 명령어 실행 시에도 바로 INSERT 안 나간다.
em.persist(member);
내부 구조
PersistenceContext
↓
ActionQueue
---------------------------------
| InsertAction(Member@A1) |
| UpdateAction(Member@B2) |
---------------------------------
Flush 발생 시 반영된다.
ActionQueue.execute()
↓
JDBC batch
↓
DB
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 영속성 컨텍스트는 다음과 같은 순서로 동작한다.
┌────────────────────┐
│ Application │
└─────────┬──────────┘
↓
┌────────────────────┐
│ Session │
└─────────┬──────────┘
↓
┌─────────────────────────────────┐
│ PersistenceContext │
│ ┌──────────────────────────┐ │
│ │ entitiesByKey (1차 캐시) │ │
│ └──────────────────────────┘ │
│ ┌──────────────────────────┐ │
│ │ entityEntries (스냅샷) │ │
│ └──────────────────────────┘ │
└───────────────┬─────────────────┘
↓
┌────────────────┐
│ ActionQueue │
└────────┬───────┘
↓
JDBC
↓
DB
실제 코드에서 중요한 것들은 다음과 같다. 이 네 개는 Hibernate의 엔티티 관리 핵심 구조이다.
SessionImpl
│
├── PersistenceContext
│ └── EntityEntry
│
└── ActionQueue
Hibernate의 핵심 엔진이며 Hibernate Session의 실제 구현체이다. 실제 모든 동작은 SessionImpl에서 수행된다.
JPA를 사용할 때 구조는 다음과 같다.
Application
↓
EntityManager (JPA 인터페이스)
↓
Hibernate EntityManager 구현체
↓
SessionImpl
EntityManager.find()
↓
SessionImpl.find()
↓
PersistenceContext 조회
↓
없으면 DB 조회
SessionImpl
├─ PersistenceContext (엔티티 관리)
└─ ActionQueue (SQL 실행 대기)
영속성 컨텍스트의 실제 구현이다.
대표적으로 이런 Map들이 있다.
PersistenceContext
├── entitiesByKey
├── entityEntries
├── proxiesByKey
Map<EntityKey, Object>
같은 트랜잭션에서 true가 된다.
(Member,1) → Member@123
(Member,2) → Member@456
member1 == member2
엔티티 메타 정보 저장소이다.
Map<Object, EntityEntry>
엔티티 객체는 관리 정보가 된다.
Member@123 → EntityEntry
엔티티 상태 메타데이터다. Hibernate가 엔티티를 관리하기 위해 필요한 정보가 들어 있다.
EntityEntry
├─ Status status
├─ Object[] loadedState
├─ Object id
├─ EntityPersister persister
└─ LockMode lockMode
엔티티 상태를 나타낸다.
Dirty Checking의 핵심이다. 변경 감지를 수행한다.
loadedState = ["id=1", "name=old"]
// flush 시 비교
현재값 vs loadedState
Dirty Checking 과정
EntityEntry.loadedState
│
▼
현재 엔티티 값 비교
│
▼
다르면 UPDATE SQL 생성
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)