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

01. 영속성 컨텍스트 1

  • JPA에서 가장 중요한 2가지

    • 객체와 관계형 데이터베이스 매핑하기
    • 영속성 컨텍스트
  • 엔티티 매니저 팩토리와 엔티티 매니저
    엔티티 매니저 팩토리를 통해 고객의 요청이 들어올 때마다 엔티티 매니저가각각 생성이 되고 엔티티 매니저는 내부적으로 DB의 커넥션(conn)을 이용해DB를 사용하게 된다.

영속성 컨텍스트

  • JPA를 이해하는데 가장 중요한 용어
  • 뜻: "엔티티를 영구 저장하는 환경"
  • EntityManager.persist(entity); - DB에 저장한다는 것이 아니라 영속성 컨텍스트를 통해서 영속화한다는 의미 즉 persist 메소드는 DB에 저장하는 게 아니라 entity를 영속성 컨텍스트에 저장한다.

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

  • 영속성 컨텍스트는 논리적인 개념
  • 눈에 보이지 않는다.
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
    엔티티 매니저를 생성하면 그 안에 1:1로 영속성 컨텍스트가 생성이 됨. 즉 엔티티 매니저 안에 눈에 보이지 않는 영속성 컨텍스트라는 공간이 생성.

엔티티의 생명주기

ex) member.persist

  • 비영속(new/transient) : 최초의 멤버 객체를 생성한 상태, 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
    - 멤버 객체를 생성을 했음. 엔티티 매니저에 아무것도 넣지 않은 상태

    • JPA와 전혀 관계가 없는 상태
      // 객체를 생성한 상태(비영속)
      Member member = new Member();
      member.setId("member1");
      member.setUsername("회원1");
  • 영속(managed) : 영속성 컨텍스트에 관리되는 상태
    - 엔티티 매니저 안에 있는 영속성 컨텍스트가 들어있음.
    멤버 객체를 생성한 다음에 엔티티 매니저를 얻어와서 persist를 통해 member객체를 집어넣으면 엔티티 매니저안에 있는 영속성 컨텍스트 안에 member 객체가 들어가면서 영속 상태가 됨.

    • 영속 상태가 된다고 해서 DB에 바로 쿼리가 날라가는 것이 아님.
      트랜잭션을 커밋하는 시점에 영속성 컨텍스트에 있는 애가 DB에 쿼리가 날라감

      // 객체를 생성한 상태(비영속)
      Member member = new Member();
      member.setId("member1");
      member.setUsername("회원1");
      package hellojpa;
      
      public class JpaMain {
          public static void main(String[] args) {
              EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
              EntityManager em = emf.createEntityManager();
              //code
              EntityTransaction tx = em.getTransaction(); // 트랜잭션 얻을 수 있음
              tx.begin(); // DB 트랜잭션 시작
      
          try {
              // Member Entity의 상태가 비영속, JPA와 관련 없음, DB에 들어가지도 않음.
              Member member = new Member();
              member.setId(100L);
              member.setName("HelloJPA");
      
              // 엔티티 매니저 안에 들어 있는 영속성 컨텍스트를 통해서 member 객체가 관리됨, DB에 저장되지 않음.
              System.out.println(" ==== BEFORE ===== " );
              em.persist(member);
              System.out.println(" ==== AFTER ===== " );
              tx.commit(); // DB 커밋
      
          } catch (Exception e) {
              tx.rollback();
          }finally {
              em.close();
          }
          emf.close();
      	}
      			}
      
      ==== BEFORE ===== 
      ==== AFTER ===== 
      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (name, id) 
              values
                  (?, ?)
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리

  • 삭제(removed): 삭제

            em.detach(member); // 영속성 컨텍스트를 지움, 아무관계가 없어짐
            em.remove(member); // 실제 DB  삭제를 요청하는 상태, 실제 영구 저장한 DB에서 지우는 상태

영속성 컨텍스트의 이점

애플리케이션과 DB 사이에 중간 계층으로 존재(버퍼링, 캐싱의 이점)

  • 1차 캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

02. 영속성 컨텍스트 2

엔티티 조회, 1차 캐시

