[JPA] PersistenceContext: 1차 캐시, dirty checking, flush
정의
PersistenceContext 는 EntityManager 가 관리하는 entity 의 메모리 저장소 + 변경 추적기. 트랜잭션 안에서만 살아 있다 (@Transactional 진입 시 생성, commit/rollback 시 소멸).
DB 와 분리해 이해:
- DB: 영속 상태 (모든 트랜잭션에 공유, persistent)
- PersistenceContext: 메모리 상태 (트랜잭션 1개 안에서만, transactional)
관련 비교 페이지: spring-jpa-context-vs-unit (PersistenceContext vs PersistenceUnit 명확히 구분).
시각화
4개 상태 전이가 PC 와 DB 양쪽에서 어떻게 진행되는지 시간순으로 표시.
핵심 책임 4가지
| 책임 | 메커니즘 | 효과 |
|---|---|---|
| Identity guarantee | 같은 id → 같은 인스턴스 보장 | em.find(...) 두 번 호출해도 == 비교 통과 |
| Dirty checking | EntityEntry 가 snapshot 보관, flush 시 비교 | setter 호출만으로 자동 UPDATE |
| Write-behind | persist/remove/dirty 변경 → action queue | INSERT/UPDATE 모아서 한 번에 flush |
| Ordered flush | FK 의존성 분석 | INSERT → UPDATE → Collection 변경 → DELETE 순 |
이 4가지가 JPA 의 핵심. 모든 함정과 패턴은 이 네 가지에서 파생.
Entity 4가지 상태
[new/transient] new User() ← JPA 모름, PC 밖
↓ persist()
[managed] PC 안에 보관, 변경 추적
↓ flush() / commit()
└─→ DB 반영 (INSERT/UPDATE)
↓ remove()
[removed] DELETE 대기, flush 시 DB 반영
↓ detach() / clear() / em 종료
[detached] PC 밖, 변경 추적 안 됨, ID 는 유지
↑ merge() ← 새 managed 인스턴스 반환
코드 흐름:
EntityManager em = ...;
User u = new User("Alice"); // transient
em.persist(u); // managed (PC 등록)
u.setName("Bob"); // 자동 추적
em.flush(); // DB 반영 (UPDATE)
em.detach(u); // detached
u.setName("Carol"); // 추적 안 됨
User merged = em.merge(u); // 새 managed 인스턴스 반환
em.remove(merged); // removed
책임 1: Identity Guarantee (1차 캐시)
같은 트랜잭션 안에서 같은 id 의 entity 는 항상 같은 인스턴스:
@Transactional
public void demo() {
User u1 = em.find(User.class, 1L); // SELECT
User u2 = em.find(User.class, 1L); // 캐시에서 (SQL 없음)
System.out.println("same instance: " + (u1 == u2));
u1.setName("Alice2");
System.out.println("u2.name: " + u2.getName()); // 같이 바뀜
}Hibernate: select u.* from user u where u.id = ?
same instance: true
u2.name: Alice2두 번째 find 는 SQL 안 보냄. identity guarantee: 같은 PC 안에서 equals 가 아닌 == 비교 가능.
em.find vs em.getReference:
find: 즉시 SELECT, entity 또는 nullgetReference: proxy 반환, 첫 메서드 호출 시 SELECT (lazy)
Post p = new Post();
p.setAuthor(em.getReference(User.class, authorId)); // User SELECT 안 함
em.persist(p); // INSERT post 만
getReference 가 반환한 proxy 의 메서드를 호출하면 그때서야 SELECT. 존재하지 않는 id 면 EntityNotFoundException. 트랜잭션 밖에서 proxy 메서드 호출 = LazyInitializationException.
책임 2: Dirty Checking
managed entity 의 필드 변경은 자동 감지. setter 호출 후 별도 update 호출 불필요:
@Transactional
public void changeName(Long id, String newName) {
User u = em.find(User.class, id);
u.setName(newName);
// em.update() 같은 거 호출 안 함
}
// 메서드 종료 = commit = flush
// Hibernate 가 snapshot vs 현재 비교, 차이가 있으면 UPDATEselect u.* from user u where u.id = ? -- find
update user set name = ?, version = ? where id = ? -- commit 시점내부 동작:
find시점에EntityEntry가 snapshot 저장 (필드 값 복사본)- 트랜잭션 진행 중 setter 호출
- flush 시점에 snapshot vs 현재 비교, 변경된 컬럼만 UPDATE
Cost: 모든 managed entity 의 모든 필드를 매번 비교. entity 수천 개면 성능 영향. 대용량 배치엔 StatelessSession (Hibernate, PC 자체를 끔).
책임 3: Write-Behind (action queue)
persist, remove, dirty 변경은 즉시 SQL 안 보내고 action queue 에 쌓임. flush 시점에 모아서 실행:
@Transactional
public void demo() {
em.persist(new User("A")); // INSERT 대기열
em.persist(new User("B")); // INSERT 대기열
em.persist(new User("C")); // INSERT 대기열
// 아직 SQL 안 감
// commit 시 한꺼번에 flush → 3 INSERT
}
Flush 시점 3가지
- commit() 직전 (가장 흔함, default)
- JPQL / Native query 실행 직전 (
FlushModeType.AUTO, 기본) - em.flush() 명시 호출
em.persist(user);
em.createQuery("SELECT u FROM User u WHERE u.name = ?1", User.class)
.setParameter(1, "Alice")
.getSingleResult();
// 위 query 실행 직전에 user INSERT 됨
// (안 그러면 새로 persist 한 user 가 query 에 안 잡혀 결과 불일치)
FlushModeType.COMMIT 으로 자동 flush 끄면 명시 호출까지 SQL 안 나감. query 결과가 미반영 상태에 기반할 수 있어 위험.
책임 4: Ordered Flush
Hibernate 가 FK 의존성 분석 후 SQL 순서 자동 조정:
1. INSERT (parent → child 순서)
2. UPDATE
3. Collection element 삭제
4. Collection element INSERT
5. DELETE (child → parent)
UNIQUE 제약 + DELETE 후 INSERT 같은 경우 Constraint violation 가능 (둘 다 flush 시점에 같이 실행). em.flush() 명시 호출로 분리하거나 deferred constraint 사용.
merge: detached → managed
User detached = ...; // 트랜잭션 밖에서 가져온 객체
@Transactional
public void update(User detached) {
User managed = em.merge(detached); // 새 managed 인스턴스 반환
// detached 는 여전히 detached, managed 가 새로 만들어짐
}
함정: merge 는 새 인스턴스를 반환. 원본 reference 를 그대로 사용하면 변경 추적 안 됨:
em.merge(detached);
detached.setName("X"); // ❌ 추적 안 됨 (detached 그대로)
User m = em.merge(detached);
m.setName("X"); // ✓ dirty checking 됨
Spring Data JPA save():
- id 없음 →
persist호출 - id 있음 →
merge호출 (불필요한 SELECT 발생)
새 entity 에 미리 id 를 set 하면 의도와 달리 merge → 성능 손해. 직접 em.persist 호출하거나 Persistable 인터페이스 활용.
clear, detach: 메모리 관리
em.clear(); // PC 전체 비움 = 모든 entity detach
em.detach(u); // 특정 entity 만
대용량 배치:
@Transactional
public void importMany(Stream<UserDto> input) {
int i = 0;
for (UserDto dto : input) {
em.persist(new User(dto));
if (++i % 1000 == 0) {
em.flush(); // INSERT 보내고
em.clear(); // PC 비우기
}
}
}
em.clear() 없으면 PC 에 entity 누적 → OOM. 1000 row 마다 flush + clear 패턴이 일반적.
트랜잭션 경계 = PC 경계
[트랜잭션 진입]
↓
[PersistenceContext 생성]
↓
[비즈니스 로직 (find, persist, merge, remove, dirty 변경)]
↓
[트랜잭션 commit]
├─→ PC 의 모든 dirty entity → flush → DB
└─→ PC 소멸
규칙:
@Transactional없이EntityManager사용 금지- 트랜잭션 종료 = 모든 managed entity 자동 detached
- 새 트랜잭션 = 새 PC (다른 인스턴스)
OSIV (Open Session In View)
spring.jpa.open-in-view: true # Spring Boot 기본
DispatcherServlet 진입 시 EntityManager 열고, view 렌더링까지 유지. lazy 로딩이 view layer 까지 가능.
운영 권장: false:
- 컨트롤러 / view 가 DB 커넥션 점유 → 풀 고갈
- lazy 가 service 밖에서 일어남 (예측 어려움)
- 트랜잭션 경계가 흐려짐
false 로 두면 lazy 접근을 service 안 (@Transactional) 으로 강제. controller 는 Optional<DTO> 받아 매핑.
함정과 베스트 프랙티스
@Transactional없이 EntityManager 사용 금지: 트랜잭션 = PC 수명- detached 객체 setter 변경은 무시:
merge후 반환된 새 인스턴스에 변경 StatelessSession(Hibernate) 대용량 배치 시: dirty checking 끔, PC 없음em.flush()명시 호출 신중: 예외 발생 위치를 옮기는 효과- 1차 캐시는 트랜잭션 범위: 2차 캐시 (
@CacheableJPA L2) 는 다른 개념, 멀티 인스턴스 stale 위험 getReference의 lazy + 트랜잭션 밖 호출 =LazyInitializationException- save 의 merge/persist 자동 판별 함정: id 가 있는 새 entity 는
persist직접 호출 - OSIV false + DTO projection: 가장 안전한 운영 패턴
관련 위키
이 글의 용어 (8개)
- [JPA] CascadeType과 orphanRemovalspring
- 정의 JPA 의 entity 연산 ( , , , , ) 을 연관 entity 에 자동 전파 하는 메커니즘. 같이 선언. 별개 개념인 orphanRemoval 은 부모 컬렉션에서 …
- [JPA] Entity Lifecycle Callbacks와 Auditingspring
- 정의 Entity 의 라이프사이클 (persist / update / load / remove) 특정 시점에 자동 호출되는 메서드. 두 가지 형태: 1. Entity 내부 메서드…
- [JPA] FetchType: LAZY vs EAGER, N+1, fetch joinspring
- 정의 FetchType 은 JPA 가 연관 entity 를 언제 로드할지 결정하는 hint. - : entity 로딩 시 즉시 함께 SELECT - : 실제 사용 시점 (prox…
- [JPA] PersistenceContext vs PersistenceUnit 비교spring
- 정의 JPA 의 두 개념은 레벨이 다르다. 이름이 비슷해서 자주 혼동하지만: - PersistenceUnit = 설정 단위 (정적, 앱 1번) - PersistenceContex…
- [JPA] PersistenceUnit: EntityManagerFactory의 정의 단위spring
- 정의 PersistenceUnit 은 하나의 논리적 DB 연결 + entity 매핑 집합 을 정의한 단위. 결과물은 1개. JPA 아키텍처의 계층: 가 트랜잭션 범위의 메모리 저…
- [Spring] RequestContextHolder, LocaleContextHolder: ThreadLocal 기반 요청 컨텍스트spring
- 정의 Spring 은 HTTP 요청 / 응답 / locale / 트랜잭션 / 인증 같은 요청 범위 데이터를 ThreadLocal 에 저장 해 전역 노출한다. 메서드 인자로 일일이…
- [Spring] Spring Data JPA: Repository, Query Methodsspring
- 정의 Spring Data JPA는 JPA(Hibernate) 위에 Repository 추상화를 얹어 boilerplate를 제거. 인터페이스만 정의하면 Spring이 구현체 자…
- [Spring] Transaction: @Transactional, propagation, isolationspring
- 정의 Spring 트랜잭션은 AOP 기반 선언형 트랜잭션 관리. 한 줄로 메서드 전체를 트랜잭션 범위로. JDBC, JPA, MongoDB 등 backend에 동일 API. 기본…
이 개념을 다룬 위키 페이지 (18)
- wiki[DB Internals] MVCC: Multi-Version Concurrency Control
- wikiN+1 쿼리 문제
- wiki[비교] JPA EAGER vs Rails eager_load
- wiki[Spring Boot] Database Initialization: schema.sql, Flyway, Liquibase
- wiki[Spring Boot] Multi-DataSource: 복수 DB 연결과 트랜잭션
- wiki[Spring Boot] Hibernate Naming Strategy
- wiki[JPA] CascadeType과 orphanRemoval
- wiki[JPA] PersistenceContext vs PersistenceUnit 비교
- wiki[JPA] @Embeddable, @Embedded, @Converter
- wiki[JPA] fetch join: JOIN FETCH 로 연관 entity 즉시 로드
- wiki[JPA] FetchType: LAZY vs EAGER, N+1, fetch join
- wiki[JPA] 상속 전략: SINGLE_TABLE, JOINED, TABLE_PER_CLASS
- wiki[JPA] Entity Lifecycle Callbacks와 Auditing
- wiki[JPA] Optimistic Lock과 Pessimistic Lock
- wiki[JPA] PersistenceUnit: EntityManagerFactory의 정의 단위
- wiki[JPA] JPQL: Jakarta Persistence Query Language
- wiki[JPA] 동적 쿼리: Criteria API, QueryDSL, Specification
- wiki[JPA] JPQL DTO Projection: constructor expression, interface, record
💬 댓글