콘텐츠로 이동

Persistence Context

엔티티를 저장하는 환경으로, EntityManager를 통해 접근하고 관리되는 엔티티의 집합이다.

스프링 환경에서의 영속성 컨텍스트

섹션 제목: “스프링 환경에서의 영속성 컨텍스트”

스프링 프레임워크와 같은 컨테이너 환경에서는 EntityManager와 영속성 컨텍스트가 직접 관리되지 않고 컨테이너에 의해 관리된다.

  • 생명주기: 영속성 컨텍스트는 트랜잭션의 생명주기와 동일하게 유지
    • 스프링의 @Transactional이 붙은 메서드가 시작될 때 생성되고, 메서드가 종료되어 트랜잭션이 커밋되거나 롤백될 때 함께 소멸
  • 공유: 같은 트랜잭션 내에서는 여러 클래스에서의 주입 받은 EntityManager는 모두 동일한 영속성 컨텍스트 인스턴스 공유
    • 스프링의 트랜잭션 동기화 매니저를 통해 보장

결과으로 @Transactional 적용을 통해 트랜잭션 범위 영속성 컨텍스트 (Transaction-Scoped Persistence Context)를 지정하고 사용할 수 있다.

flowchart TD
New((New)) -->|persist| Managed((Managed))
Managed -->|" detach() / clear() / close() "| Detached((Detached))
Detached -->|merge| Managed
Managed -->|remove| Removed((Removed))
Removed -->|persist| Managed
Managed -->|flush| DB[(DB)]
DB -->|find| Managed
Removed -->|flush| DB
  1. 비영속 (New/Transient): 순수 자바 객체 상태
    • 영속성 컨텍스트나 데이터베이스와 전혀 관계가 없는 상태
  2. 영속 (Managed): entityManager.persist() 등을 통해 영속성 컨텍스트에 의해 관리되는 상태
    • 1차 캐시에 저장
    • 변경 감지 등의 기능을 제공 받음
  3. 준영속 (Detached): 영속성 컨텍스트에 의해 관리되다가 분리된 상태
    • 영속성 컨텍스트가 소멸 (트랜잭션 종료)되거나 detach(), clear() 호출 시 전환
  4. 삭제 (Removed): entityManager.remove()를 통해 삭제된 상태
    • 트랜잭션 커밋 시 실제 데이터베이스에서 삭제
class Example {
public static void main(String[] args) {
Member member = new Member(); // 1. 비영속
member.setId("member1");
member.setUsername("회원1");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(member); // 2. 영속
entityManager.detach(member); // 3. 준영속
entityManager.remove(member); // 4. 삭제
}
}

영속성 컨텍스트는 내부에 맵과 유사한 1차 캐시를 가지고 있으며, 엔티티의 식별자 (@Id)를 키로, 엔티티 인스턴스를 값으로 저장한다.

@IdEntity
1Member(id=1, username=“회원1”)
  • em.find() 호출 시, 우선 1차 캐시에서 엔티티 조회
  • 캐시에 존재하면 DB 조회 없이 즉시 해당 인스턴스를 반환
  • 캐시에 없으면 DB에서 조회한 후, 그 결과를 1차 캐시에 저장하고 반환

이러한 메커니즘 덕분에 같은 트랜잭션 내에서 동일한 식별자로 조회한 엔티티는 항상 같은 메모리 주소를 가진 인스턴스임이 보장된다 (member1 == member2 결과가 true).

트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)

섹션 제목: “트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)”
class Example {
public static void main(String[] args) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");
entityManager.persist(member1);
entityManager.persist(member2);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않음
entityManager.getTransaction().commit(); // INSERT SQL 수행
}
}

영속성 컨텍스트를 이용한 쓰기 지연 작업은 다음과 같은 절차로 진행된다.

  1. em.persist() 호출 시, 엔티티를 1차 캐시에 저장
    • 그와 동시에 INSERT SQL을 생성하여 쓰기 지연 SQL 저장소에 등록
  2. 트랜잭션 커밋 시점에 flush()가 호출
    • 쓰기 지연 SQL 저장소에 등록된 쿼리를 데이터베이스에 전송

JPA는 엔티티가 1차 캐시에 처음 저장될 때의 상태를 스냅샷으로 만들어 별도로 보관한다.

class Example {
public static void main(String[] args) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
Member member = entityManager.find(Member.class, 150L);
member.setUsername("OGU");
// persist()와 같은 메서드 호출 X
entityManager.getTransaction().commit(); // UPDATE SQL 수행
}
}
  1. 트랜잭션 커밋 시점에 flush() 호출
  2. JPA는 1차 캐시에 있는 모든 영속 상태 엔티티와 이전에 만들어둔 스냅샷을 비교
  3. 만약 변경된 점이 있다면, UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장
  4. 쓰기 지연 SQL 저장소의 쿼리들을 데이터베이스에 전송하고 트랜잭션을 커밋

OSIV (Open Session In View)와 커넥션 고갈

섹션 제목: “OSIV (Open Session In View)와 커넥션 고갈”

OSIV는 영속성 컨텍스트의 생명주기를 트랜잭션 범위 (Service 계층)를 넘어 클라이언트 응답 (Controller 계층) 시점까지 확장하는 기능이다.

  • 장점: 지연 로딩 (Lazy Loading)을 컨트롤러 계층에서도 자유롭게 호출할 수 있어 초기 개발 생산성 향상
  • 치명적 트레이드오프: 영속성 컨텍스트가 뷰 렌더링 끝까지 유지되므로, 할당받은 데이터베이스 커넥션 역시 클라이언트 응답이 끝날 때까지 반환되지 않고 점유
  • 장애 시나리오
    • 컨트롤러에서 지연이 긴 외부 API (결제 등)를 호출할 경우, 해당 작업이 끝날 때까지 DB 커넥션을 놓지 않음
    • 대규모 트래픽 유입 시 DB 쿼리 실행이 진작에 끝났음에도 불구하고 커넥션이 반환되지 않음
    • 결과적으로 커넥션 풀 고갈 (Connection Pool Exhaustion) 현상이 발생하며 전체 시스템 장애로 확산
  • 실무적 대안: 대량의 트래픽을 다루는 환경에서는 반드시 OSIV 설정을 false로 끄고 사용
    • 지연 로딩이 필요한 데이터는 트랜잭션이 끝나기 전에 페치 조인 (Fetch Join)이나 DTO 변환을 통해 미리 가져오도록 설계

마지막 업데이트:

Spring