영속성 컨텍스트는 내부에 1차 캐시를 가지고 있음

멤버 객체를 생성한 비영속 상태에서 member객체를 집어넣으면

@Id@Entity
"member1"member

1차 캐시
Key가 DB PK로 매핑한 애가 되고 Entity 객체 자체가 값이 됨
Key: "member1" value: member(객체 자체가 값)

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

// 엔티티를 영속
em.persist(member);

장점은 무엇이 있을까?

1차 캐시에서 조회

Member member  = new Member();
member.setId("member1");
member.setUsername("회원1");

// 1차 캐시에 저장됨
em.persist(member);

// 1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");

em.find를 통해서 조회를 하면 JPA는 영속성 컨텍스트에서 find("member1")라고 찾으면 DB에서 찾는 게 아니고 먼저 1차 캐시에서 값을 찾는다. 만약 1차 캐시에 값이 존재하면 캐시에 있는 값을 조회해온다.

데이터베이스에서 조회

만약 1차 캐시에 없는 member2를 조회하면
1. find('member2') - 1차 캐시 조회했지만 없음
2. JPA는 DB를 조회
3. 만약 DB에 member2가 있으면 1차 캐시에 member2를 저장
4. 반환
5. 이후에 다시 member2를 조회하면 DB를 조회하지 않고 1차 캐시에 저장된 값을 반환 해줌.
But. 사실 크게 도움이 되지는 않음
엔티티 매니저는 DB 트랜잭션 단위로 많이 만들고 DB 트랜잭션이 끝날 때 같이 종료를 시킴. 고객의 요청이 하나 들어와서 비즈니스 로직이 하나 끝나게 되면 영속성 컨텍스트를 지운다는 의미(1차 캐시도 다 날라감). 여러명의 고객의 사용하는 캐시가 아님. 애플리케이션 전체에서 공유하는 캐시는 2차 캐시. DB의 트랜잭션 하나당 1차 캐시에서 처리하기 때문에 큰 이득은 없음.

Member findMember = em.find(Member.class, "member2");
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        //code
        EntityTransaction tx = em.getTransaction(); // 트랜잭션 얻을 수 있음
        tx.begin(); // DB 트랜잭션 시작

        try {
            // Member Entity의 상태가 비영속, JPA와 관련 없음, DB에 들어가지도 않음.
            Member member = new Member();
            member.setId(101L);
            member.setName("HelloJPA");

            // 엔티티 매니저 안에 들어 있는 영속성 컨텍스트를 통해서 member 객체가 관리됨, DB에 저장되지 않음.
            System.out.println(" ==== BEFORE ===== " );
            em.persist(member);
            System.out.println(" ==== AFTER ===== " );

            Member findMember = em.find(Member.class, 101L);
            // 여기서 중요한거 조회용 SQL이 나가는지 안나가는지
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.name = " + findMember.getName());

            tx.commit(); // DB 커밋

        } catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}
==== BEFORE ===== 
==== AFTER ===== 
findMember.id = 101
findMember.name = HelloJPA
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

조회를 했는데 SELECT 쿼리가 안나감 why? 저장을 할 때 1차캐시에 저장이 됨.그리고 똑같은 PK로 조회를 했기 때문에 DB에서 가져오는 게 아니라 1차 캐시에 있는 걸 먼저 조회.

이번에는 코드를 약간 수정하면 어떻게 되는지 보자.

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        //code
        EntityTransaction tx = em.getTransaction(); // 트랜잭션 얻을 수 있음
        tx.begin(); // DB 트랜잭션 시작

        try {

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

            tx.commit(); // DB 커밋

        } catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}

SQL문이 한번만 실행이 됨.
JPA가 가져올 때 1차 캐시에 값이 없어서 DB에서 값을 가져오기 위해 SQL문이 실행됨. 이후 영속성 컨텍스트 1차 캐시에 값을 저장함. 이후에 같은 값을 요청하니까 1차 캐시부터 조회하기 떄문에 1차 캐시에 저장된 값을 반환해주기 때문에 SQL문을 실행할 필요가 없음

성능의 이점보다 객체지향적으로 코드를 작성하는데 이점이 있음.

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

