Spring/JPA

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

Ynghan 2023. 2. 18. 19:22

JPA에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스 매핑하기 ( ORM )
  2. 영속성 컨텍스트

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

영속성 컨텍스트

  • JPA를 이해하는 데 가장 중요한 용어
  • "엔티티를 영구 저장하는 환경" 이라는 뜻
  • EntityManager.persist(entity);

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

  • 영속성 컨텍스트는 논리적인 개념
  • 눈에 보이지 않는다.
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근

엔티티의 생명주기

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

비영속

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

영속

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

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

//객체를 저장한 상태(영속)
em.persist(member);

준영속, 삭제

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

//객체를 삭제한 상태(삭제)
em.remove(member);

영속성 컨텍스트의 이점

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

 

엔티티 조회 - 1차 캐시

id가 key가 되고 객체 자체가 값이 된다.

 

//엔티티를 생성한 상태(비영속)
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");
find 메소드를 실행하면 JPA는 영속성 컨텍스트에 있는 1차 캐시를 제일 먼저 탐색한다.
캐시에 해당하는 엔티티가 있는 경우 그대로 값을 가져온다.

 

데이터베이스에서 조회

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

 

 

 

 

1차 캐시에 해당하는 엔티티가 없는 경우 DB에서 탐색하여 찾고 1차 캐시에 엔티티를 저장하고 값을 가져온다.

영속성 컨텍스트는 하나의 트랜잭션 안에서 생성되고 삭제되기 때문에 찰나의 순간동안의 성능에 이점을 가진다.

 

 

영속 엔티티의 동일성 보장

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

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

1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공

 

 

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

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

 

em.persist(memberA);

 

영속성 컨텍스트 안에는 1차 캐시와 더불어 쓰기 지연 SQL 저장소가 있다.

em.persist(memberA)를 하면 영속성 컨텍스트에서 memberA를 분석하여 1차 캐시에 엔티티를 저장함과 동시에 쓰기 지연 SQL 저장소에 INSERT SQL을 형성해둔다.

 

 

 

em.persist(memberB);

 

em.persist(memberB)도 마찬가지로 1차 캐시에 엔티티가 등록되고 쓰기 지연 SQL 저장소에 memberA에 대한 SQL 뒤에 memberB에 대한 SQL을 형성해둔다.

 

 

 

 

transaction.commit();

transaction.commit되는 시점에 쓰기 지연 SQL 저장소에 있던 SQL들이 flush되면서 query가 나가게 된다.

 

 

 

 

 

 

 

엔티티 수정 - 변경 감지

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) 이런 코드가 있어야 하지 않을까?

transaction.commit(); // [트랜잭션] 커밋

데이터베이스에 commit을 하면 내부적으로 flush()가 동작하는데 1차 캐시에 엔티티와 스냅샷을 비교한다.

스냅샷이란 1차 캐시에 엔티티를 처음 가져온 시점의 상태를 저장해둔 공간이다. 그래서 이 엔티티와 스냅샷을 비교해서 변경된 엔티티가 있는지 찾고 변경된 엔티티에 대한 UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장해두고 DB에 flush한다. 

 

 

엔티티 삭제

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

트랜잭션 commit 시점에 DELETE 쿼리가 나감.

 

 

아 ! JPA는 트랜잭션 커밋 시점에 변경된 값을 지 알아서 변경하는구나!!!

 

플러시

플러시란 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것이다.

플러시 발생

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

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

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

Member member = new Member(200L, "member200");
em.persist(member);

em.flush();

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

 

이미지는 줄을 출력하기 전에 insert 쿼리가 나가는 것을 보여준다.
flush를 해서 DB에 바로 쿼리가 나가는 것이다.
이것은 1차 캐시와 무관하며 쓰기 지연 SQL 저장소에 쌓여 있던 쿼리들이 DB로 나가는 것이다.

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();

JPA는 persist 시점에 DB로 쿼리가 날라가지 않기 때문에 DB에서 데이터를 가져와야하는 JPQL이 트랜잭션 커밋 이전에 호출된다면 문제가 발생할 수 있다. 이를 방지하기 위해서 JPA는 JPQL이 실행되는 시점에 flush를 자동으로 호출시키고 쿼리를 날린다. 

 

플러시는!

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

 

준영속 상태

  • 영속 -> 준영속
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

준영속 상태로 만드는 방법

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

'Spring > JPA' 카테고리의 다른 글

실전 예제  (0) 2023.02.19
엔티티 매핑  (0) 2023.02.19
JPA 애플리케이션 개발  (0) 2023.02.18
JPA 프로젝트 생성  (0) 2023.02.18
프록시와 연관관계 관리 - 영속성 전이(CASCADE)와 고아 객체  (0) 2023.02.10