본문 바로가기
JPA

엔티티 매니저와 영속성 컨텍스트

by hseong 2023. 7. 30.

1. 엔티티 매니저란?

엔티티 매니저는 엔티티를 저장, 수정, 삭제, 조회 등 엔티티와 관련된 모든 일을 처리하는 관리자이다. 내부에 데이터소스를 유지하면서 데이터베이스와 통신한다. 개발자 입장에서 엔티티 매니저를 엔티티를 저장하는 가상의 데이터베이스로 생각하면 된다.

이때, 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드간에 공유하거나 재사용해서는 안 된다.

이러한 엔티티 매니저는 엔티티 매니저 팩토리에 의해서 생성된다.

엔티티 매니저 팩토리

엔티티 매니저 팩토리는 엔티티를 만들기 위한 공장이다. JPA 구현체에 따라 데이터베이스 커넥션 풀도 생성하므로 엔티티 매니저 팩토리를 생성하는데 많은 비용이 든다. 때문에 어플리케이션은 하나의 엔티티 매니저 팩토리를 공유해서 사용해야 한다.

엔티티 매니저 팩톨는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유해서는 안 된다.

순수한 JPA를 이용하여 개발하는 경우 다음과 같이 엔티티 매니저 팩토리를 만들기 위한 설정 정보인 persistence.xml을 작성해주어야 한다.

persistence.xml

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             version="2.1">
    <persistence-unit name="jpa">
        <properties>
            <property name="jakarta.persistence.jdbc.driver"
                      value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.user"
                      value="sa"/>
            <property name="jakarta.persistence.jdbc.password"
                      value=""/>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:mem:test"/>
            <property name="hibernate.dialect"
                      value="org.hibernate.dialect.H2Dialect"/>
        </properties>
    </persistence-unit>
</persistence>

어플리케이션에서는 다음과 같이 엔티티 매니저 팩토리를 생성해서 JPA를 사용하면 된다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTranscation();

try {
    tx.begin();
    Member member = new Member();
    em.persist(member);
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}
emf.close();

Persistence.createEntityManagerFactory("jpa")를 호출하면 persistence.xml에 있는 정보를 바탕으로 엔티티 매니저 팩토리를 생성한다.

스프링 부트를 사용하는 경우 위와 같은 persistence.xml이나 LocalContainerEntityManagerFactoryBean을 등록하지 않아도 된다. 부트는 이러한 복잡한 설정을 자동화해준다. application.yml에 필요한 설정 정보를 등록해주면 부트가 자동으로 해결해준다.

application.yml

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true

logging.level:
  org.hibernate.SQL: debug

EntityManagerFactory는 부트에 의해 빈으로 등록된다. 엔티티 매니저를 사용하고 싶다면 @PersistenceContext를 사용하면 된다. 부트는 트랜잭션 범위 내에서 매번 새로운 엔티티 매니저를 만들어 주입해준다.

스프링 데이터 JPA를 사용한다면 어노테이션조차 생략할 수 있다.



2. 영속성 컨텍스트란?

영속성 컨텍스트(persistence context)란 엔티티를 영구 저장하는 환경이다. 엔티티 매니저로 저장, 조회한 엔티티는 영속성 컨텍스트에 보관하고 관리한다.

엔티티의 생명주기

엔티티는 4가지 상태가 존재한다.

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed): 영속성 컨텍스트에 저장된 상태
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed): 삭제된 상태

1) 비영속

엔티티 객체를 생성만 하고 아직 저장하지 않은 상태이다. 따라서 영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없다.

Member member = new Member("memberA");

2) 영속

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한다. 이렇게 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다.

em.persist(member);

3) 준영속

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않는 상태를 준영속 상태라 한다.

em.detach(member);  //특정 엔티티를 영속성 컨텍스트에서 분리
em.close();  //영속성 컨텍스트 닫기
em.clear();  //영속성 컨텍스트 초기화

따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다. 엔티티를 준영속 상태로 만드는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.

4) 삭제

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.

em.remove(member);


3. 영속성 컨텍스트의 특징

영속성 컨텍스트와 식별자 값

영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다.

영속성 컨텍스트와 데이터베이스 저장

JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스 반영한다. 이를 플러시(flush)라고 한다.

영속성 컨텍스트의 장점

1) 1차 캐시와 동일성 보장

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라 한다. 영속 상태의 엔티티는 모두 이곳에 저장된다.

1차 캐시의 키는 식별자 값이다. 식별자 값은 데이터베이스 기본 키와 매핑되어 있다. 따라서 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스 기본 키 값이다.

em.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다.

식별자에 해당하는 엔티티가 영속성 컨텍스트에 의해 관리되고 있으면 영속성 컨텍스트는 항상 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다. 따라서 영속성 컨텍스트는 성능상의 이점은 물론이고 엔티티의 동일성 또한 보장한다.

2) 트랜잭션을 지원하는 쓰기 지연

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 인티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아둔다. 그리고 트랜잭션 커밋 시점에 모아둔 쿼리를 데이터베이스에 보내는데 이를 트랜잭션을 지원하는 쓰기 지연(transaction write-behind)이라 한다.

두 개의 엔티티를 영속화하여 두 건의 insert 쿼리가 쓰기 지연 SQL 저장소에 저장되어 있다. 트랜잭션을 커밋하면 엔티티 매니저는 영속성 컨텍스트를 플러시한다. 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보내고 영속성 컨텍스트의 변경 내용이 데이터베이스와 동기화 된다. 이후 실제 데이터베이스 트랜잭션을 커밋한다.

3) 변경 감지

JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지(dirty checking)라 한다.

JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이를 스냅샷이라 한다. 플러시 시점에 스냇샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.

변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다. 비영속, 준영속 상태와 같이 연속성 컨텍스트가 관리하지 않는 엔티티는 값을 변경해도 데이터베이스에 반영되지 않는다.

JPA는 기본적으로 update 쿼리를 날릴 때 엔티티의 모든 필드를 업데이트한다. 이는 다음과 같은 장점을 가진다.

  • 항상 수정 쿼리가 같다. 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
  • 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.

4. 플러시

플러시(flush)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 플러시를 실행하면 다음과 같은 일이 일어난다.

  1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
  2. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.

영속성 컨텍스트를 플러시하는 방법은 3가지이다.

  1. em.flush()를 직접 호출한다.
  2. 트랜잭션 커밋 시 플러시가 자동 호출된다.
  3. JPQL 쿼리 실행 시 플러시가 자동 호출된다.

JPQL 실행 시 SQL로 변환되어 데이터베이스에서 엔티티를 조회한다. 이 때, 영속성 컨텍스트의 내용이 데이터베이스와 동기화되지 않아 예측하지 못한 쿼리 결과가 반환될 수 있다. 이러한 상황을 예방하기 위해 JPA는 JPQL을 실행할 때도 플러시를 자동 호출한다. 영속성 컨텍스트의 내용이 데이터베이스와 동기화되어 JPQL을 실행할 때도 우리가 의도한 결과를 반환받을 수 있다.

플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는 것이 아니다. 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 것이 플러시이다. 이것이 가능한 이유는 트랜잭션이라는 작업 단위가 있기 때문이다. 트랜잭션 커밋 직전에만 변경 내용을 데이터베이스에 보내 동기화하면 된다.

참고

자바 ORM 표준 JPA 프로그래밍