영속 엔티티의 동일성 보장

마치 자바 컬렉션에서 똑같은 레퍼런스 객체를 꺼내면 값이 동일하듯이
JPA가 영속 엔티티의 동일성을 보장 - 1차 캐시가 있기 때문에 가능, 같은 트랙잰션 내에서 실행

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

System.out.println( a== b); // 동일성 비교 true

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

먼저 트랜잭션을 시작하고 persist로 member객체를 저장
여기까지 INSERT SQL을 DB에 보내지 않는다.
영속성 컨텍스트 안에는 쓰기 지연 SQL 저장소도 있음.
1. meberA를 영속성 컨텍스트에 persist하는 순간 1차 캐시에 값이 들어가면서 동시에 JPA가 Entity를 분석해서 Insert 쿼리를 생성해서 쓰기 지연 SQL 저장소라는 곳에 쌓아 둠.
2. meberB를 영속성 컨텍스트에 persist하는 순간 1차 캐시에 값이 들어가면서 동시에 JPA가 Entity를 분석해서 Insert 쿼리를 생성해서 쓰기 지연 SQL 저장소라는 곳에 쌓아 둠.
3. JPA가 SQL 저장소에 쭉죽 쌓고 있음.
4. 트랜잭션을 커밋하는 순간 쓰기 지연 SQL 저장소에 있던 쿼리문들이 flush가 되면서 DB에 쿼리문들이 날아간다.
5. 그리고 실제 DB에 커밋된다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        //code
        EntityTransaction tx = em.getTransaction(); // 트랜잭션 얻을 수 있음
        tx.begin(); // DB 트랜잭션 시작

        try {

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

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

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


            tx.commit(); // DB 커밋

        } catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}

인서트 쿼리가 2번 나갔지만 밑에 있는 프린트 문이 먼저 출력되는 것을 알 수 있음. 왜 이렇게 할까? persist 할때마다 쿼리문을 날리면 DB를 최적화할수 있는 여지 자체가 없음. 옵션 하나로 성능을 좌지우지 할 수 있기 때문에 사용

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

엔티티 수정 변경 감지

JPA는 더티체킹 - 변경 감지(Dirty Checking)라는 기능으로 엔티티를 변경할 수 있음. DB의 값이 변경됨 -
1. DB에 트랜잭션 커밋을 하는 순간 내부적으로 flush()를 호출한다.
2. 엔티티와 스냅샷을 비교한다.

  • 스냅샷: 1차 캐시에 들어온 값의 최초 시점의 상태를 떠 놓음
  1. memberA가 값이 변경된 것을 확인하면 SQL 쓰기 지연 저장소에 UPDATE 쿼리문을 저장한다.
    4.그리고 UPDATE 쿼리를 DB에 반영한다.
    5.마지막으로 commit을 수행한다.
@IdEntity스냅샷
memberAmemberAmemberA 스냅샷
memberBmemberBmemberB 스냅샷
package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        //code
        EntityTransaction tx = em.getTransaction(); // 트랜잭션 얻을 수 있음
        tx.begin(); // DB 트랜잭션 시작

        try {

            // 영속
            Member member1 = em.find(Member.class, 150L);
            member1.setName("ZZZZ");

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


            tx.commit(); // DB 커밋

        } catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}
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=?

엔티티 삭제

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

03. 플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영

플러시 발생

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

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

  • em.flush() - 직접 호출
  • 트랜잭션 커밋 - 플러시 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동 호출

JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();

플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)
  • FlushModeType.AUTO
    커밋이나 쿼리를 실행할 때 플러시 (기본값)
  • FlushModeType.COMMIT
    커밋할 때만 플러시

영속성 컨텍스트를 비우지 않음

  • 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
  • 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화하면 됨

04. 준영속 상태

영속 -> 준영속

  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)

  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

  • 준영속 상태로 만드는 방법
    em.detach(entity)

  • 특정 엔티티만 준영속 상태로 전환
    em.clear()

  • 영속성 컨텍스트를 완전히 초기화
    em.close()

  • 영속성 컨텍스트를 종료

좋은 웹페이지 즐겨찾기