03강. 영속성 관리 - 내부 동작 방식


1. 영속성 컨텍스트


JPA에서 가장 중요한 2가지


  • 객체와 관계형 데이터베이스 매핑하기 (Object Relational Mapping) (정적)
  • 영속성 컨텍스트 (동작 메커니즘)



엔티티 매니저 팩토리와 엔티티 매니저


  • EntityManagerFactory 통해서 요청이 올 때마다 EntityManager를 만듦
  • EntityManage는 내부적으로 DB Connection을 사용해서 DB를 사용



영속성 컨텍스트


  • JPA를 이해하는데 가장 중요한 용어
  • 엔티티를 영구 저장하는 환경
  • 엔티티 매니저를 통해 영속성 컨텍스트에 접근
    • 엔티티 매니저를 생성하면 영속성 컨텍스트라는 공간이 생김
  • EntityManager.persist(entity);
    • EntityManager에서 persist호출해서 entity를 넣으면 DB에 저장하는 것.. 처럼 보이지만
    • 실제로는 ..
      • 영속성 컨텍스트를 통해서 Entity를 영속화
      • persist : Entity를 영속성 컨텍스트에 저장 (DB에 저장 X)
  • J2SE 환경 : 엔티티 매니저와 영속성 컨텍스트가 1:1

    J2EE, 스프링 프레임워크 같은 컨테이너 환경 : 엔티티 매니저와 영속성 컨텍스트가 N:1



영속성 컨텍스트의 이점


  • 애플리케이션과 DB 사이에 영속성 컨텍스트라는 중간 계층 존재
  • 중간 계층이 있다는 점으로 인해 장점을 얻을 수 있음 (ex. 버퍼링, 캐싱 등)


1) 엔티티 조회 - 1차 캐시


  • 영속성 컨텍스트 내부에 1차 캐시 가지고 있음
  • 1차 캐시 : Key(@Id), 값(Entity 자체)으로 이루어짐

    ex. Key : member1, 값 : persist한 member 자체

      //엔티티를 생성한 상태(비영속)
      Member member = new Member();
      member.setId("member1");
      member.setUsername("회원1");
        
      //엔티티를 영속
      em.persist(member);
    
  • 조회할 때에 1차 캐시 확인 후, 없는 경우에 DB로 쿼리를 보냄

    ex. 1차 캐시에서 조회

      Member member = new Member();
      member.setId("member1");
      member.setUsername("회원1");
        
      //1차 캐시에 저장됨 (jpa는 엔티티 조회하면 무조건 영속성 컨텍스트에 올림)
      em.persist(member);
        
      //1차 캐시에서 조회
      Member findMember = em.find(Member.class, "member1");
    

    ex. DB에서 조회

      Member findMember2 = em.find(Member.class, "member2");
    

    • 과정 : 1차 캐시에서 찾아봄 -> member2는 1차 캐시에 없었음 -> JPA가 DB에서 조회 -> DB의 member2를 2차 캐시에 저장 . 그리고 member2를 반환 -> 이후에 member2를 조회하면 1차 캐시에서 찾아봄
  • 그러나, 트랜잭션 끝날 때 1차 캐시도 사라져서 크게 도움이 되지는 않음

💡 전체에서 공유하는 캐시 : 2차 캐시

  • 참고 코드 : 1차 캐시에서 조회

      public class JpaMain {
        
          public static void main(String[] args) {
                
              // 생략
        
              try {
        
                  // 비영속
                  Member member = new Member();
                  member.setId(101L);
                  member.setName("HelloJPA");
        
                  // 영속
                  System.out.println("=== BEFORE ===");
                  em.persist(member);
                  System.out.println("=== AFTER ===");
        
                  Member findMember = em.find(Member.class, 101L);
        
                  System.out.println("findMember.id = " + findMember.getId());
                  System.out.println("findMember.name = " + findMember.getName());
        
                  tx.commit();
              } 
                
              // 생략
    
          }
      }
    

    실행결과 : Select 쿼리가 나가지 않았는데도 1차 캐시를 조회해서 결과 출력

      === BEFORE ===
      === AFTER ===
      findMember.id = 101
      findMember.name = HelloJPA
      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (name, id) 
              values
                  (?, ?)
    

