05. EntityManager와 Persistence Context
- -
이번 포스트에서는 JPA의 동작을 이해하는데 매우 중요한 EntityManager와 Persistence Context에 대해 알아보자.
EntityManager
EntityManager?
EntityManager라는 객체는 JPA에서 엔티티를 관리하는 핵심 객체이다. EntityManager는 엔티티의 생명주기를 관리하고 엔티티에 대한 C/R/U/D 처리를 위한 API를 제공한다.
다음은 EntityManager의 주요 메서드이다.
package jakarta.persistence;
public interface EntityManager extends AutoCloseable {
public void persist(Object entity); // create, update
public void remove(Object entity); // delete
public <T> T find(Class<T> entityClass, Object primaryKey); // select
public void detach(Object entity); // 상태 관리(분리)
public <T> T merge(T entity); // 상태 관리(병합)
public void flush(); // 즉시 반영
public void clear(); // 정리
public EntityTransaction getTransaction();
public void close();
}
그런데 우리는 Member를 저장하면서 한번도 EntityManager라는 객체를 만나본 적이 없다. 과연 Spring Data JPA에서는 필요 없어진 것일까?
JpaRepository와 EntityManager
JPA에서는 EntityManager가 Entity를 관리한다. Spring Data JPA도 JPA를 활용하기 때문에 EntityManager가 필요한데 도대체 어디에 있는 것일까?
Spring Data JPA에서는 JpaRepository를 통해 간접적으로 EntityManager를 사용하기 때문에 존재를 모르고 넘어가는 경우도 많다. 앞서 작성한 MemberRepository는 interface이고 이것에 대한 구현체는 AOP에 의한 동적 Proxy 객체인데 이 Proxy는 SimpleJpaRepository를 target으로 한다. 즉 has a 관계로 SimpleJpaRepository를 가지는 것이다.
@Test
@DisplayName("사용자 정의 Repository의 Proxy가 가지는 Target 타입 확인")
public void repositoryProxyType(){
// given
// when
Object target = AopTestUtils.getTargetObject(prepo);
// then
Assertions.assertTrue(target instanceof SimpleJpaRepository);
}
SimpleJpaRepository는 has a관계로 EntityManager를 가지고 있다. 다음은 SimpleJpaRepository의 일부이다.
package org.springframework.data.jpa.repository.support;
@Repository
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final EntityManager entityManager; // 내부적으로 EntityManager 사용 중
@Transactional
@Override
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
. . .
}
EntityManager가 내부에 깊숙히 숨겨있기 때문에 굳이 쓸 일은 없지만(필요 없으니 숨겨놨겠죠? ㅎ) 일부 복잡한 쿼리나 특정한 JPA의 기능이 필요해서 직접 사용하려는 경우는 직접 주입받아서 사용할 수 있다. 대표적으로 엔티티를 영속상태에서 준영속 상태로 변경하거나 그 반대의 경우이다.
@PersistenceContext // runtime에 EntityManager 주입
EntityManager em;
@Autowired // 단위 테스트 과정에서는 TestEntityManager 주입
TestEntityManager tem;
runtime에 @Autowired를 사용하는 것도 가능하지만 @PersistenceContext가 JPA에서 제공하는 표준 애너테이션이므로 이것을 사용하는 것이 표준화, 가독성에 도움이 될 수 있다. 테스트 과정에서 사용되는 TestEntityManager는 테스트를 위해 가볍게 디자인 된 객체이므로 테스트 시에는 EntityManager 보다는 TestEntityManager가 적절하다.
Persistence Context
Persistence Context
Persistence Context (이하 P.C) 는 EntityManager가 엔티티의 생명주기를 관리하는 환경으로 엔티티들의 상태 변화를 추적하고 관리하며 데이터의 일관성과 동일성을 유지하는 중요한 역할을 한다. 즉 P.C에서 EntityManager가Entity를 관리한다.
Persistence Context는 크게 1차 캐시 영역과 쓰기 지연 SQL 저장소로 구성된다.
1차 캐시 영역(이하 1차 캐시)은 하나의 T.X 내에서 엔티티의 식별자를 key로 엔티티 객체의 참조 값(레퍼런스)를 관리할 수 있는데 추가로 엔티티의 원본 내용에 해당하는 snapshot 정보도 함께 관리한다. snapshot는 엔티티가 관리되기 시작할 때(영속상태가 될 때) 생성된다. 만약 entity의 내용과 snapshot의 내용이 다른 경우(dirty한 상태)는 update가 필요한 상황으로 인지하게 된다.
쓰기 지연 SQL 저장소는(이하 SQL 저장소) insert, update, delete 쿼리를 바로 DB에 반영하지 않고 저장해두는 영역이다. 1차 캐시의 내용을 DB와 동기화 하기 위해서는 SQL 저장소의 내용이 DB로 전달되어야 하는데 이를 위해서는 flush() 메서드가 호출되야 한다. 아무튼 SQL 저장소에 여러 쿼리들을 모아놨다가 한번에 실행하는 덕에 네트워크 비용이 줄어들게 된다.
P.C의 주요 특징은 다음과 같다.
특징 | 설명 |
1차 캐시 기능 | 조회된 Entity는 P.C의 1차 캐시에 저장된다. Entity를 조회 할 때는 먼저 1차 캐시에서 @Id 기반으로 검색해보고 있으면 그 엔티티를 반환하며 없다면 DB를 조회하게 된다. 따라서 DB 접근을 최소화 하고 성능을 향상시킬 수 있다. |
지연 로딩 (Lazy Loading) |
연관관계가 있는 엔티티를 로딩할 때 필요한 순간까지 로딩을 지연시킨다. 이는 성능 최적화를 위해 필요한 정보만을 로딩하여 리소스 사용을 최소화 한다. 이는 Fetch 전략에 따라 다르게 동작한다. |
식별자를 통한 엔티티 관리 | 엔티티들은 기본 키(@Id)에 의해 관리된다. 따라서 P.C에 저장되기 위한 엔티티는 반드시 식별자를 가져야 하며 동일한 T.X 내에서 한 Entity 타입에서 동일한 기본 키를 가진 객체들은 동일하다(==). |
변경 감지 (Dirty Checking) |
P.C는 Snapshot과 Entity의 내용을 비교해서 변경 상황을 감지하고 SQL 저장소에 update 문장을 추가한다. 따라서 개발자가 명시적으로 update를 수행할 필요가 없다. |
쓰기 지연 방식 (Write-Behind) |
P.C는 엔티티의 상태가 변경(insert, update, delete)되더라도 즉시 DB에 반영하지 않고 flush()가 호출될 때 비로서 SQL 저장소의 내용을 DB로 전송해서 실행한다. 이를 통해 DB와의 통신 회수를 줄일 수 있다. |
P.C의 기본 동작
그럼 P.C는 어떻게 엔티티를 관리할까?
- 엔티티객체를 생성한다. 이 객체는 아직 P.C와 전혀 무관하다.
- EntityManager의 persist()를 호출하면 1차 캐시 영역에서 엔티티가 관리된다. snapshot에는 엔티티의 원본 정보가 저장된다.
- persist()의 결과로 SQL 저장소에는 insert 쿼리가 등록된다. 단 아직 insert가 실행되는 것은 아니다.
- 엔티티의 상태 값을 변경(member의 이름 변경 등)하면 snapshot 정보와 차이가 발생하게된다.
- 상태 값 변경의 결과로 SQL 저장소에는 update 쿼리가 등록된다. 역시 아직 실행되는 것은 아니다.
- 마지막으로 flush()가 호출되면 SQL 저장소의 내용이 DB에서 실행되고 트랜젝션이 커밋되면 DB에 최종 반영된다.
SQL 저장소 내용의 DB 반영 시점
Spring Data Jpa는 SQL 저장소에 실행할 SQL 들을 저장해 두고 쓰기 지연 정책을 펼친다. 그리고 flush()가 호출될 때 저장소의 내용을 실제 DB에서 실행시켜 1차 캐시와 DB를 동기화 시킨다. 그럼 언제 flush()가 호출될까?
- 명시적 호출: JpaRepository의 flush()를 명시적으로 호출할 때이다. 이는 궁긍적을 EntityManager의 flush()를 호출한다. saveAndFlush()역시 마찬가지이다.
- 트랜젝션 commit 시: TransactionManager를 통해 트랜젝션을 커밋할 때 flush()가 호출된다. T.X의 커밋 관련 내용은 서비스 레이어를 공부하면서 살펴보자.
- JPQL, Native SQL을 이용한 쿼리 실행 시: 일반적으로 JPA는 1차 캐시에서 데이터를 쿼리하는데 JPQL등은 실 DB에서 쿼리를 한다. 이를 위해 DB가 1차 캐시의 내용이 DB와 동기화 되어있어야 한다.
실습용 데이터 로딩
기본 데이터 활용을 위한 @Sql
P.C의 동작을 테스트하기 위해서는 사전에 일정 데이터가 있는 것이 유리하다. 테스트 과정에서 사용할 데이터를 로드하는 스크립트를 이용해보자. 이때 사용할 수 있는 애너테이션으로 @Sql이 있다.
public @interface Sql {
@AliasFor("scripts")
String[] value() default {};
@AliasFor("value")
String[] scripts() default {};
// 언제 스크립트를 실행할 것인가?
ExecutionPhase executionPhase() default ExecutionPhase.BEFORE_TEST_METHOD;
enum ExecutionPhase {
BEFORE_TEST_CLASS, AFTER_TEST_CLASS, BEFORE_TEST_METHOD, AFTER_TEST_METHOD
}
}
test 아래에 resources/init_sql 폴더를 생성하고 data_board.sql 파일을 저장 후 AbstractBoardTest에 위 애너테이션을 추가해주자.
@DataJpaTest
@ActiveProfiles({"dev"})
@Sql(scripts = {"classpath:/init_sql/data_board.sql"})
public class AbstractBoardTest { }
이제 다음의 테스트를 실행해서 데이터가 잘 로딩되었는지 확인해보자.
@Test
public void loadSqlTest(){
long count = prepo.count();
Assertions.assertEquals(18, count);
}
'Spring Model > 02. 객체 매핑과 P.C' 카테고리의 다른 글
06. 엔티티의 상태 관리 (0) | 2022.04.12 |
---|---|
04.자동 키 매핑 전략 (0) | 2020.06.07 |
03. Value Type (0) | 2020.06.07 |
02. OR-Mapping과 상속 (0) | 2020.06.02 |
01. Hello Spring Data JPA (0) | 2020.05.30 |
소중한 공감 감사합니다