[Spring] JPA Persistence Management

Gogh·2023년 1월 2일
0

Spring

목록 보기
14/23

🎯 목표 : Spring Framework에서 JPA의 영속성 관리 개념 학습

📒 Persistence Context


📌 EntityManagerFactory & EntityManager

  • 엔티티 매니저 팩토리는 엔티티 매니저를 만들어준다.
  • 엔티티 매니저는 한 한개의 엔티티 매니저로 애플리케이션 전체에서 공유한다.
  • 엔티티 매니저 팩토리는 여러 스레드간에 공유가 가능하지만 엔티티 매니저는 하나의 스레드에서만 접근 할수 있도록 한다.

image

  • 엔티티 매니저는 JPA에서 제공하는 인터페이스이며, 하나의 트랜젝션 범위에 있는 엔티티 매니저는 같은 영속성 컨텍스트에 접근하게 된다.
  • 엔티티 매니저 인터페이스에 대한 공식 문서

📌 Persistence Context

  • 영속성 컨텍스트는 엔티티를 영구 저장하는 환경의 하나의 논리적인 개념이다.
  • 영속성 컨텍스트는 엔티티 매니저를 사용하여 엔티티를 저장하고 수정하고 삭제한다.
  • 엔티티의 생명주기
    • 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태.
    • 영속 : 영속성 컨텍스트에 저장된 상태.
    • 준영속 : 영속성 컨텍스트에 저장되었다가 분리된 상태.
    • 삭제 : 삭제된 상태
  • 영속성 컨텍스트에서의 식별자
    • 영속 상태 데이터는 식별자 값이 반드시 존재 해야한다. = @Id로 매핑된 값.
  • 영속성 컨텍스트와 데이터 베이스
    • 트랜젝션을 커밋하는 순간 엔티티 메니저의 flush()도 호출하게 되는데 이때, 데이터가 저장되게 된다.
  • 영속성 컨텍스트 관리 데이터의 장점
    • 1차 캐시에서 데이터를 관리하므로, 데이터를 조회할때 1차 캐시에 데이터가 있다면, DB를 거치지 않고 조회하게 된다.
    • DB에서 조회된 데이터는 1차 캐시에 등록되며, 영속화 된 데이터는 언제나 동일성을 보장해준다.
    • 트랜잭션에 맞게 쓰기 지연을 지원해줘 한번에 필요한 쿼리를 날려주게 도와준다.
    • 영속화 된 데이터에 대한 변경 감지를 하여 데이터가 변경 되었다면 자동으로 변경된 내용에 대한 업데이트 쿼리를 날려주는 변경 감지 기능을 지원한다.
    • 필요한 엔티티 객체를 상속받은 프록시 객체를 먼저 할당하여, 실제 해당 객체의 데이터가 조회 될때, 쿼리문을 날려 데이터를 조회하는 지연 로딩 기능을 지원한다.
  • persistence Contex 구조

image


📌 Persistence Context CRUD

  • 예제 실습 환경
  • Gradle , H2 DB
// JpaTest.java

@Configuration
public class JpaTest {
      private EntityManager em;
      private EntityTransaction tx;

      @Bean
      public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
          this.em = emFactory.createEntityManager();
          this.tx = em.getTransaction();

          return args -> {
              ex1();
              ex2_1stcache();
              ex3_select();
              ex4_equals();
              ex5_delaycommit();
              ex6_dirtychecking();
              emFactory.close();
          };
      }
}

// MyMember.java

@Entity
@NoArgsConstructor
@Getter
@Setter
public class MyMember {
      @Id
      private Long id;

      private String name;

}
  • EntityManagerFactory을 주입 받아 EntityManagerEntityTransaction을 사용하기 위해 em,tx에 할당하였다.