💡 동일한 조회를 두 번 하면?

public class JpaMain {

    public static void main(String[] args) {
   
        // 생략

        try {

            Member findMember1 = em.find(Member.class, 101L);
            Member findMember2 = em.find(Member.class, 101L);

            tx.commit();
        } 

       // 생략

   }
}

실행결과 : Select 쿼리 한 번만 나감!

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
  • 처음에 db에서 가져와서 영속성 컨텍스트에 올림
  • 두번째 find에서 똑같은 것을 조회 -> 1차 캐시부터 찾고, 찾은 것을 반환



2) 영속 엔티티의 동일성 보장


Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); //동일성 비교 true
  • 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
  • 즉, 1차 캐시가 있기 때문에 가능

ex.

public class JpaMain {

    public static void main(String[] args) {

        // 생략

        try {

            Member findMember1 = em.find(Member.class, 101L);
            Member findMember2 = em.find(Member.class, 101L);

            System.out.println("result = " + (findMember1 == findMember2));

            tx.commit();
        } 
        
        // 생략

    }
}

결과

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
result = true



3) 엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연


  • 영속성 컨텍스트에는 1차 캐시 외에 쓰기 지연 SQL 저장소가 있음. 쓰기 지연 SQL 저장소를 이용
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 함
transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않음 (쓰기 지연 SQL 저장소에 쌓음)

//커밋하는 순간 데이터베이스에 INSERT SQL을 보냄
transaction.commit(); // [트랜잭션] 커밋
  • 내부 메커니즘
    • em.persist(memberA); : memberA가 1차 캐시에 들어가고, 동시에 JPA가 엔티티 분석해서 insert 쿼리 생성 -> 쿼리를 쓰기 지연 SQL 저장소에 쌓음 (아직 DB에 넣지 X)

    • em.persist(memberB); : 1차 캐시에 memberB 넣으면서 insert 쿼리 생성하여 쓰기 지연 SQL 저장소에 쌓음(DB에 넣지 X)

      ⇒ 쓰기 지연 SQL : insert A, B 모두 쌓여 있음

    • transaction.commit();

      • 쓰기 지연 SQL 저장소에 쌓여있던 쿼리가 flush되면서 나감
      • 실제 database transaction이 commit됨

ex.

Member 객체에 생성자

@Entity
public class Member {

    @Id // PK
    private long id;
    private String name;

    // 생성자 (기본 생성자 꼭!!)
    public Member() {
    }
    public Member(long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter, setter
    // 생략

}

main

public class JpaMain {

    public static void main(String[] args) {

        // 생략

        try {

            Member member1 = new Member(150L, "A");
            Member member2 = new Member(160L, "B");

            em.persist(member1);
            em.persist(member2);

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

            tx.commit();
        } 
        
        // 생략

    }
}

실행 결과 : ======= 이후에 쿼리가 나감(commit 시점에)

===================
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

💡 버퍼링 기능

