Spring

[Spring]JPA - 영속성 컨텍스트(Persistence Context)

PlatinumeOlive 2024. 4. 12. 21:03

스프링 프로젝트를 하던 중, 엔티티 매니저가 무엇인지 궁금하여 찾아보다가 알게된 영속성 컨텍스트에 대해 공부해보려고 합니다.

<블로그를 참고하여 쓰면서 공부한다는 느낌으로 작성한 게시물입니다.>

출처는 아래에 기재하였습니다.

영속성 컨텍스트


EntityManagerFactory& EntityManager


<시나리오>

  1. 새로운 고객의 요청이 올때마다 엔티티 매니저 팩토리는 엔티티 매니저를 생성합니다.
  2. 엔티티 매니저는 내부적으로 데이터베이스 커넥션을 사용하여 DB를 사용합니다.

EntityManagerFactory

  • 말 그대로 엔티티 매니저를 만드는 공장입니다.
  • 엔티티 매니저 팩토리는 생성하는 비용이 커서 한 개만 만들어 애플리케이션 전체에서 공유합니다다.
  • 여러 스레드가 동시에 접근해도 안전합니다.

EntityManager

  • EntityManager는 영속성 컨텍스트 내에서 Entity들을 관리하고 있습니다.
  • EntityManager는 JPA에서 제공하는 interface로 spring bean으로 등록되어 있어 Autowired를 통해 사용할 수 있습니다.
  • Query Method, Simple JPA repository는 직접적으로 entityManager를 사용하지 않도록 한번 더 감싸준 것입니다.
  • Spring JPA에서 제공하지 않는 기능을 사용하거나 특별한 문제가 있어서 별도로 customizing을 해야한다면 entityManager를 직접 받아서 처리합니다.
  • EntityManager는 Entity Cache를 가지고 있습니다.
  • Entity의 CRUD등 엔티티와 관련된 모든 일을 처리합니다.
  • 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안됩니다.

영속성 컨텍스트란?

  • JPA를 이해하는데 가장 중요한 용어입니다. 엔티티를 영구 저장하는 환경이라는 뜻을 가지고 있습니다.
  • 어플리케이션과 DB사이에서 객체를 보관하는 가상의 DB같은 역할을 합니다.
  • 서비스당 하나의 EntityManager Factory가 존재하며, ENtity Manager Factory에서 DB에 접근하는 트랜잭션이 생길 때 마다 스레드 별로 Entity Manager를 생성하여 영속성 컨텍스트에 접근합니다.
  • EntityManager에 엔티티를 저장하거나 조회하면 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리합니다.
  • 영속성 컨텍스트는 EntityManager를 생성할 때 만들어지며 EntityManager를 통해 영속성 컨텍스트에 접근하고 관리합니다.
  • @Entity 어노테이션을 갖는 엔티티 인스턴스를 막 생성했을 때는 영속성 컨텍스트에서 관리하지 않습니다. 따라서 아래 EntityManager의 persist 메소드를 사용하여 영속 상태로 변경할 수 있습니다.
em.persist(entity);
  • Spring에서는 EntityManager를 주입하여 사용하면 같은 트랜젝션 범위에 있는 EntityManager는 영속성 컨텍스트에 접근합니다.

Spring Framework의 컨테이너 환경


Entity의 생명 주기

엔티티에는 4가지 상태가 존재합니다.

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

비영속(new/transient)

  • 엔티티 객체가 생성된 순수 객체 상태
  • 아직 영속성 컨텍스트나 데이터베이스와는 전혀 관계가 없는 상태를 말합니다.(영속성 컨텍스트에 저장 x)
//객체만 생성한 비영속 상태
User user = new User();

영속(managed)

  • 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장하면, 영속성 컨텍스트가 엔티티를 관리하므로 영속 상태가 됩니다.
EntityManager.persist(member)

위 코드를 수행하면 member 엔티티는 영속성 컨텍스트에 의해 관리되는 영속 상태가 됩니다. 위 코드가 수행된다고 데이터 베이스에 저장되는 것은 아닙니다. (트랜잭션이 commit하는 시점에 영속성 컨텍스트에 있는 엔티티들에 대한 쿼리가 날아갑니다.)

@Autowired
private EntityManager entityManager;
// Class내에 Autowired로 EntityManager추가

    //객체만 생성한 비영속상태 
    User user = new User();
    
    // 객체를 저장한 영속상태
    entityManager.persist(user);

JPA는 일반적으로 id 필드가 존재하지 않으면 예외를 뱉어내는데, 영속 상태의 엔티티를 관리하기 위해서 입니다.

id로 데이터를 관리하기 때문에 꼭 필요한 것 입니다.
이 상태가 되면 몇 가지의 장점을 가지게 됩니다.