👉 persist()와 commit()

    private void ex1() {
            tx.begin();
            MyMember myMember = new MyMember();
            myMember.setId(1L);
            myMember.setName("HelloA");

            em.persist(myMember);

            tx.commit();
            em.close();
    }
  • 엔티티 객체를 생성하여 엔티티 매니저로 persist 하고 트렌잭션을 commit으로 끝냈다.
  • 엔티티 객체를 생성하는 쿼리문은 언제 DB로 날아가게될까?
  • em.persist(myMember);가 호출되면 엔티티 메니저가 , 엔티티 객체는 영속성 컨텍스트의 1차 캐시에 영속화 되며 JPA의 관리하에 들어오게 된다.
  • 엔티티 객체가 1차 캐시에 영속화 되며 쓰기지연 SQL 저장소에는 myMember엔티티 데이터를 저장하기 위한 쿼리문이 저장되게 된다.
  • tx.commit(); 동시에 내부적으로는 em.flush()가 실행되며, DB로 쿼리문이 날아가 엔티티는 DB에 저장되고 트랜젝션은 끝나게 된다.
  • em.close();를 통해 엔티티 매니저를 종료된다.

👉 1차 캐시에서의 데이터 조회 ex2_1stcache(),ex3_select()

    private void ex2_1stcache() {
        tx.begin();
        /* 비영속 상태 */
        MyMember myMember = new MyMember();
        myMember.setId(1L);
        myMember.setName("helloJPA");

        /* 영속 1차 캐시*/
        System.out.println("Berore==============");
        em.persist(myMember);
        System.out.println("After===============");
        /* 쿼리문이 날아가지 않음 - 영속성 컨텍스트의 1차캐시에서 데이터를 가져오기 때문 */
        MyMember findMyMember = em.find(MyMember.class, 1L);

        System.out.println("findMember.getId() = " + findMyMember.getId());
        System.out.println("findMember.getName() = " + findMyMember.getName());
        tx.commit();
        /* 영속 상태로 만들고 컨텍스트를 비운다 */
        em.clear();

    }

    private void ex3_select() {
        tx.begin();
        /* 영속성 컨텍스트에는 현재 존재하고 있는 1차 캐시 데이터가 없기때문에 셀렉 쿼리를 날려서 데이터를 가져오게된다*/
        MyMember reFindMyMember = em.find(MyMember.class, 1L);
        System.out.println("reFindMember.getId() = " + reFindMyMember.getId());
        System.out.println("reFindMember.getName() = " + reFindMyMember.getName());

        em.close();
    }
  • ex2_1stcache()에서 새로운 엔티티 객체를 생성하여 em.persist(myMember);로 영속화 하게 되면, 1차 캐시에 데이터가 저장되게 되며, MyMember findMyMember = em.find(MyMember.class, 1L);
    단계에서 조회한 데이터는 1차 캐시에 있는 데이터를 호출하게 될 것이다. 실제 코드를 실행하여 디버그 메세지의 쿼리문을 보게되면, em.find()를 호출한 시점에 쿼리문이 날아가지 않은 것을 확인 할수 있다.
  • 이는, 1차 캐시에서 데이터를 조회 할수 있다는 얘기며, persist()후 엔티티 메니저가 종료되거나 비워지지 않았다면 해당 데이터를 DB에 거치지 않고 조회 할수 있게 된다.
  • 트랜잭션을 종료하고, em.clear();를 호출하여 엔티티 메니저를 비운 후 ex3_select()의 새로운 트랜잭션에서 em.find(MyMember.class, 1L);를 하게되면
    새로운 엔티티 매니저가 호출 되며, 이 엔티티 매니저 영속성 컨텍스트의 1차 캐시에 데이터는 모두 비워져 있는 상태기 떄문에 Select 쿼리문을 날려 데이터를 조회하게 된다.

👉 영속 엔티티의 동일성 보장 ex4_equals()

    private void ex4_equals() {
        tx.begin();
        /* 비영속 상태 */
        MyMember myMember = new MyMember();
        myMember.setId(1L);
        myMember.setName("helloJPA");

        em.persist(myMember);
        tx.commit();
        em.clear();

        tx.begin();
        MyMember findMyMember1 = em.find(MyMember.class, 1L);
        MyMember findMyMember2 = em.find(MyMember.class, 1L);

        System.out.println("findMember1 equals findMember2 = "+ findMyMember1.equals(findMyMember2));
        em.close();

    }
  • 새로운 엔티티를 생성후 em.persist(myMember);, tx.commit();, em.clear(); 차례로 호출하여 DB에 데이터를 저장하고 엔티티 매니저를 비웠다.
  • 새로운 트랜잭션에서 em.find(MyMember.class, 1L);로 같은 영속 엔티티를 두번 호출하여 두개의 변수에 할당해준다.
  • equals()로 두개의 객체가 같은지 확인 해보면, true를 반환한다.
  • 즉, 영속성 컨텍스트에 등록되어 있는 영속 엔티티는 몇번을 호출하여 다른 변수에 할당하더라도 동일성이 보장되는 것을 확인 할 수 있다.

