전체 목록
SpringHard#76

JPA의 영속성 컨텍스트(Persistence Context)란 무엇이며 1차 캐시, 변경 감지의 동작 원리를 설명해주세요.

#Spring#JPA#영속성컨텍스트#ORM
힌트

EntityManager가 관리하는 엔티티 저장소로, 스냅샷을 통해 변경을 감지합니다.

정답 및 해설

JPA의 영속성 컨텍스트(Persistence Context)란 무엇이며 1차 캐시, 변경 감지의 동작 원리를 설명해주세요.

영속성 컨텍스트(Persistence Context)는 JPA에서 엔티티 객체를 관리하는 논리적인 저장 공간입니다. EntityManager가 이 공간을 통해 엔티티의 생명 주기를 관리하며, 데이터베이스와의 동기화를 담당합니다. 영속성 컨텍스트는 트랜잭션 범위와 함께 생성되고 소멸되며, 1차 캐시, 변경 감지, 지연 쓰기 등 강력한 기능을 제공합니다.

영속성 컨텍스트 개요

EntityManager와의 관계

// EntityManager가 영속성 컨텍스트를 통해 엔티티를 관리
@Service
@Transactional
public class UserService {

    @PersistenceContext
    private EntityManager em; // 영속성 컨텍스트에 접근하는 인터페이스

    public void example() {
        // 트랜잭션 시작 → 영속성 컨텍스트 생성
        User user = em.find(User.class, 1L); // 영속 상태
        user.setName("변경된 이름");
        // 트랜잭션 커밋 → 자동으로 DB에 UPDATE → 영속성 컨텍스트 소멸
    }
}

엔티티의 4가지 상태

JPA에서 엔티티는 영속성 컨텍스트와의 관계에 따라 4가지 상태를 가집니다.

  new User()          em.persist()        트랜잭션 커밋
  ┌──────────┐    ────────────────────>  ┌──────────────┐
  │  비영속   │                           │    영속       │
  │  (New)   │                           │  (Managed)   │
  └──────────┘                           └──────┬───────┘
                                                │
                                     em.detach() │ em.remove()
                                                │
                                    ┌───────────┴──────────┐
                                    │                      │
                               ┌────▼─────┐         ┌──────▼──────┐
                               │  준영속   │         │    삭제     │
                               │(Detached)│         │  (Removed)  │
                               └──────────┘         └─────────────┘

1. 비영속 (New/Transient)

// new 키워드로 생성, 영속성 컨텍스트와 무관
User user = new User();
user.setName("홍길동");
user.setEmail("hong@example.com");
// DB와 전혀 연관 없음, ID도 없음

2. 영속 (Managed)

@Transactional
public void example() {
    // em.persist()로 영속 상태로 전환
    User newUser = new User("홍길동", "hong@example.com");
    em.persist(newUser); // 영속 상태 → INSERT SQL은 커밋 시 실행

    // em.find()로 조회된 엔티티도 영속 상태
    User foundUser = em.find(User.class, 1L); // 영속 상태
}

3. 준영속 (Detached)

@Transactional
public void example() {
    User user = em.find(User.class, 1L);

    em.detach(user);    // 특정 엔티티만 준영속으로 전환
    // em.clear();      // 영속성 컨텍스트 전체 초기화 (모든 엔티티 준영속)
    // em.close();      // EntityManager 종료

    user.setName("변경"); // 변경 감지 안됨 → DB에 반영 안됨
}

4. 삭제 (Removed)

@Transactional
public void example() {
    User user = em.find(User.class, 1L);
    em.remove(user); // 삭제 예약 → 트랜잭션 커밋 시 DELETE SQL 실행
}

1차 캐시 (First-Level Cache)

동작 원리

영속성 컨텍스트 내부에는 Map 형태의 1차 캐시가 존재합니다. 키는 엔티티의 식별자(PK), 값은 엔티티 인스턴스입니다.

// 1차 캐시 구조 (개념적)
Map<EntityKey, Object> firstLevelCache = new HashMap<>();
// EntityKey = {EntityClass, primaryKey}

캐시 히트 과정

@Transactional
public void cacheExample() {
    // 첫 번째 조회: DB에서 SELECT
    User user1 = em.find(User.class, 1L);
    // → SELECT * FROM users WHERE id = 1 (SQL 실행)
    // → 결과를 1차 캐시에 저장

    // 두 번째 조회: 1차 캐시에서 반환 (SQL 미실행)
    User user2 = em.find(User.class, 1L);
    // → SQL 실행 없음! 캐시에서 바로 반환

    System.out.println(user1 == user2); // true (동일한 인스턴스)
}
em.find(User.class, 1L) 호출
        │
        ▼
  1차 캐시에 존재?
  ┌────┴────┐
  │YES      │NO
  ▼         ▼
캐시에서   DB에서 SELECT
반환       ↓
           1차 캐시에 저장
           ↓
           반환

1차 캐시의 범위

중요한 점은 1차 캐시는 트랜잭션 범위에서만 유효합니다. 요청이 끝나면 영속성 컨텍스트와 함께 소멸합니다. Redis나 Ehcache 같은 2차 캐시(공유 캐시)와는 다릅니다.

// 트랜잭션 A
@Transactional
public void transactionA() {
    User user = em.find(User.class, 1L); // DB 조회 후 캐시 저장
    // 트랜잭션 종료 → 1차 캐시 소멸
}

