Java 아키텍처·Spring 용어 사전
JPA1차 캐시 · 쓰기 지연 · 변경 감지 · 더티 체킹

영속성 컨텍스트

엔티티를 관리하는 작업 공간. 1차 캐시·쓰기 지연·변경 감지·지연 로딩이 전부 여기서 나온다.

엔티티를 담아 관리하는 작업 공간. JPA의 거의 모든 동작이 여기서 파생된다.

네 가지 기능

  • 1차 캐시 — 같은 트랜잭션 안에서 같은 id 를 조회하면 DB 를 다시 안 간다
  • 동일성 보장 — 그 결과가 == 로 같다
  • 쓰기 지연 — persist 해도 곧바로 INSERT 하지 않고 모아 뒀다 flush 때 보낸다
  • 변경 감지 — 엔티티 필드를 바꾸기만 하면 UPDATE 가 나간다
@Transactional
void demo(Long id) {
    Member a = em.find(Member.class, id);   // SELECT 실행
    Member b = em.find(Member.class, id);   // DB 안 감 (1차 캐시)
    System.out.println(a == b);             // true (동일성 보장)

    a.setName("홍길동");                     // save() 를 부르지 않는다
}                                            // 커밋 시 UPDATE 가 나간다 (변경 감지)

변경 감지의 내부 — 스냅샷

엔티티를 영속 상태로 만들 때

  • 그 시점의 필드 값을 '스냅샷' 으로 함께 보관한다

flush 시점

  • 현재 값과 스냅샷을 비교한다
  • 다른 필드가 있으면 UPDATE 문을 만든다

그래서 save()를 부를 필요가 없다. 오히려 부르면 불필요한 merge가 일어날 수 있다.

준영속 상태에서는 동작하지 않는다

@Transactional
Member load(Long id) { return em.find(Member.class, id); }   // 트랜잭션 종료 → 준영속

void outside() {
    Member m = load(1L);
    m.setName("변경");         // 아무 일도 일어나지 않는다. 컨텍스트가 없다
}

1차 캐시는 스레드 전용이다

영속성 컨텍스트는 트랜잭션(스레드) 마다 따로 만들어진다

  • 다른 요청과 공유되지 않는다
  • "캐시" 라는 이름 때문에 성능 캐시로 오해하기 쉽지만,
    • 수명이 트랜잭션 하나이므로 캐시 효과는 거의 없다
  • 애플리케이션 범위 캐시는 2차 캐시(별도 설정)다

flush가 일어나는 시점

  • ① 트랜잭션 커밋 직전
  • ② JPQL 쿼리 실행 직전 (쓰기 지연된 내용이 반영돼야 정확한 결과가 나오므로)
  • ③ em.flush() 직접 호출

②를 모르면 "아직 커밋 안 했는데 왜 INSERT가 나갔지?" 에서 막힌다.

면접 함정

  • "1차 캐시로 성능이 좋아진다" → 수명이 트랜잭션 하나라 효과가 제한적이다. 주 목적은 동일성 보장과 변경 감지다.
  • "수정하려면 save()를 불러야 한다" → 영속 상태면 필드만 바꾸면 된다.

상태를 직접 확인해 보기

em.contains(entity);          // 영속 상태인가
em.detach(entity);            // 준영속으로 만든다
em.clear();                   // 컨텍스트를 통째로 비운다 (대량 처리에서 필수)
em.flush();                   // 쓰기 지연 내용을 지금 SQL 로 보낸다

대량 처리에서 반드시 비운다

@Transactional
void bulkInsert(List<Member> members) {
    for (int i = 0; i < members.size(); i++) {
        em.persist(members.get(i));
        if (i % 1000 == 0) {
            em.flush();      // SQL 전송
            em.clear();      // 컨텍스트 비우기 ← 없으면 OOM
        }
    }
}

비우지 않으면 영속성 컨텍스트가 10만 개 엔티티 + 10만 개 스냅샷을 들고 있게 된다

  • 메모리도 문제지만, flush 마다 전부 변경 감지를 돌아 급격히 느려진다

JPQL은 flush를 유발한다

em.persist(newMember);                                     // 아직 INSERT 안 나감
List<Member> all = em.createQuery("select m from Member m").getResultList();
// ← 이 순간 flush 가 일어난다 (방금 persist 한 것도 결과에 포함돼야 하므로)

"커밋도 안 했는데 왜 INSERT가 나갔지" 의 답이다. 반대로 em.find()는 1차 캐시를 먼저 보므로 flush를 유발하지 않는다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — JPA·영속성 — 영속성 컨텍스트·N+1