관리 메뉴

The Nirsa Way

[Spring Data JPA] 영속성 컨텍스트와 생명주기 (1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩, 스냅샷) 본문

Development/Spring Data JPA

[Spring Data JPA] 영속성 컨텍스트와 생명주기 (1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩, 스냅샷)

KoreaNirsa 2025. 7. 17. 17:44
반응형

 

영속성 컨텍스트(Persistence Context)란?

JPA에서 엔티티 객체를 보관하는 저장소로써 EntityManager를 통해 엔티티를 관리하는 1차 캐시라고 볼 수 있습니다. 영속성 컨텍스트를 사용함으로써 아래와 같은 특징을 가지게 됩니다.

  1. 1차 캐시 (First-Level Cache) : 동일한 엔티티를 여러 번 조회하면 DB에 재접근 하지 않고 캐시에서 반환
  2. 동일성 보장 (Write-behind SQL Store) : 동일한 식별자를 가진 엔티티에 대해 항상 같은 인스턴스 반환 (== 비교 연산자로도 동일한 인스턴스로 인식함)
  3. 쓰기 지연 (SQL 저장소) : 엔티티의 필드 변경 시 SQL이 즉시 실행되지 않고 트랜잭션 커밋 시 한번이 실행
  4. 변경 감지 (Dirty Checking) : 엔티티의 값이 변경되면 커밋 시 자동으로 update하여 변경된 내용을 DB에 반영
  5. 지연 로딩 (Lazy Loading) : 연관된 데이터는 실제 사용할 때 로딩 (프록시 객체로 대기)

즉 JPA의 영속성 컨텍스트는 스프링과 DB 사이에서 객체(엔티티)를 중심으로 데이터 변경을 추적 및 최적화하는 핵심 저장소로써 SQL 없이도 ORM 기능을 수행할 수 있도록 도와주는 핵심 매커니즘이라고 할 수 있습니다.


1차 캐시와 동일성 보장

1차캐시란 JPA에서 EntityManager가 관리하는 영속성 컨텍스트 내부의 Map 자료구조이며 식별자로 저장되고 관리됩니다. 영속성 컨텍스트는 hibernate의 PersistenceContext에 의해 관리 및 사용됩니다.

org.hibernate.engine.spi.PersistenceContext

find()를 호출하려 할 때 동작하는 순서는 아래와 같습니다.

  1. find() 호출
  2. 1차 캐시에서 Member(1L) 존재 여부 확인
  3. 존재하면 캐시에서 바로 반환
  4. 존재하지 않으면 DB 조회 → 1차 캐시에 저장 →  반환

즉 1차 캐시에 이미 존재하는 엔티티라면 바로 반환하기 때문에 같은 데이터를 여러 번 요청해도 DB에는 한 번만 접근됩니다.  

Member member1 = em.find(Member.class, 1L);

 

아래의 코드를 확인해 보면 다음과 같은 흐름을 가집니다

  1. TestRepository.save(member) 호출 : 1차 캐시에 엔티티 저장
  2. Member member1 = em.find(Member.class, member.getId()) : 1차 캐시 조회 → 해당 엔티티 존재 → 그대로 반환
  3. Member member2 = em.find(Member.class, member.getId()) : 1차 캐시 조회 → 해당 엔티티 존재 → 그대로 반환

이러한 이유로 member1과 member2는 동일한 인스턴스를 반환 받기 때문에 true가 나옵니다. (동일성 보장)

@Transactional
public void testFirstLevelCache() {
    // 테스트용 회원 등록
    Member member = new Member();
    member.setName("홍길동");
    TestRepository.save(member);

    // 동일 트랜잭션 내에서 두 번 조회
    Member member1 = em.find(Member.class, member.getId());
    Member member2 = em.find(Member.class, member.getId());

    // 결과 비교 (true)
    System.out.println("member1 == member2 ? " + (member1 == member2));
}

쓰기 지연 (SQL 저장소)

JPA에서 persist() 또는 엔티티 필드 값을 변경하더라도 즉시 DB에 SQL을 실행하지 않고 SQL 저장소에 쌓아두었다가 트랜잭션 커밋 시 한꺼번에 실행하는 전략입니다. 

