SQL mapper & ORM/JPA

3.영속성 관리

wooweee 2023. 10. 1. 10:39
728x90

1. 영속성 컨텍스트

1.1. jpa에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스 매핑하기 : 실제 설계적인 측면 (정적 측면), 객체와 db를 어떻게 연결하는지

  2. 영속성 컨텍스트(entity Manager의 1차 cache라고 봐도 무방 - spring에선 약간 다르다.) : JPA 내부 동작 방식 및 메커니즘

 

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

 

 

1.3. 영속성 컨텍스트

  • 엔티티를 영구 저장하는 환경
  • 영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다.
  • 엔티티 매니저를 통해서 영속석 컨텍스트에 접근 가능하다.
  • 영속성 context 내부에 1차 캐시, 1차 캐시 내부에 스냅샷, 쓰기 지연 SQL 저장소 존재
EntityManager.persist(entity); //db에 저장하는 것이 아니라 영속성 컨텍스트라는 곳에 저장

 

1차 캐시가 영속성 context
spring일 때 영속성 컨텍스트

 

1.4. 영속성 컨텍스트의 이점

  1. 1차 캐시 - 개별적이다. 10명의 고객이 동시 접속 시, 10개의 별도 캐시를 가진다.
  2. 동일성 보장 - java의 컬랙션과 같다고 보면 된다.
  3. 트랜잭션을 지원하는 쓰기 지연 - 쓰기 지연 SQL 저장소
  4. 변경 감지 - dirty check   :  update에 유용하게 사용
  5. 지연 로딩 - lazy loading   : 실무에서 중요하게 여기는 기능 select query를 나중에 날림

 

2. 엔티티 생명주기

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

 

 

3. 1차 캐시 & 조회

3.1. 1차 캐시에서 조회

 

3.2. 데이터베이스에서 조회 (1차 캐시에서 못찾았을 경우)

  • entityMnager는 tx 단위로 만들고 tx가 끝났을 때 종료 된다.
    = 고객 biz가 끝나면 영속성 컨텍스트를 지운다는 의미 == 1차 캐시도 날라감
  • 영속성 context는 여러명의 고객이 사용하는 cache가 아니다
    -> 여러명의 고객이 공유하는 cache는 2차 cache라고 한다.
  • 1차 캐시는 db의 1개 tx에서만 효과가 있기 때문에 실질적으로 성능상 이점을 얻지 못한다.
    - 정말 biz 로직이 복잡할 경우 도움이 되고 성능상 이점 보다는 컨셉적인 이득이 크다. (객체지향적 코드 작성 가능하게 해줌)

 

 

 

 

4. 영속 엔티티의 동일성 보장 

  • 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 db가 아니 application 차원에서 제공
