영속성 컨택스트 - 내부 동작 방식

hyyyynjn·2021년 8월 24일
0
post-thumbnail

✅영속성 컨텍스트

JPA에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스 매핑하기 (설계와 관련)
  2. 영속성 컨택스트 (실제 JPA가 내부에서 동작하는지 이해하기)

고객의 요청이 들어올 때마다 EntityManagerFactory를 통해 EntityManager를 생성한다.
EntityManager는 내부적으로 DatabaseConnection을 통해 DB를 사용한다.

📢영속성 컨택스트란 무엇인가❓

영속성 컨택스트 : 엔티티를 영구 저장하는 환경 이라는 의미이다.
EntityManager.persist(entity); 의 의미 : DB에 엔티티를 저장한다는게 아니라 영속성 컨택스트에 엔티티를 영속화(저장)한다는 의미이다.

📢영속성 컨택스트는 논리적인 개념으로 눈에 보이지 않는다.

영속성 컨택스트에 접근하기 위해 EntityManager를 통해야한다.

J2SE 환경에서 EntityManager를 생성하면 1대1 관계의 PersistenceContext (영속성 컨택스트) 가 생성된다.
👉 쉽게 말해 EntityManager 안에 눈에 보이지 않은 공간인 PersistenceContext이 존재한다.

📢엔티티의 생명주기

  • 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 (객체를 처음 생성한 상태)
  • 영속 (managed) : 영속성 컨텍스트에 관리되는 상태 (EntityManager.persist(entity) 이후의 상태)
  • 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 (removed) : 삭제된 상태

✍비영속

member 객체만 생성한 상태 = JPA와 무관한 상태

✍영속

member 객체를 생성한 뒤,
EntityManager를 생성하여 em.persist(member); 코드를 실행하면
👉 EntityManager영속성 컨택스트 안에 member 객체가 들어가서 영속 상태가 된다.

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            // 비영속 상태
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloJPA");

            // 영속 상태 (이 때 DB에 저장되지 않음)
            System.out.println("=== BEFORE ===");
            em.persist(member);
            System.out.println("=== AFTER ===");

            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다다
        emf.close();
    }
}

=== BEFORE ====== AFTER === 사이에 쿼리가 생성되지 않는다.
👉 영속 상태가 된다고 해서 바로 DB에 쿼리가 날라가진 않는다.
👉 tx.commit(); 코드가 실행되는 시점에 DB에 쿼리가 날라간다.

✍준영속, 삭제

준영속 상태 👉 detach

실제 DB 삭제를 요청하는 상태

📢영속성 컨택스트의 이점

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

✍1차 캐시

member객체를 생성하고 persist하면
영속성 컨택스트 내부의 1차 캐시에 member의 pk가 key, member객체가 value가 되어 담긴다.

em.find(Member.class, "member1");으로 member를 조회하면
JPA는 DB에서 찾기 전에 1차 캐시에서 pk가 member1인 member객체를 조회한다.

em.find(Member.class, "member2"); 으로 member를 조회하면
find("member2")으로 1차 캐시에서 member를 찾지만 없다.
👉 그럼 DB에서 조회하여 member2 객체를 1차 캐시에 저장한다.
👉 그리고 member2를 반환한다.
이후에 member2를 다시 조회하면 1차 캐시에 있는 member2 객체가 반환된다.

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            // 비영속 상태
            Member member = new Member();
            member.setId(101L);
            member.setName("HelloJPA");

            // 영속 상태 (이 때 DB에 저장되지 않음)
            System.out.println("=== BEFORE ===");
            // 1차 캐시에 "101" : "member" 가 저장된다
            em.persist(member);
            System.out.println("=== AFTER ===");

            // find 했지만 SELECT 쿼리가 DB로 날아가지 않는다.
            // 1차 캐시에서 pk = 101에 해당하는 member를 조회하여 findMember에 넣는다.
            Member findMember = em.find(Member.class, 101L);
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.name = " + findMember.getName());
            
            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다다
        emf.close();
    }
}

select query가 나가지 않았는데 member 객체를 조회할 수 있다.
em.persist(member); 에서 1차 캐시에 "key=101" : "value=member" 가 저장된다
em.find(Member.class, 101L); 코드가 실행되면 DB에서 조회하기 전에 1차 캐시에서 key가 101인 member객체를 조회한다.

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            System.out.println("=== BEFORE findMember1 ===");
            Member findMember1 = em.find(Member.class, 101L);
            System.out.println("=== AFTER findMember1 ===");
            System.out.println("=== BEFORE findMember2 ===");
            Member findMember2 = em.find(Member.class, 101L);
            System.out.println("=== AFTER findMember2 ===");

            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다다
        emf.close();
    }
}