  • hibernate의 옵션으로 : persistence.xml에서 <property name="hibernate.jdbc.batch_size" value="10"/>
  • size만큼 모아서 데이터베이스에 한방에 네트워크로 쿼리 보내고 DB를 커밋



4) 엔티티 수정 : 변경 감지 (Dirty Checking)


  • 자바 컬렉션처럼 setter로 값을 변경해주는 persist하지 않아도 변경이 DB에 반영됨 : Dirty Checking 덕분

      EntityManager em = emf.createEntityManager();
      EntityTransaction transaction = em.getTransaction();
      transaction.begin(); // [트랜잭션] 시작
        
      // 영속 엔티티 조회
      Member memberA = em.find(Member.class, "memberA");
        
      // 영속 엔티티 데이터 수정
      memberA.setUsername("hi");
      memberA.setAge(10);
        
      //em.update(member) 이런 코드가 있어야 하지 않을까? X
        
      transaction.commit(); // [트랜잭션] 커밋
    
  • Dirty Checking

    1. JPA를 커밋하면 내부적으로 flush 호출
    2. JPA가 엔티티와 스냅샷을 비교

      💡 스냅샷

      • 1차 캐시에는 @Id, Entity 외에 스냅샷이 있음.
      • 스냅샷 : 값을 읽어온 시점, 최초 시점의 상태를 스냅샷으로 저장
    3. 변경 사항이 있으면 Update 쿼리를 쓰기지연 저장소에 만들어두고 DB에 반영

ex.

public class JpaMain {

    public static void main(String[] args) {
        
        // 생략

        try {

            Member member = em.find(Member.class, 150L);
            member.setName("ZZZZ");

//            em.persist(member); // 하지 않음!!!

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

            tx.commit();
        } 
        
        // 생략

    }
}

실행 결과

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
===============
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            name=? 
        where
            id=?



5) 엔티티 삭제


//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, memberA");

em.remove(memberA); //엔티티 삭제. transaction commit 시점에 delete가 나감



플러시


  • DB commit할 때 발생
  • 쌓아둔 SQL 쿼리들이 데이터베이스로 나가서 영속성 컨텍스트의 변경내용을 데이터베이스에 반영
  • 영속성 컨텍스트의 변경사항과 데이터베이스를 맞추는 작업 (동기화)
  • 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화하면 됨

    💡 동시성에 대한 것은 다 데이터베이스 커밋에 위임해서 사용

  • 플러시가 발생하면..
    • 변경 감지
    • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
    • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)

💡 flush 발생할 때, db transaction이 commit 되는 것이 아니고, 보내고 database commit



영속성 컨텍스트 플러시 방법


  • 방법 1. em.flush() - 직접 호출

    ex.

      public class JpaMain {
        
          public static void main(String[] args) {
                
              // 생략
        
              try {
        
                  Member member =  new Member(200L, "member200");
                  em.persist(member);
        
                  // 트랜잭션 commit 되기 전까지는 쿼리를 볼 수 없음
                  // 쿼리를 미리 반영하고 싶거나 쿼리 보고 싶으면 flush로 강제로 호출
                  em.flush(); // insert 쿼리가 이 시점에 나감
        
                  System.out.println("==================");
        
                  tx.commit();
              } 
                
              // 생략
    
          }
      }
    

    실행결과

      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (name, id) 
              values
                  (?, ?)
      ==================
    
    • ==== 전에 insert 쿼리가 호출됨
    • em.persist한 시점에서 영속성 컨텍스트에 담기고 flush 해버리니까 DB에 반영됨

    💡 flush하면 1차 캐시가 다 지워질까? -> X

    • flush는 1차 캐시 지우는 것이 아니고 변경 감지가 일어나고 지연 SQL 저장소에 있는것들이 DB에 반영되는 과정
  • 방법 2. 트랜잭션 커밋 - 플러시 자동 호출
  • 방법 3. JPQL 쿼리 실행 - 플러시 자동 호출

    💡 JPQL 쿼리 실행했을 때 Flush 왜 자동으로 호출될까?

    em.persist(memberA);
    em.persist(memberB);
    em.persist(memberC);
    
    //중간에 JPQL 실행 -> memberA, B, C 조회되지 않음. insert 쿼리 자체가 안날아감
    // JPQL은 sql로 번역되서 실행되는데 가져올게 없음. 잘못하면 문제 발생!!
    // 이를 발생하고자 jpql 실행할 때 무조건 flush
    query = em.createQuery("select m from Member m", Member.class);
    List<Member> members= query.getResultList();
    



