공부하기/Spring

[Spring] 영속성 컨텍스트에 대해 정리해보자

다섯자두 2025. 2. 27. 14:52

영속성 컨텍스트

Spring Framework에서 영속성 컨텍스트란 어플리케이션 엔티티를 관리하는 환경으로,

어플리케이션과 데이터베이스 사이에서 객체를 보관/관리하는 저장소 역할을 한다.

트랜잭션 단위로 관리된다.

  • 애플리케이션 단위가 아니라 트랜잭션 단위로 관리된다.
  • 즉, 트랜잭션이 끝나면 해당 영속성 컨텍스트가 사라지면서 관리되던 엔티티는 준영속 상태가 된다.
  • 같은 트랜잭션 내에서는 같은 영속성 컨텍스트를 사용한다.
  • 하나의 Spring 어플리케이션에서 여러 개의 영속성 컨텍스트가 존재할 수 있다.

누가 관리하는가?

`EntityManagerFactory` : 애플리케이션 전체에서 하나만 생성되는 팩토리 객체 (싱글톤)

`EntityManager` : 영속성 컨텍스트를 직접 관리하는 JPA 핵심 객체 << 얘로 영속성 컨텍스트를 관리한다.

  • persist(), find(), remove() 등등을 수행하는 역할

JPA에서 영속성 컨텍스트의 동작

Spring에서 `@Transactional`을 통해 트랙잭션 시작 시

  • EntityManagerFactory가 EntityManager를 생성하고, 영속성 컨텍스트가 활성화된다.

트랜잭션이 종료될 경우

  • 트랜잭션이 commit 될 때, EntityManager의 `flush()`를 자동으로 호출한다.
  • 트랜잭션이 종료되면 EntityManager도 `close()`처리되며, 영속성 컨텍스트가 종료된다.
  • 관리되던 모든 엔티티는 준영속 상태가 된다.

JPA에서 Entity란?

영속성 컨텍스트에 관리되며, 데이터베이스의 테이블을 represent하는 클래스를 의미한다. (ex. User, Post, Comment etc...)

Entity의 생명주기

  1. 비영속 : 영속성 컨텍스트가 모르는 상태, DB와 전혀 연관 없는 객체
  2. 영속 : 영속성 컨텍스트에 저장되어 관리되고 있는 상태, 데이터베이스와 동기화 되는 객체
  3. 준영속 : 영속성 컨텍스트에 저장되었다가 분리되어 더이상 관리되지 않는 객체 (데이터베이스 내에는 존재하지만 영속성 컨텍스트가 더 이상 관리하지 않아 변경 사항을 추적하지 않는 객체)
    • `em.detach(member);` ← 특정 하나의 엔티티를 준영속 상태로 변경
    • `em.clear();` ← 영속성 컨텍스트 초기화, 모든 엔티티를 준영속 상태로 변경
  4. 삭제 : 영속성 컨텍스트에 의해 삭제로 표시된 상태, 트랜잭션이 완료되면 데이터베이스에서 제거 처리 될 객체

영속성 컨텍스트의 동작에 대해 알아보자

1차 캐시

영속된 상태의 Entity를 1차 캐시에 저장한다.

  • `EntityManager`의 `persist()`로 영속한다.
  • DB에 실제로 변화를 반영하기 전에 1차 캐시에 저장해놓고 사용한다.
  • 실제 insert 쿼리가 나가기 전에도 find로 해당 엔티티를 찾아올 수 있다.

DB에서 데이터를 가져온 후 1차 캐시에 저장해놓고 사용한다.

  • 한 번 조회해온 인스턴스를 1차 캐시에 캐싱해놓고 사용한다.
  • 캐싱된 데이터를 사용함으로써 해당 인스턴스를 조회할 때마다 조회 쿼리가 실행되지 않는다.

동일성을 보장한다.

  • 같은 1차 캐시에서 조회해서 가져온 객체 인스턴스는 같은 참조값을 가진다.

쓰기 지연

영속성 컨텍스트 내에 쓰기 지연 저장소가 존재한다.

새로운 데이터에 대한 insert 작업 요청이 들어왔을 때 바로 insert 쿼리문을 실행하지 않고, 쓰기 지연 저장소에 저장한다.

  • 이후 여러 개의 insert 쿼리문을 한 번에 실행한다.
  • 이를 통해 JDBC 커넥션 사용을 최소화할 수 있다. (매번 DB 커넥션을 열고 닫지 않고 한 번에 처리함으로써)
  • 객체의 상태를 추적하여 필요한 SQL문만 실행되도록 한다.

Batch 처리

`batch_size`를 설정하면 해당 사이즈만큼 SQL문을 하나로 합쳐 보낼 수 있다.

  • 커넥션을 효율적으로 사용할 뿐만 아니라, DB와의 통신 횟수도 줄여 성능을 향상시킨다.
// 전
INSERT INTO member (name) VALUES ('User0'); 
INSERT INTO member (name) VALUES ('User1'); 
INSERT INTO member (name) VALUES ('User2'); 
// ...
INSERT INTO member (name) VALUES ('User99'); 

// 후 (batch_size = 50 설정)
INSERT INTO member (name) VALUES ('User0'), ('User1'), ('User2'), ..., ('User49');
INSERT INTO member (name) VALUES ('User50'), ('User51'), ('User52'), ..., ('User99');

변경 감지 (Dirty Checking)

DB에서 엔티티를 최조 조회하여 1차 캐시에 저장할 때, 스냅샷 상태로 저장한다.

이후 SQL문을 실행하기 전에 1차 캐시에 저장된 스냅샷과 현재 객체의 상태를 비교한다.

  • 만일 변경사항이 있다면 자동으로 SQL문을 생성하여 함께 실행한다.
  • ex) 단순히 user.setName()을 해도 update문이 자동으로 작성된다.

정확히 언제 변경을 감지할까?

`EntityManager`의 `flush()`가 호출될 때, 스냅샷과 현재 객체 인스턴스간 변경 감지가 수행된다.

EntityManager의 flush()
- 영속성 컨텍스트에 있는 변경 사항을 DB에 반영한다.
- 즉, 엔티티의 변경 사항(insert, update, delete 등등)이 SQL로 변환되어 DB에 전달된다.
- 단, 트랜잭션을 종료하는 것이 아니므로 성공적으로 commit() 되지 않으면 rollback 될 수 있다.
EntityTransaction의 commit()
- flush()를 먼저 실행한다.
- 트랜잭션을 확정하고 변경 사항을 DB에 영구 저장한다.