본문으로 건너뛰기
김신건의 로그

[JPA] PersistenceContext: 1차 캐시, dirty checking, flush

· 수정 · 📖 약 3분 · 1,132자/단어 #jpa #hibernate #persistence-context #entity-lifecycle
PersistenceContext, JPA 영속성 컨텍스트, 1차 캐시, first-level cache, dirty checking, flush, EntityManager state

정의

PersistenceContextEntityManager 가 관리하는 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 checkingEntityEntry 가 snapshot 보관, flush 시 비교setter 호출만으로 자동 UPDATE
Write-behindpersist/remove/dirty 변경 → action queueINSERT/UPDATE 모아서 한 번에 flush
Ordered flushFK 의존성 분석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 는 항상 같은 인스턴스:

java
@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());   // 같이 바뀜
}
stdout
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 또는 null
  • getReference: 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 호출 불필요:

java
@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 현재 비교, 차이가 있으면 UPDATE
실행된 SQL
select u.* from user u where u.id = ?      -- find
update user set name = ?, version = ? where id = ?   -- commit 시점

내부 동작:

  1. find 시점에 EntityEntrysnapshot 저장 (필드 값 복사본)
  2. 트랜잭션 진행 중 setter 호출
  3. 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가지

  1. commit() 직전 (가장 흔함, default)
  2. JPQL / Native query 실행 직전 (FlushModeType.AUTO, 기본)
  3. 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차 캐시 (@Cacheable JPA 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. 기본…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기