1차 캐시란?
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 한다. 영속 상태의 엔티티는 모두 이곳에 저장된다. 쉽게 말해 영속성 컨텍스트 내부에 Map이 하나 있는데 (1차 캐시), 키는 @Id로 매핑한 식별자고 값은 엔티티 인스턴스다.

  1. 1차 캐시
    • em.find(key)를 호출하면 영속성 컨텍스트에 캐시된 테이터를 먼저 찾습니다.
    • 캐시된 데이터가 없다면 DB에 접근하여 데이터를 로드하고 1차 캐시 데이터에 저장합니다.
    • 1차 캐시의 존재로 JAVA영역에서 REPEATABLE READ 등급의 트랜잭션 격리 수준을 활용합니다.
  2. 동일성 보장
    • JPA를 통해 불러온 데이터는 모두 캐시 데이터에 저장되기 때문에, 같은 id를 가진 데이터는 같은 데이터 입니다.
      • 일반적으로 Java에서 '같다'라는 기준은
        Identity(hashcode) / Equality(equals)입니다.
  1. 트랜잭션 지원하는 쓰기 지연
    • Transaction이 시작된 이후 JPA가 생성한 쿼리는 모두 쓰기 지연 저장소에 저장됩니다.
    • commit이 수행되면 저장된 모든 쿼리를 실행합니다.
  2. 변경 감지
    • SQL을 직접 활용하여 개발하면 update문을 수행할 때 매우 귀찮은 점이 있습니다.
    • 컬럼 1개, 2개, 3개, ... N개를 수정해야 할 때를 모두 쿼리로 작성해야 하는 것입니다.
    • 이렇게 되면 비즈니스 로직은 SQL에 의존할 수밖에 없습니다.
    • JPA는 데이터를 저장하기 전 영속성 컨텍스트에 저장된 데이터가 있는지 확인한다.
    • 동일 데이터가 존재하면 update, 없으면 insert를 수행합니다(upsert).
    • JPA가 실제로 수행하는 쿼리는 모든 컬럼을 변경합니다.
      • 컬럼이 굉장히 많은 (30개 이상) 테이블이 아니면 성능에 크게 영향을 미치지 않습니다.
      • 엔티티 클래스에 @DynamicUpdate를 붙여주면 SET절에 변경된 데이터만 삽입됩니다.
  3. 지연 로딩

준영속(detached)

  • 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 됩니다.
  • 영속 상태의 엔티티를 detach 시키거나 영속성 컨텍스트 자체가 초기화 / 종료되면 컨텍스트 내부의 모든 데이터는 준영속 상태가 됩니다. 관리되지는 않는 상태이지만 JPA의 지원을 받지 못할 뿐, 정상적인 데이터를 갖는 인스턴스입니다.

엔티티를 준영속 상태로 만들려면?

  • EntityManager.detach(entity) : 엔티티를 준영속 상태로 만듬
  • EntityManager.close() : 영속성 컨텍스트를 닫음
  • EntityManager.clear() : 영속성 컨텍스트를 초기화

삭제

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

EntityManager.remove(entity)

Flush

  • 영속성 컨텍스트의 변경 내용을 DB에 반영하는 절차 입니다.
    플러시를 수행하면 아래 순서대로 동작합니다.
  1. 데이터의 변경을 감지한다.
  2. 생성된 쿼리를 쓰기 지연 저장소에 등록한다.
  3. commit되면 저장되어 있던 쿼리를 모두 수행한다.
  • 1차 캐시를 지우지 않고 쿼리를 DB에 날려서 DB와의 싱크를 맞추는 역할(동기화)을 합니다.
  • flush를 실행하면 Dirty checking을 통해 스냅샷과 비교하여 수정된 entity를 찾고 UPDATE Query를 만들어 쿼리 저장소에 등록한 후 쿼리 저장소에 저장된 모든 쿼리를 DB에 보내어 동기화 합니다.

영속성 context의 값이 DB에 반영되는 경우
1. flush를 통해 개발자가 직접 반영하는 경우
2. Transaction이 끝나서 해당 query가 commit되는 시점
3. 복잡한 조회 조건에 JPQL query가 실행되는 시점
(사전에 영속성 콘텍스트에 추가한 데이터들이 flush()되지 않아서 DB에 업데이트 되지 않았다면 JPQL쿼리문을 수행하는데 오류가 발생할 수 있기에 JPQL 쿼리문을 실행하기 전에 자동으로 flush()를 호출한다.)

em.flush()를 활용하면 직접 플러시 할 수 있습니다.

플러시 모드

  1. FlushModeType.AUTO (Default)
    • commit이나 쿼리 실행할 때 플러시
  2. FlushModeType.AUTO (Default)
    • commit 할 때만 플러시

병합

준영속 상태의 데이터는 병합 기능을 사용하여 다시 영속 상태로 돌릴 수 있습니다.

SomeEntity entity = em.find(key);
em.detach(entity);
em.merge(entity);

그림으로 보는 엔티티 생명 주기

 


 

사진 출처 및 Reference