아래와 같이 엔티티의 필드가 변경 되더라도 즉시 SQL 실행을 하지 않고 마지막 트랜잭션 커밋 시점 또는 개발자가 flush()를 직접 호출 할 때 해당 SQL들이 한꺼번에 실행됩니다.

@Transactional
public void saveMember() {
    Member member = new Member();
    member.setName("홍길동");

    em.persist(member); // INSERT SQL 아직 실행 X
    member.setName("김길동"); // UPDATE도 아직 실행 X

    // 트랜잭션 커밋 시점 또는 flush() 호출 시 INSERT, UPDATE 실행
}

이러한 전략을 사용하는 이유는 DB의 접근을 최소화하고 한꺼번에 보냄으로써 네트워크 비용 절감, 변경 감지(Dirty Checking), 트랜잭션 롤백에 대한 대비를 하기 위함입니다. 만약 코드에서 중간에 예외가 발생한다면 DB에 SQL을 실행한 상태가 아니므로 전혀 반영되지 않도록 하는 것 입니다.

즉, 이러한 상태를 통해 아래와 같은 구조를 가지게 됩니다.

  1. Member member = new Member() : 비영속 상태
  2. member.setName("홍길동") : 비영속 상태 (자바 객체의 필드 변경)
  3. em.persist(member) :  영속 상태로 전환 → 1차 캐시에 저장   SQL 저장소에 insert 쿼리 생성 및 보관
  4. member.setName("김길동") : 현재 member는 영속 상태이므로 JPA가 변경 감지(Dirty Checking) 대상으로 추적. 단, update 쿼리는 현재 시점에 생성되지 않으며 스냅샷(초기 상태)과의 차이만 기록
  5. 트랜잭션 커밋 시점 : 최종적으로 커밋을 하기 전 flush()를 호출 → 변경 감지(Dirty Checking) 수행 →  insert 실행 및 update 쿼리 생성 후 실행
  6. 트랜잭션 커밋 : 마지막으로 커밋을 수행하여 DB에 반영 후 트랜잭션 종료

 

만약 위의 과정에서 3번까지 진행되었다면 아래와 같은 그림이 됩니다. (실제 스냅샷 저장소의 필드는 객체 배열 형태로 저장되며 초기값을 저장해두는 공간이라고 생각하시면 됩니다) 

 

이후 member.setName("김길동")를 실행하면 1차 캐시에 있는 필드 값이 변경됩니다. 아직 update 쿼리문은 생성되지 않았습니다.

 

마지막으로 flush() 를 호출하게 되면 1차 캐시에 있는 필드와 스냅샷 저장소에 있는 필드가 서로 다름(Dirty Checking)을 감지하여 쿼리를 생성 후 insert, update를 DB에 실행하고 커밋을 하게 됩니다.


생명 주기

JPA 에서 생명 주기는 아래와 같이 크게 4가지로 나뉘어 집니다. 즉 해당 상태들은 영속성 컨텍스트로써 관리 되냐 안되냐를 상태를 통해 나타내는 방법입니다.

 

해당 상태들을 관리하기 위해 메서드도 제공을 해줍니다. 아래의 코드에 없는 clear()는 모든 영속 엔티티를 준영속으로 상태를 바꿉니다. 즉, 더이상 영속성 엔티티로써 사용되지 않도록 해주는 메서드입니다.

// 1. 비영속 상태
Member member = new Member(); // 아무런 관리 안 됨
member.setName("홍길동");

// 2. 영속 상태 (엔티티 매니저에 등록됨)
em.persist(member); // INSERT SQL은 아직 안 나감

// 3. 변경 감지 (Dirty Checking)
member.setName("김길동"); // em이 추적 중이므로 나중에 UPDATE 발생

// 4. 준영속 상태
em.detach(member); // em이 관리 안함 → 더티 체킹 안됨

// 5. 삭제 상태
em.remove(member); // 트랜잭션 커밋 시 DELETE SQL 실행

 

반응형