공부하기/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의 생명주기
- 비영속 : 영속성 컨텍스트가 모르는 상태, DB와 전혀 연관 없는 객체
- 영속 : 영속성 컨텍스트에 저장되어 관리되고 있는 상태, 데이터베이스와 동기화 되는 객체
- 준영속 : 영속성 컨텍스트에 저장되었다가 분리되어 더이상 관리되지 않는 객체 (데이터베이스 내에는 존재하지만 영속성 컨텍스트가 더 이상 관리하지 않아 변경 사항을 추적하지 않는 객체)
- `em.detach(member);` ← 특정 하나의 엔티티를 준영속 상태로 변경
- `em.clear();` ← 영속성 컨텍스트 초기화, 모든 엔티티를 준영속 상태로 변경
- 삭제 : 영속성 컨텍스트에 의해 삭제로 표시된 상태, 트랜잭션이 완료되면 데이터베이스에서 제거 처리 될 객체
영속성 컨텍스트의 동작에 대해 알아보자
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에 영구 저장한다.