ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SPRING] JPA의 영속성 컨텍스트
    study/spring 2020. 8. 30. 15:27

    영속성 관리에서 가장 중요한 것은 2가지이다.

    • 객체와 관계형 데이터베이스간의 매핑
    • 영속성 컨텍스트 ( JPA 내부 동작 )

     

    EntityManagerFactory와 EntityManager

    클라이언트의 요청이 올때 마다 ( 즉, thread가 하나씩 생성될 때마다 ) EntityManager를 생성한다.

    EntityManager는 내부적으로 DB 커넥션 풀을 사용해서 DB에 접근한다.

     

    EntityManagerFactory

    JPA는 EntityManagerFactory를 만들어야 한다.

    애플리케이션이 로딩되는 시점에 DB당 딱 하나만 생성해야 한다.

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("emf");

    그리고 WAS가 종료되는 시점에 EntityManagerFactory를 닫는다.

     

    EntityManager

    실제 트랜잭션 단위가 수행될 때마다 생성된다.

    클라이언트의 요청이 들어올 때 생성했다가 요청이 끝나면 닫는다.

    thread간에 공유를 하면 안되고 트랜잭션이 수행된 후에는 반드시 닫고 DB 커넥션을 반환한다.

    public void createMember() {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        tx.begin();
        
        Member member = new Member("Id", "pw");
        em.persist(member);
        
        tx.commit();
    }

     

    EntityTransaction

    데이터를 변경하는 모든 작업은 반드시 트랜잭션 안에서 이루어져야 한다. ( 조회는 상관 없음 )

    EntityTransaction tx = entityManager.getTransaction();
    
    tx.begin(); // 트랜잭션 시작
    tx.commit(); // 트랜잭션 수행
    tx.rollback(); // 작업에 문제 발생 시

     

    관계도를 살펴보면 아래 그림과 같다.

    EntityManager 관계도 - 출처 : https://iyoungman.github.io/jpa/EntityManagerFactory-EntityManager-PersistenceContext/

    EntityManager는 DB 연결이 필요한 시점에 커넥션 풀에서 커넥션을 하나 얻는다.

    JPA의 구현체( ex. hibernate )들은 EntityManagerFactory를 생성할 때 커넥션 풀을 만든다.

     

     

    영속성 컨텍스트 ( Persistence Context )

    Entity를 영구 저장하는 환경을 뜻한다.

    EntityManager로 Entity를 저장하거나 조회하면 영속성 컨텍스트에 Entity를 보관하고 관리한다.

    em.persist(entity);

    위 코드는 실제로 DB에 저장한다는 것이 아니라 영속성 컨텍스트를 통해서 Entity를 영속화하겠다는 의미다.

    즉, Entity를 영속성 컨텍스트에 저장하는 것이다.

     

    EntityManager를 통해서 영속성 컨텍스트에 접근하게 된다.

    package javax.persistence;
    
    import java.util.List;
    import java.util.Map;
    import javax.persistence.criteria.CriteriaBuilder;
    import javax.persistence.criteria.CriteriaDelete;
    import javax.persistence.criteria.CriteriaQuery;
    import javax.persistence.criteria.CriteriaUpdate;
    import javax.persistence.metamodel.Metamodel;
    
    public interface EntityManager {
        void persist(Object var1);
    
        <T> T merge(T var1);
    
        void remove(Object var1);
    
        <T> T find(Class<T> var1, Object var2);
    
        <T> T find(Class<T> var1, Object var2, Map<String, Object> var3);
    
        <T> T find(Class<T> var1, Object var2, LockModeType var3);
    
        <T> T find(Class<T> var1, Object var2, LockModeType var3, Map<String, Object> var4);
    
        <T> T getReference(Class<T> var1, Object var2);
    
        void flush();
    
        ...
       }

    EntityManager가 생성되면 1:1로 영속성 컨텍스트가 생성된다.

    하지만 컨테이너 환경의 JPA에서는 여러 EntityManager가 하나의 영속성 컨텍스트를 공유하게 된다.

     

    컨테이너 환경의 JPA라 함은 무엇일까?

    컨테이너를 사용하는 환경 (ex. spring)에서는 개발자가 EntityManager를 직접 생성하지 않고 컨테이너에 위임한다.

     

    일반적으로 스프링은 싱글톤 기반으로 동작하기 때문에 속성값은 모든 thread가 공유하게 된다.

    그래서 여러 thread가 동시에 접근하면 동시성 문제가 발생할 수도 있다.

    그렇다면 스프링이 관리하는 EntityManager의 Thread-safe를 어떻게 보장할까?

    EntityManager를 Proxy를 통해서 감싸고 EntityManager 메소드 호출 때 마다 Proxy를 통해서 EntityManager를 생성한다.

    EntityManager를 직접 사용하는 경우에는 @PersistenceContext를 사용하면 된다.

     

    • 직접 생성한 EntityManager에 @PersistenceContext를 선언하는 경우
    @Service
    public class TestService {
    
        @PersistenceContext
        private EntityManager entityManager;
        
        ...
    }
    entityManager = {$Proxy84@9656} "Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@4f356b98]"

    스프링 컨테이너가 초기화 되면서 @PersistenceContext 어노테이션으로 주입받은 EntityManager를 Proxy로 감싼다.

    SharedEntityManagerCreator에 의해 Proxy로 만들어진다.

     

    • SimpleJpaRepository의 EntityManager
    @Repository
    @Transactional(readOnly = true)
    public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    
    	private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
    
    	private final JpaEntityInformation<T, ?> entityInformation;
    	private final EntityManager em;
    	private final PersistenceProvider provider;
    
    	private @Nullable CrudMethodMetadata metadata;
    	private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
        
        ...
    }
    em = {$Proxy84@9748} "Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@4f356b98]"

    SharedEntityManagerCreator에 의해 Proxy로 만들어진다.

     

    트랜잭션 범위의 영속성 컨텍스트

    스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.

    트랜잭션이 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 끝낸다.

    즉, 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용하는 것이다.

    트랜잭션 범위의 영속성 컨텍스트 - 출처 : https://iyoungman.github.io/jpa/EntityManagerFactory-EntityManager-PersistenceContext/

     

    여러 EntityManager를 사용해도 한 트랜잭션으로 묶이면 영속성 컨텍스트를 공유한다.

    출처 : https://iyoungman.github.io/jpa/EntityManagerFactory-EntityManager-PersistenceContext/

     

    같은 EntityManager를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.

    따라서 같은 EntityManager를 호출해도 접근하는 영속성 컨텍스트가 다르므로 멀티스레드에 안전한다.

    출처 : https://iyoungman.github.io/jpa/EntityManagerFactory-EntityManager-PersistenceContext/

     

     

    Entity의 생성주기 ( Entity LifeCycle )

    • 비영속 ( new / transient )

    영속성 컨텍스트와는 전혀 관계가 없는 상태로 객체를 생성만 한 상태를 말한다.

    Member member = new Member();
    member.setId("memberID");
    member.setPw("password");

     

    • 영속 ( managed )

    영속성 컨텍스트에 저장된 상태로 Entity가 영속성 컨텍스트에 의해 관리되고 있는 상태를 말한다.

    Member member = new Member();
    member.setId("memberID");
    member.setPw("password");
    
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    
    tx.begin();
    
    em.persist(member); // 영속 상태가 된다고 바로 쿼리가 날라가지는 않음
    
    //tx.commit(); 을 해주어야 영속성 컨텍스트에 있는 정보들이 DB에 쿼리로 날라감

     

    • 준영속 ( detached )

    영속성 컨텍스트에 저장되었다가 분리된 상태로 영속성 컨텍스트에서 지운 상태를 말한다.

    em.detach(member);

     

    • 삭제 ( removed )

    실제 DB 삭제를 요청한 상태를 말한다.

    em.remove(member);

     

     

    영속성 컨텍스트가 존재하는 이유는 무엇일까?

     

    1. 1차 캐시

    영속성 컨텍스트 내부에는 1차 캐시가 존재한다. ( 1차 캐시를 영속성 컨텍스트라고 이해해도 된다. )

    Map<Key, Value> 형태로 1차 캐시에 저장이 된다. ( key = @Id로 선언한 PK, value = 해당 Entity 자체 )

    Member member = new Member();
    member.setId("memberID");
    member.setPw("password");
    /* 영속 상태 (Persistence Context 에 의해 Entity 가 관리되는 상태) */
    // DB 저장 X, 1차 캐시에 저장됨
    em.persist(member); 
    // 1차 캐시에서 조회
    Member findMember = em.find(Member.class, "memberID"); 

     

    1차 캐시에 Entity가 있을 때 이점은 무엇일까?

    em.find()를 하면 DB보다 먼저 1차 캐시를 조회하게 된다. 그리고 1차 캐시에 해당 Entity가 존재한다면 바로 반환할 수 있다.

    1차 캐시에 해당 Entity가 없다면 DB를 직접 조회하고 조회한 결과를 1차 캐시에 저장한 후 Entity를 찾아온다.

    EntityManager는 트랜잭션 단위로 만들고 트랜잭션이 끝날 때 함께 종료가 된다.

    그때 1차 캐시도 모두 날라가기 때문에 트랜잭션이 수행되는 동안에만 이득이 있기 때문에 큰 성능 이점을 가지고 있지는 않다.

    하지만 비즈니스 로직이 굉장히 복잡한 경우에는 효과를 볼 수 있다.

     

    2. 동일성 보장

    Member member1 = entityManager.find(Member.class, "member1");
    Member member2 = entityManager.find(Member.class, "member1");
    System.out.println(member1 == member2); // 동일성 비교 true

    영속 Entity의 동일성을 보장한다.

    member1에 해당하는 Entity를 2번 조회하게 되면 1차 캐시에 의해 같은 Reference로 인식이 된다.

    따라서 하나의 트랜잭션 내에서 같은 Entity를 비교하게 되면 동일성은 보장된다.

    1차 캐시로 Repeatable read 등급의 isolation level을 DB가 아닌 애플리케이션 차원에서 제공한다.

     

    3. Entity 등록 시 트랜잭션을 지원하는 쓰기 지연 ( 버퍼링 )

    public void createMember() {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        //EntityManager는 테이터 변경시 트랜잭션을 시작해야 한다.
        tx.begin();
        
        Member member1 = new Member("Id1", "pw1");
        Member member2 = new Member("Id2", "pw2");
        
        //이때까지 Insert 쿼리를 DB에 보내지 않는다.
        em.persist(member1);
        em.persist(member2);
        
        //commit 하는 순간 DB에 쿼리를 보낸다.
        tx.commit();
    }

    em.persist()

    JPA가 insert SQL을 계속 쌓고 있는 상태다.

    member1을 1차 캐시에 저장한후 JPA가 Entity를 분석하여 insert SQL을 만든다.

    그렇게 만들어진 insert SQL을 쓰기 지연 SQL 저장소라는 곳에 쌓고 DB에 바로 넣지 않고 기다린다.

    member2도 같은 과정을 반복한다.

     

    tx.commit()

    commit 시점에 insert SQL을 동시에 DB로 보낸다.

    쓰기 지연 SQL 저장소에 쌓여있는 SQL들을 DB로 날린다. ( flush() )

    flush()는 1차 캐시를 지우지는 않고 쿼리들을 DB에 날려서 DB와의 싱크를 맞추는 역할을 한다.

    flush() 후에는 실제 DB 트랜잭션에 커밋이 된다. ( commit() )

     

    4. 더티 체킹 ( Dirty Checking )

    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    
    tx.begin();
    
    // 영속 엔티티 조회
    Member memberA = em.find(Member.class, "memberA");
    
    // 영속 엔티티 데이터 수정
    memberA.setUsername("hi");
    memberA.setAge(10);
    
    //트랜잭션 커밋
    tx.commit();
    

     

    Entity 데이터를 수정하고 커밋하기 전에 update()나 persist()로 영속성 컨텍스트에 해당 데이터를 업데이트 해달라고 알려주어야 하지 않을까?

    그럴 필요 없다.

    Entity 데이터만 수정하고 commit만 하면 DB에 자동으로 반영이 된다.

    즉, 데이터가 set하면 해당 데이터 변경을 감지하여 자동으로 update SQL이 나가는 것이다.

     

    tx.commit()을 하면 flush()가 일어날 때 Entity와 스냅샷을 일일이 비교한다.

    변경사항이 있으면 update SQL을 만들고 쓰기 지연 SQL 저장소에 저장을 한다.

    update SQL을 DB에 반영한 후 commit()을 하게 된다.

    @DynamicUpdate
    더티 체킹으로 생성되는 update SQL은 기본적으로 모든 필드를 업데이트한다.
    @DynamicUpdate를 사용하면 변경된 필드만 반영되도록 할 수 있다.

     

    5. Entity 삭제

    Member memberA = em.find(Member.class, "memberA");
    
    em.remove(memberA);

    위의 Entity 수정 매커니즘과 동일하다.

    트랜잭션의 commit 시점에 delete SQL이 나간다.

     

    참고

    https://gmlwjd9405.github.io/2019/08/06/persistence-context.html

     

    [JPA] 영속성 컨텍스트(Persistence Context)란 - Heee's Development Blog

    Step by step goes a long way.

    gmlwjd9405.github.io

    https://iyoungman.github.io/jpa/EntityManagerFactory-EntityManager-PersistenceContext/

     

    'study > spring' 카테고리의 다른 글

    [SPRING] @SpringBootApplication  (0) 2020.08.29
    [SPRING] Spring Batch Chunk 지향처리  (0) 2020.08.21
    [SPRING] DispatcherServlet  (0) 2020.08.04
    [SPRING] Reqeust 흐름과 WAS  (0) 2020.07.26
    [SPRING] Spring vs. Spring boot  (0) 2020.07.23
Designed by Tistory.