👉 DB에 데이터가 저장되는 시점 확인 ex5_delaycommit()

    private void ex5_delaycommit() {
        tx.begin();
        MyMember myMember1 = new MyMember();
        myMember1.setId(1L);
        myMember1.setName("helloJPA");
        MyMember myMember2 = new MyMember();
        myMember2.setId(2L);
        myMember2.setName("helloSpring");
        System.out.println("Persist Berore==============");

        em.persist(myMember1);
        em.persist(myMember2);

        System.out.println("Persist After===============");

        System.out.println("Commit==============");
        tx.commit();
        em.close();

    }
  • 두개의 엔티티 객체를 생성후 영속성 컨텍스트에 등록하고 커밋 시점을 확인 해 보았다.
  • 디버그 메세지를 확인 해 보면, System.out.println("Commit==============");이 실행 된 이후에 쿼리문이 날아가는 것을 확인 할 수 있다.
  • 영속성 컨텍스트에서 트랜잭션에 따라 쿼리문이 날아가는 시점을 지연시키고 commit()이 호출되는 시점에 쿼리문이 날아가도록 쓰기지연 기능을 지원하는 것을 확인 할 수 있다.

👉 DirtyChenking - 변경 감지 ex6_dirtychecking()

    private void ex6_dirtychecking() {
        tx.begin();
        MyMember myMember1 = new MyMember();
        myMember1.setId(1L);
        myMember1.setName("helloJPA");
        em.persist(myMember1);
        tx.commit();
        em.clear();

        tx.begin();
        MyMember findMyMember1 = em.find(MyMember.class, 1L);
        System.out.println("findMember1.getName() = " + findMyMember1.getName());
        findMyMember1.setName("helloSpring");
        tx.commit();
        em.clear();

        tx.begin();
        MyMember findMyMember2 = em.find(MyMember.class, 1L);
        System.out.println("findMember2.getName() = " + findMyMember2.getName());
        em.close();
    }
  • 3개의 트랜잭션으로 나눠 데이터 조회 흐름을 확인 해 보았다.
  • 첫번쨰 트랜잭션에서는 새로운 엔티티 객체를 생성하여 persist()를 호출하여 영속성 컨텍스트에 등록 후 commit()으로 DB에 저장하고 엔티티 매니저를 초기화 했다.
  • 두번째 트랜잭션에서 DB에 저장된 엔티티를 조회하여 name값을 수정 하고 persist()를 호출 하지 않고 commit()만 호출한 다음, 엔티티 매니저를 초기화 했다.
  • 세번째 트랜잭션에서 DB에 저장된 엔티티 데이터를 조회 하였을때 조회된 엔티티가 가지고 있는 name의 값은 "helloSpring"인 것을 확인 할수 있다.
  • 즉, 두번째 트랜잭션에서 name의 값이 수정된 엔티티는 persist()를 호출하지 않더라도 commit()이 호출되는 시점에 엔티티의 변경된 데이터를 감지하여 업데이트 쿼리문을 자동을 날려 주는 것을 확인할 수 있다.
  • JPA에서 지원해주는 변경감지(Dirty Checking) 기능이다.

👉 그 외의 기능

  • clear() : 영속성 컨텍스트 초기화.
  • close() : 영속성 컨텍스트 종료.
  • merge() : 준영속(영속 상태였다가 영속성 컨텍스트에서 제거된 엔티티) 상태의 엔티티를 영속상태로 만들어 준다.
  • detach(entity) : 해당 엔티티를 준영속 상태로 만든다.
  • flush() : 영속상태의 데이터를 DB에 저장한다.

Reference

https://www.baeldung.com/jpa-hibernate-persistence-context

https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html

profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글