// 트랜잭션 B (새로운 영속성 컨텍스트)
@Transactional
public void transactionB() {
    User user = em.find(User.class, 1L); // 새로운 영속성 컨텍스트, 다시 DB 조회
}

변경 감지 (Dirty Checking)

동작 원리

JPA는 영속 상태의 엔티티를 관리할 때, 처음 조회(또는 저장)한 시점의 **스냅샷(Snapshot)**을 함께 저장합니다. 트랜잭션 커밋 시 현재 상태와 스냅샷을 비교하여 변경된 필드가 있으면 자동으로 UPDATE SQL을 생성합니다.

@Transactional
public void updateExample() {
    User user = em.find(User.class, 1L);
    // 스냅샷 저장: { id: 1, name: "홍길동", email: "hong@old.com" }

    user.setEmail("hong@new.com"); // 엔티티 수동 수정

    // 별도의 update() 메서드 호출 불필요
    // 트랜잭션 커밋 시 자동으로:
    // 1. 스냅샷과 현재 상태 비교
    // 2. email 필드 변경 감지
    // 3. UPDATE users SET email = 'hong@new.com' WHERE id = 1 실행
}
트랜잭션 커밋 호출
       │
       ▼
flush() 실행
       │
       ▼
영속 상태의 모든 엔티티 순회
       │
       ▼
스냅샷과 현재 상태 비교
       │
  변경 있음?
  ┌────┴────┐
  │YES      │NO
  ▼         ▼
UPDATE SQL  건너뜀
생성 및 실행

변경 감지가 동작하지 않는 경우

@Transactional
public void detachedExample() {
    User user = em.find(User.class, 1L);
    em.detach(user); // 준영속 상태로 전환

    user.setEmail("new@email.com");
    // 변경 감지 안됨! 준영속 상태는 관리 대상 아님
}

// 준영속 엔티티를 수정하려면 merge() 사용
@Transactional
public void mergeExample(User detachedUser) {
    User managedUser = em.merge(detachedUser); // 준영속 → 영속 상태로 병합
    // detachedUser는 여전히 준영속, managedUser가 영속 상태
}

쓰기 지연 (Write-Behind / Transactional Write-Behind)

JPA는 persist() 호출 시 즉시 DB에 INSERT하지 않고, SQL 저장소에 모아두었다가 트랜잭션 커밋 직전에 일괄 실행합니다.

@Transactional
public void batchInsertExample() {
    User user1 = new User("사용자1", "user1@example.com");
    User user2 = new User("사용자2", "user2@example.com");
    User user3 = new User("사용자3", "user3@example.com");

    em.persist(user1); // SQL 저장소에 INSERT 추가 (DB 미실행)
    em.persist(user2); // SQL 저장소에 INSERT 추가 (DB 미실행)
    em.persist(user3); // SQL 저장소에 INSERT 추가 (DB 미실행)

    // 트랜잭션 커밋 시 3개 INSERT를 한 번에 실행
    // → JDBC batch 처리로 성능 최적화 가능
}

flush() 수동 호출

커밋 전에 강제로 DB에 반영하고 싶을 때 사용합니다. (영속성 컨텍스트는 그대로 유지)

@Transactional
public void flushExample() {
    User user = new User("홍길동", "hong@example.com");
    em.persist(user);

    em.flush(); // 지금 당장 INSERT 실행 (트랜잭션은 유지)

    // 이후 같은 트랜잭션에서 user.getId() 사용 가능
    Long generatedId = user.getId();
}

영속성 컨텍스트의 이점 요약

@Service
@Transactional
public class ExampleService {

    public void demonstrateAllFeatures(Long userId) {
        // 1. 1차 캐시: 동일 트랜잭션 내 재조회 시 DB 접근 없음
        User user1 = em.find(User.class, userId); // DB 조회
        User user2 = em.find(User.class, userId); // 캐시 반환
        assert user1 == user2;

        // 2. 동일성 보장: == 비교 가능
        assert user1 == user2; // true

        // 3. 변경 감지: update() 호출 불필요
        user1.setName("새 이름");
        // 커밋 시 자동 UPDATE

        // 4. 쓰기 지연: persist()가 즉시 INSERT하지 않음
        User newUser = new User("신규", "new@email.com");
        em.persist(newUser); // SQL 저장소 대기

        // 5. 지연 로딩: 연관 엔티티를 실제 접근 시 로딩
        List<Order> orders = user1.getOrders(); // 프록시 반환
        orders.size(); // 여기서 SELECT orders WHERE user_id = ? 실행
    }
}

요약 표

기능설명이점
1차 캐시트랜잭션 내 동일 엔티티 재조회 시 캐시 반환DB 접근 최소화
동일성 보장같은 PK의 엔티티는 항상 동일 인스턴스== 비교 가능
변경 감지스냅샷 비교로 변경된 필드만 UPDATE명시적 update() 불필요
쓰기 지연SQL을 모아두었다가 커밋 시 일괄 실행JDBC 배치 최적화
지연 로딩연관 엔티티를 실제 접근 시 로딩불필요한 조인 방지
엔티티 상태영속성 컨텍스트 관리변경 감지DB 반영
비영속 (New)XXX
영속 (Managed)OO커밋 시 자동
준영속 (Detached)XXX
삭제 (Removed)O (삭제 예약)X커밋 시 DELETE