플러시 모드 옵션


em.setFlushMode(FlushModeType.COMMIT)
옵션 상세
FlushModeType.AUTO 커밋이나 쿼리를 실행할 때 플러시 (기본값)
FlushModeType.COMMIT 커밋할 때만 플러시 (쿼리 실행할 때는 flush X)
  • auto 사용 권장



2. 엔티티의 생명주기


엔티티의 생명주기


생명주기 상세
비영속 (new/transient) - 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 최초의 멤버 객체를 생성한 상태
영속 (managed) - 영속성 컨텍스트에 관리되는 상태
- entityManager.persist하면 영속상태가 됨
준영속 (detached) - 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제 (removed) - 삭제된 상태


1) 비영속


//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
  • member 객체를 생성하여 세팅만 하고 entityManager에는 넣지 않은 상태
  • JPA와 상관 없는 상태



2) 영속


//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername(회원1);

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

//객체를 저장한 상태(영속)
em.persist(member);
  • 영속상태가 되는 경우
    1. persist하여 entityManager 안의 영속성 컨텍스트에 member 객체 들어가면서 영속 상태가 됨
    2. em.find해서 가져왔는데 1차 캐시에 없으면 db에서 가져와서 1차 캐시에 올림(= 영속 상태)
  • entityManager 안의 영속성 컨텍스트에 의해 관리됨
  • 이때는 DB에 저장되지 않음 (INSERT 쿼리는 commit 할 때 나감


3) 준영속


//객체를 삭제한 상태(삭제)
em.remove(member);
  • 영속 상태였던 entity가 아무 관계 없어짐 (영속 → 준영속)
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함 (Dirty Checking 등)
  • 준영속 상태로 만드는 방법

    방법 상세
    em.detach(entity) 특정 엔티티만 준영속 상태로 전환
    em.clear() 영속성 컨텍스트를 완전히 초기화
    em.close() 영속성 컨텍스트를 종료

    ex. detach

      public class JpaMain {
        
          public static void main(String[] args) {
                
              // 생략
        
              try {
        
                  Member member = em.find(Member.class, 150L); // 이거 하면 member는 영속 상태
                  member.setName("AAAA");
        
                  // 더 이상 영속성컨텍스트에서 관리하기 싫음
                  em.detach(member);
                  // JPA에서 관리하지 않음. 그러면 트랜잭션 실행할 때 아무일도 일어나지 않음
        
                  System.out.println("=============");
        
                  tx.commit();
              } 
                
              // 생략
    
          }
      }
    

    실행결과 : detach부터 영속성 컨텍스트에서 관리하지 않으므로 Select만 실행됨

      Hibernate: 
          select
              member0_.id as id1_0_0_,
              member0_.name as name2_0_0_ 
          from
              Member member0_ 
          where
              member0_.id=?
      =============
    

    ex. clear

      public class JpaMain {
        
          public static void main(String[] args) {
                
              // 생략
        
              try {
        
                  Member member = em.find(Member.class, 150L); // -> member : 영속 상태
                  member.setName("AAAA");
        
                  em.clear(); // 1차 캐시 다 날림
        
                  Member member2 = em.find(Member.class, 150L);
        
                  System.out.println("=============");
        
                  tx.commit();
              } 
                
              // 생략
    
          }
      }
    

    실행결과 : Select -> 1차 캐시 지움 -> Select (다시 DB에서 가져옴)

      Hibernate: 
          select
              member0_.id as id1_0_0_,
              member0_.name as name2_0_0_ 
          from
              Member member0_ 
          where
              member0_.id=?
      Hibernate: 
          select
              member0_.id as id1_0_0_,
              member0_.name as name2_0_0_ 
          from
              Member member0_ 
          where
              member0_.id=?
      =============
    


4) 삭제


//객체를 삭제한 상태(삭제)
em.remove(member);
  • 실제 DB 삭제를 요청한 상태

Leave a comment