Member a = em.find(Member.class, "member1);
Member b = em.find(Member.class, "member1);

Suytem.out.printlb (a == b); // true

 

 

5. 트렌잭션을 지원하는 쓰기 지연 - 등록, 변경, 삭제

  • 현재는 등록을 기준으로 설명
em.persist(memberA);
em.persist(memberB);
// 여기까지 insert SQL을 db에 보내지 않는다.

transaction.commit();
// commit()하는 순간 insert sql을 보낸다. - 예외 존재. Identity 설정의 경우

 

 

6. 엔티티 수정 - 변경 감지(dirty check)

  •  참고
    • persist(): 새로운 엔티티를 영속성 컨텍스트에 저장(1차 캐시에 저장)하고, 쓰기 지연 SQL 저장소에 INSERT SQL을 생성하여 추가. 스냅샷 비교 없이 영속 상태의 엔티티를 DB에 삽입

    • delete(): 엔티티를 영속성 컨텍스트에서 삭제(1차 캐시에서 삭제)하고, 쓰기 지연 SQL 저장소에 DELETE SQL을 생성하여 추가합니다. 스냅샷 비교 없이 엔티티를 DB에서 삭제

    • update(): JPA에서는 엔티티의 변경 감지(Dirty Checking)를 사용하여 수정된 엔티티를 자동으로 감지하고, 쓰기 지연 SQL 저장소에 UPDATE SQL을 추가. 스냅샷 비교를 통해 어떤 필드가 변경되었는지 확인하고 변경된 필드만을 업데이트

 

7.  3~6 까지 학습 code

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaChap3 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            /*
            // 비영속
            Member member = new Member();
            member.setId(100L);
            member.setName("HelloJPA");

            // 영속 - 이 때 db에 저장이 되는 것이 아니다.
            System.out.println("=== BEFORE ===");
            em.persist(member); // 만약 db에 저장되지 않기 때문에 before after 사이에 query가 생성되어 db에 날라가지 않는다.
            System.out.println("=== AFTER ===");

            tx.commit(); // commit하는 시점에 영속성 context에 존재하는 값이 db에 날라간다.
             */

            /*
            // 비영속
            Member member = new Member();
            member.setId(101L);
            member.setName("HelloJPA");

            // 영속 - 이 때 db에 저장이 되는 것이 아니다.
            em.persist(member); // 만약 db에 저장되지 않기 때문에 before after 사이에 query가 생성되어 db에 날라가지 않는다.

            // 1차 캐시 - 영속성 컨텍스트에서 값을 찾는 것이기 때문에 select query가 출력되지 않는다.
            Member findMember = em.find(Member.class, 101L);
            System.out.println("findMember.getId() = " + findMember.getId());
            System.out.println("findMember.getName() = " + findMember.getName());

            tx.commit(); // commit하는 시점에 영속성 context에 존재하는 값이 db에 날라간다.
             */

            /*
            // 101L이 저장되고 나서 tx가 종료 된 상태 이후, 다시 조회 하는 과정
            * tx 종료시, 영속성 context 또한 종료 되기 때문에 entity manager 종료, 1차 cache 종료 된다.
            * 1st 조회는 1차 cache에 아무것도 없기 때문에 db를 찾아서 select 쿼리를 날리게 되고
            * 2nd 조회시, 1차 cache를 통해서 조회 함. select 쿼리 안날림

            // 1차 cache - 처음에 없는 경우
            Member findMember1 = em.find(Member.class, 101L); // select query 날림
            Member findMember2 = em.find(Member.class, 101L); // 1차 캐시 이용, select query 안날림

            // 영속 엔티티의 동일성 보장
            System.out.println("result = " + (findMember1 == findMember2));
            tx.commit(); // commit하는 시점에 영속성 context에 존재하는 값이 db에 날라간다.
            */

            /*
            엔티티 등록 - tx를 지원하는 쓰기 지연
            Member member1 = new Member(150L, "A");
            Member member2 = new Member(160L, "A");

            System.out.println("=== 1st persist start===");
            em.persist(member1);
            System.out.println("=== 1st persist finish ===");
            System.out.println("=== 2nd persist start===");
            em.persist(member2);
            System.out.println("=== 2nd persist finish ===");

            tx.commit(); // 커밋 시점에서 insert 쿼리를 한번에 db로 보내서 성능에 도움
            */

            /*
            // 엔티티 수정 변경 감지
            Member member = em.find(Member.class, 150L); // 초기 값 찾음
            member.setName("changeName"); // 값 변경

            System.out.println(" =========== ");
            tx.commit(); // commit 시점에 초기값과 값 변경 값이 다르면 update 날리고 쓰기지연SQL에서 해당 sql 제거
             */

            // 엔티티 삭제 - 엔티티 수정과 동일 메커니즘
            Member member = em.find(Member.class, 150L);
            em.remove(member); // 영속성 context에서 제거

            System.out.println(" =========== ");
            tx.commit(); // commit 시점에 스냅샷 확인 없이 delete 쿼리를 날려서 실제 db에서 삭제


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

 

8. 플러시

  • insert, update, delete SQL을 db에 날리는 method()

 

8.1. 플러시 메커니즘

  • 변경
    1. tx가 commit()되는 시점에 flush() 자동 발생
    2. 변경 감지( )
    3. 수정된 엔티티 쓰기지연SQL저장소에 등록
    4. 쓰기지연SQL저장소의 쿼리를 데이터베이스에 전송
  • 등록, 제거
    1. tx가 commit()되는 시점에 flush() 자동 발생
    2. 이미 쓰기지연SQL저장소에 등록되어있는 쿼리를 데이터베이스에 전송

 

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

  1. em.flush() - 직접 호출 
    • 거의 사용할 일이 없으나 test 때 사용되니깐 알아둬야함
  2. 트랜잭션 커밋 - 플러시 자동 호출
  3. JPQL 쿼리 실행 - 플러시 자동 호출
    • JPQL은 그냥 sql로 번역이 되어 수행이 되는 작업이기 때문에 영속성 컨텍스트에서 값을 찾지 않고 바로 db로 부터 값을 찾게 된다.
    • 따라서, JPQL이 쿼리를 조회할 수 있도록 Flush()를 발생시켜 db로 부터 값을 조회할 수 있도록 해준다.

 

8.3. 플러시 모드 옵션

  • 내가 건들일 일이 극히 없음
  • 그리고 건들이지 말고 Auto 사용하는 것을 권장
em.setFlushMode(FlushModeType.AUTO) // 기본값. 커밋이나 쿼리(jpql)를 실행할 때 플러시
em.setFlushMode(FlushModeType.COMMIT) // 커밋할 때문 플러시

 

8.4. 플러시 정리

  • 영속성 컨텍스트를 비우지 않는다.
  • 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
    = 쓰기 지연 sql 저장소 Query를 db로 넘긴다.
  • 트랜잭션 작업 단위가 중요하다. -> commit 직전에만 동기화 되면 된다.

 

8.5. 플러시 코드

  • 쓰기 지연 SQL 저장소에 있는 SQL이 DB로 전송되면 해당 SQL은 쓰기 지연 SQL 저장소에서 제거
  • 즉, flush 작업을 수행하면 쓰기 지연 SQL 저장소의 SQL은 DB로 전송되어 실행되며, 이후 해당 SQL은 저장소에서 제거된다.

  • flush()를 명시적으로 호출한 경우와 commit()에 의한 자동 flush()가 중복 호출되는 경우
  • 중복 호출은 일반적으로 문제가 되지 않는다. 이 두 번의 flush() 호출은 동일한 내용을 DB로 보내질 때, JPA는 중복 호출에 대한 최적화를 수행하여 불필요한 SQL 전송을 방지
package hellojpa;

import javax.persistence.*;

public class JpaChap3_flush {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            /*
            // 엔티티 삭제 - 엔티티 수정과 동일 메커니즘
            Member member = new Member(200L, "member200");
            em.persist(member);

            // flush()
            * tx가 commit 되기 전에 미리 db에 반영을 하고싶거나 query를 미리 보고 싶은 경우 강제로 호출
            * flush를 했다고 1차 캐시가 지워지는 것이 아니라 1차 캐시의 스냅샷 확인 후,
            * 달라진 것이 존재 시 쓰기 지연 sql 저장소의 쿼리를 db로 보내주는 역할 까지만 수행

            em.flush();
            tx.commit(); // commit 시점에도 flush()가 자동 호출 되므로 flush() 중복 호출, JPA가 중복 호출에 대한 최적화 수행함 걱정 ㄴㄴ
            */

            /*
            // 플러시 모드 옵션
            em.setFlushMode(FlushModeType.COMMIT); // jpql code 위에만 있으면 된다.
            Member member = new Member(202L, "member202");
            em.persist(member);

            TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
            List<Member> resultList = query.getResultList();
            System.out.println("resultList = " + resultList); // resultList = []
             */

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

 

 

9. 준영속 상태

  • 영속 상태 였던 것을 영속성 컨텍스트에서 빠지는 상태로 만드는 것
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리 (detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

 

9.1. 준영속 상태로 만드는 방법

  1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환

  2. em.clear() : 영속성 컨텍스트를 완전히 초기화, test시, 아주 유용

  3. em.close() : 영속성 컨텍스트를 종료

 

9.2. 준영속 상태 code

package hellojpa;

import javax.persistence.*;

public class JpaChap3_detach {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            /*
            // sample Data
            Member member = new Member(1L, "zzzz");
            Member member1 = new Member(2L, "zzzz");
            em.persist(member);
            em.persist(member1);
            tx.commit();
            */

            /*
            // em.detach() : 개별 영속성 컨텍스트 전체 준영속 상태로 변경
            Member member = em.find(Member.class, 1L); // 영속 상태로 올라감
            Member controlMember =em.find(Member.class, 2L); // detach에 영향 안받는걸 보여주기 위한 용도

            // 영속 컨텍스트를 관리하기 싫어 할 때 detach()로 영속성 context에서 원하는 key-value를 날려버림
            em.detach(member); // member만 1차 cache에서 준영속 상태로 날라감

            member.setName("aaaa"); // dirty checking 후 변경 update query를 commit 시점에 날릴 것임.
            controlMember.setName("aaaa");

            System.out.println("=== === ===");
            tx.commit();
            */

            /*
            // em.clear() : 전체 영속성 컨텍스트 전체 준영속 상태로 변경
            Member member = em.find(Member.class, 1L); // 영속 상태로 올라감
            Member controlMember = em.find(Member.class, 2L); // detach에 영향 안받는걸 보여주기 위한 용도

            member.setName("bbbb"); // dirty checking 후 변경 update query를 commit 시점에 날릴 것임.
            controlMember.setName("bbbb");

            em.clear(); // 영속 컨텍스트 통으로 다 날림
            System.out.println("=== === ===");

            Member member1 = em.find(Member.class, 1L); // 영속성 context에 없어서 다시 쿼리 날림
            Member controlMember1 = em.find(Member.class, 2L); // 동일

            System.out.println("=== === ===");
            tx.commit();
            */

            /*
            // em.clear() : 영속성 컨텍스트를 아예 날리는 거여서 이후, 관리 자체를 못함
            Member member = em.find(Member.class, 1L); // 영속 상태로 올라감
            Member controlMember = em.find(Member.class, 2L); // detach에 영향 안받는걸 보여주기 위한 용도

            member.setName("bbbb"); // dirty checking 후 변경 update query를 commit 시점에 날릴 것임.
            controlMember.setName("bbbb");

            em.close(); // 영속 컨텍스트 통으로 다 날림

            System.out.println("=== em 외적인 작업은 수행 됨 ===");
            int i = 1;
            int i2 = i + 3;
            System.out.println("i2 = " + i2);

            System.out.println("=== em 관련 작업 수행시, 이후 모른 line 작업 중단 ===");
            Member member1 = em.find(Member.class, 1L); // 수행 안됨
            Member controlMember1 = em.find(Member.class, 2L); // 수행 안됨
            System.out.println("작업 중단 됨. 콘솔에 안 찍힘");

            tx.commit(); // 애매한 부분...
             */

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