Member findMember1 = em.find(Member.class, 101L); 을 실행하면 SELECT SQL을 통해 DB에서 pk=101의 member를 영속성 컨택스트에 올려놓는다.
Member findMember2 = em.find(Member.class, 101L); 을 실행하면 영속성 컨택스트 안의 1차 캐시에서 pk=101인 member를 조회하기 때문에 Select 쿼리가 나가지 않는다.

1차 캐시는 하나의 트랜잭션 안에서만 활용할 수 있다. (1차 캐시 만으로 큰 성능 향상을 기대하기 어렵다)

✍영속 엔티티의 동일성 보장

1차 캐시로 인해 영속 엔티티의 동일성이 보장된다.

✍트랜잭션을 지원하는 쓰기 지연

JPA는 commit할 때 INSERT SQL를 보낸다.

영속성 컨택스트 안에는 쓰기 지연 SQL 저장소가 존재한다.
em.persist(memberA); 로 memberA를 영속성 컨택스트에 넣으면 1차 캐시에 memberA가 들어감과 동시에
memberA 엔티티를 분석하여 INSERT SQL을 생성한 뒤 이를 쓰기 지연 SQL 저장소에 쌓아둔다.

em.persist(memberA);의 경우에도 1차 캐시에 넣고, INSERT SQL을 쓰기 지연 SQL 저장소에 차곡 차곡 쌓아둔다.

transaction.commit(); 하는 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 한꺼번에 날아간다 (flush)

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

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

            // 쓰기 지연 SQL 저장소에 INSERT SQL이 쌓인다.
            em.persist(member1);
            em.persist(member2);
            System.out.println("=== BEFORE commit ===");

            // commit 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 날아간다.
            tx.commit();
            System.out.println("=== AFTER commit ===");
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다다
        emf.close();
    }
}

commit 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 날아간다.

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

JPA는 더티 체킹 (변경 감지)를 통해 DB의 값이 변경된다.
HOW ❓ 👉 영속성 컨택스트안에 비밀이 숨어있다.

tx.commit(); 하면 내부적으로 flush가 실행된다
👉 엔티티와 스냅샷을 비교하여 변경된 데이터를 감지한다. (스냅샷 : 엔티티 값을 읽어온 최초 시점의 엔티티의 데이터)
👉 데이터가 변경되었다면 UPDATE SQL을 쓰기 지연 SQL 저장소에 쌓아둔다
👉 쓰기 지연 SQL 저장소의 UPDATE SQL을 DB에 반영하고
👉 commit한다.
이 과정이 변경 감지 (더티 체킹) 이다.

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

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

            // em.persist(member); -> 이걸 해줄 필요가 없다!!
            System.out.println("=== BEFORE commit ===");
            // commit 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 날아간다.
            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다다
        emf.close();
    }
}

member.setName("ZZZZ"); 으로 member의 값만 변경했을 뿐인데 UPDATE SQL이 날아갔다.
게다가 em.persist(member) 코드를 실행하지 않았음에도 불구하고
👉 Java Collection을 다루듯 값만 변경했는데도 UPDATE SQL이 나간다.

✍엔티티 삭제

em.remove(memberA); 하면 DELETE SQL이 나간다.


✅ 플러시

플러시란 영속성 컨택스트의 변경 내용을 데이터베이스 반영하는 것이다.
다시 말해, 영속성 컨택스트의 Query들을 데이터베이스에 날리는 것

  • 트랜잭션이 commit되면 자동적으로 플러시가 발생한다.

📢플러시가 발생한 뒤 생기는 일

  1. 변경 감지 (Dirty Checking)이 일어난다
  2. 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록한다
  3. 쓰기 지연 SQL 저장소의 등록, 수정, 삭제 Query를 데이터베이스에 전송한다.

👉 플러시를 해도 영속성 컨택스트의 1차 캐시가 지워지지 않는다.
다만 쓰기 지연 SQL 저장소에 담긴 쿼리가 데이터베이스에 반영될 뿐이다.

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

  • em.flush()를 직접 호출한다. (사실상 직접 호출할 일이 없다)
  • 트랜잭션을 commit하면 자동으로 플러시가 호출된다.
  • JPQL Query를 실행하면 자동으로 플러시가 호출된다.

✍ em.flush() 호출

package hellojpa;

import javax.persistence.*;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

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

            em.flush();
            System.out.println("=== BEFORE commit ===");
            // commit 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 날아간다.
            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다
        emf.close();
    }
}

em.persist(member); 한 시점에 쿼리가 쓰기 지연 SQL 저장소에 담겨있다.
em.flush()하는 순간 쿼리가 DB에 바로 반영된다.

✍ JPQL 쿼리 실행

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 영속성 컨택스트에 memberA, B, C가 저장된다.
// 이 때까지 데이터베이스에 날아가는 쿼리가 없다. 

//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();
  • 일단 JPQL의 쿼리를 실행하기 위해서 데이터베이스에 원하는 정보가 있어야 한다.
  • createQuery 실행 전에 데이터베이스에 INSERT Query가 나가지 않았다 (데이터베이스에 원하는 정보가 저장되지 않은 상태)
    👉 JPA는 JPQL 쿼리를 실행하기 전에 무조건 플러시를 호출하여 JPQL 쿼리가 실행되기 전 쓰기 지연 SQL 저장소의 쿼리들을 데이터베이스에 날린다..

📢플러시 모드 옵션

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (기본값, 권장)
  • FlushModeType.COMMIT : 커밋할 때만 플러시 (쿼리를 실행할 때는 플러시를 실행하지 않는다)
    • JPQL 쿼리 실행시 플러시가 자동 호출되는 것을 막을 수 있다.

📢플러시 정리

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

✅ 준영속 상태

  • 영속 상태 : persist로 영속성 컨택스트에 저장한 상태, find 하였는데 1차 캐시에 없어서 DB에 조회한 후 1차 캐시에 저장한 상태
  • 준영속 상태 : 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 것
    • 영속성 컨택스트가 제공하는 기능(변경 감지 등)을 사용하지 못한다.

📢준영속 상태로 만드는 방법

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

✍ em.detach(entity)

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            // 150L의 member는 영속상태가 된다.
            Member member = em.find(Member.class, 150L);
            // 값이 변경되어 Dirty Checking 이 일어난다.
            member.setName("AAAAA");

            // member는 영속 상태에서 준영속 상태로 전환된다.
            // JPA에서 더이상 관리하지 않기 때문에 트랜잭션 commit 시에도 아무 일도 일어나지 않는다.
            em.detach(member);

            System.out.println("=== BEFORE commit ===");
            // commit 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 날아간다.
            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다
        emf.close();
    }
}

  • find에 의해 select query가 나간 뒤, 영속성 컨택스트 속 1차 캐시에 엔티티가 저장된다.
  • em.detach(member) 로 인해 준영속 상태가 된 member객체는 setName("AAAAA")로 값을 변경했지만 update query는 나가지 않는다. (JPA가 더 이상 관리하지 않기 때문)
    👉 member와 관련된 모든 내용이 영속성 컨택스트에서 빠지게 된다.

✍ em.clear()

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            // 150L의 member는 영속상태가 된다.
            Member member = em.find(Member.class, 150L);
            // 값이 변경되어 Dirty Checking 이 일어난다.
            member.setName("AAAAA");

            // EntityManager 내의 영속성 컨택스트 전체가 지워진다.
            em.clear();
            System.out.println("=== AFTER em.clear() ===");

            // 150L의 member를 다시 조회한다.
            // 영속성 컨택스트에 member2가 없기 때문에 1차 캐시에 저장된다.
            Member member2 = em.find(Member.class, 150L);

            System.out.println("=== BEFORE commit ===");
            // commit 시점에 쓰기 지연 SQL 저장소에 쌓인 SQL문들이 날아간다.
            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다
        emf.close();
    }
}

✍em.close()

package hellojpa;

import javax.persistence.*;
import java.util.List;

public class JpaMain {
    public static void main(String[] args) {
        // <persistence-unit name="hello"> 의 name 속성을 createEntityManagerFactory 의 파라미터로 전달한다.
        // EntityManagerFactory는 어플리케이션 로딩 시점에 오직 1개만 만들어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager는 쓰레드간 공유해선 안된다. 사용하고 버릴 것.
        EntityManager em = emf.createEntityManager();

        // JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            // 150L의 member는 영속상태가 된다.
            Member member = em.find(Member.class, 150L);
            // 값이 변경되어 Dirty Checking 이 일어난다.
            member.setName("AAAAA");

            // EntityManager 내의 영속성 컨택스트 전체가 지워진다.
            em.close();
            System.out.println("=== AFTER em.close() ===");

            // 150L의 member를 다시 조회해도 영속성 컨택스트가 닫혀서 저장할 곳이 없다.
            Member member2 = em.find(Member.class, 150L);

            System.out.println("=== BEFORE commit ===");
            tx.commit();
        } catch (Exception e) {
            // 비정상 종료시 rollback
            tx.rollback();
        } finally {
            // EntityManager는 쓰레드간 공유해선 안되므로 사용을 다하면 꼭 닫아줘야 한다.
            em.close();
        }

        // 어플리케이션이 끝나면 EntityManagerFactory를 닫는다
        emf.close();
    }
}

em.close() 시점 이후 try 구문 내 코드가 실행되지 않는다.

0개의 댓글