0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] JPA 영속성 컨텍스트(+ flush, 준영속 상태)

힘들면힘을내는쿼카 2023. 2. 9. 23:21
728x90
반응형

[JPA] JPA 영속성 컨텍스트(+ flush, 준영속 상태)

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

2023.02.09 - [0 + 프로그래밍/0 + JPA] - JPA 구동 방식과 간단 실습(CRUD)

 

JPA 구동 방식과 간단 실습(CRUD)

[JPA] JPA 구동 방식과 간단 실습(CRUD) 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고 강의 내용을 다시

howisitgo1ng.tistory.com

 

JPA를 학습하면 반드시 들어보신 말이 바로 영속성 컨텍스트 입니다.
그만큼 중요하기 때문인데요.
영속성 컨텍스트가 무엇인지 한 번 제대로 알아봅시다.

 

영속성 컨텍스트

영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 의미 합니다.
EntityManager.persist(entity);를 사용하면 INSERT 쿼리가 발생했다고 생각할 수 있습니다.
persist(entity)를 했다고 INSERT SQL이 DB에 발생한것이 아닙니다. 아닙니다. 아닙니다.


persist(entity)entity를 영속성 컨텍스트에 저장한다는 의미 입니다.

 

영속성 컨텍스트는 논리적인 개념입니다.(🙈 눈에 보이지 않아요..ㅎㅎ)
JPAEntityManager를 통해서 영속성 컨텍스트에 접근 합니다.
EntityManager를 생성하면 그안에 PersistenceContext가 생성 됩니다.

 

영속성 컨텍스트를 이해하기 위해서는 먼저 엔티티의 생명주기에 대해서 알아야 합니다.

엔티티 생명주기(Entity Life Cycle)

비영속(new/transient)

영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 입니다.

// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(1L);
member.setUsername("이용진");

 

영속(menaged)

영속성 컨텍스트에 관리되는 상태 입니다.

// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(1L);
member.setUsername("이용진");

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

//객체를 저장한 상태(영속)
em.persist(member); // INSERT 쿼리 생성 X

 

em.persist(member); 에서 진짜로 INSERT 쿼리가 발생하지 않는지 확인해 봅시다.

 

실험

public class JpaMainV1 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 비영속
        Member member = new Member();
        member.setId(1L);
        member.setName("이용진");

        System.out.println("persist(member) 전");
        em.persist(member); // 영속
        System.out.println("persist(member) 후");

        tx.commit(); // 커밋
    }
}

 

결과

persist(member) 전
persist(member) 후
Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

😯 오잉?! 진짜로 persist(member) 전, persist(member) 후 사이에 쿼리가 생기지 않았습니다.!!!

 

결론
결론부터 말씀드리면,
영속성 컨텍스트에 저장 됐다고 해서 DB에 쿼리가 날라가는게 아닙니다.
그러면 언제 쿼리가 날라가는거지?(쓰기 지연 SQL 저장소에 저장되어 있음ㅎㅎ)
DB에는 커밋 이후에 저장됩니다…!!!!!!!!!!!!!!!!!!!!!!!!

 

준영속(detached)

영속성 컨텍스트에 저장되었다가 분리된 상태 입니다.

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

 

삭제(removed)

삭제된 상태 입니다.

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

 

🤔 그렇다면 왜 JPA영속성 컨텍스트로 엔티티를 관리하는 걸까요?

영속성 컨텍스트 장점

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

 

1차 캐시

// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(1L);
member.setUsername("이용진");

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

//객체를 저장한 상태(영속)
em.persist(member); // INSERT 쿼리 생성 X

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

 

member를 영속성 상태로 만들면 다음과 같은 현상이 발생합니다.

 

 

그리고 조회를 하면 다음과 같은 현상이 발생합니다.

 

 

만약 1차 캐시에 엔티티가 존재하지 않으면 DB에서 조회하게 됩니다.

 

 

해당 과정을 직접 코드로 확인해 볼까요?

 

실험

public class JpaMainV1 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 비영속
        Member member = new Member();
        member.setId(1L);
        member.setName("이용진");

        System.out.println("persist(member) 전");
        em.persist(member); // 영속
        System.out.println("persist(member) 후");

        // member가 영속성 컨텍스트로 관리 되기 때문에 SELECT 쿼리 생성 X
        Member findMember = em.find(Member.class, 1L);
        System.out.println("memberName="+findMember.getName());

        tx.commit(); // 커밋
    }
}

 

결과

persist(member) 전
persist(member) 후
memberName=이용진
Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

1차 캐시에 member가 존재하기 때문에 SELECT가 생성되지 않는것을 확인 할수 있습니다.

 

참고
1차 캐시는 1개의 트랜잭션 안에서만 이루어지는 과정이기 때문에 엄청나게 큰 이점은 없습니다...ㅎㅎ

 

동일성(identity) 보장

1차 캐시로 반복 가능한 읽기(REPEATABLE READ)등급의 트랜잭션 격리 수준을
DB가 아닌 애플리케이션 차원에서 제공
합니다.(1차 캐시가 있기 때문에 가능!)

(필자는 동일성 보장의 효과가 어떤 것인지 파악을 하지 못했습니다....)

 

public class JpaMainV1 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 비영속
        Member member = new Member();
        member.setId(1L);
        member.setName("이용진");

        em.persist(member); // 영속

        // member가 영속성 컨텍스트로 관리 되기 때문에 SELECT 쿼리 생성 X
        Member findMember1 = em.find(Member.class, 1L);
        Member findMember2 = em.find(Member.class, 1L);

        // 동일성 비교 true
        System.out.println("is true? "+(findMember1 == findMember2));

        tx.commit(); // 커밋
    }
}

 

결과
동일성 보장은 1차 캐쉬가 있기 때문에 가능합니다.

is true? true

 

트랜잭션을 지원하는 쓰기 지연(transaction write-behind)

transaction write-behindpersist(entity) 일때 SQL을 데이터 베이스에 보내지 않는 것을 의미합니다.
(쓰기 지연 SQL 저장소에 저장 ㅎㅎ)

앞에서 영속 상태를 학습할 때 확인해본 내용 입니다.
(커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.!!)

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다. 
transaction.begin(); // [트랜잭션] 시작 
em.persist(member1);
em.persist(member2);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않음.

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

 

쓰기 지연 동작 방식은 다음과 같습니다.
member1 영속

 

 

member2 영속
쓰기 지연 SQL 저장소에 SQL이 쌓이는 것을 확인 할수 있습니다.

 

commit()을 하게되면 다음과 같이 동작합니다.


flush가 발생한뒤 DB에서 commit이 발생하게 됩니다.

 

실험
이러한 과정을 코드로 작성해보겠습니다.^^

public class JpaMainV1 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        // 비영속
        Member member1 = new Member(1L, "유재석");
        Member member2 = new Member(2L, "박명수");

        // 영속성 컨텐스트에 저장, 쓰기 지연 SQL 저장소에 SQL 저장
        em.persist(member1);
        em.persist(member2);

        System.out.println("persist 이후에 쿼리 나감!");

        tx.commit(); // 커밋(쿼리 발생)
    }
}

 

결과

persist 이후에 쿼리 나감!
Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)

 

JPA는 이렇게 쓰기 지연을 하는 이유가 뭘까? 🤔


만약 persist(entity)를 하는 동시에 쿼리를 DB에 날린다면, 개발자가 최적화 할수 있는 상황이 사라지게 됩니다.


예를 들어 현재 em.persist(member1); em.persist(member2); 처럼 쓰기 지연 SQL 저장소에 쿼리가 쌓이게 되면 이 쿼리를 한번에 DB에 보낼수 있습니다.
이것을 우리는 JDBC Batch라고 합니다.
버퍼링 같은 기능이라고 생각하면 쉽습니다. (말 그대로 모았다가 한방에 보냅니다.)


hibernate의 옵션을 설정해주면 이와같은 기능을 사용할 수 있습니다.
<property name=“hibernate.jdbc.batch_size” value=“10”/>식으로 설정 하면 됩니다.

 

변경 감지(dirty checking)

바로 코드로 보여드리겠습니다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작 

// 영속 엔티티 조회 
Member member1 = em.find(Member.class, 1L);

// 영속 엔티티 데이터 수정(변경 감지!)
member1.setUsername("유재석");

//em.update(member) 이런 코드가 있어야 하지 않을까? 

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

변경 감지 동작 방식은 다음과 같습니다.

 

 


스냅샷이라는 것을 통해 변경 감지를 하게되는데,
여기에서 스냅샷이란 DB를 통해 엔티티를 처음 읽어오거나 영속화한 시점의(객체가 특정 시점에 구체적으로 어떤 상태 인지) 엔티티를 저장하는 것 입니다.

 

JPA는 엔티티의 상태가 변경되면 스냅샷을 통해 저장해둔 엔티티와 비교를 하게 됩니다.
이때 스냅샷의 상태와 다른 엔티티가 있다면,
해당 엔티티의 상태를 변경해 버리는 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 쌓게 됩니다.
그 이후에 DB에 쿼리를 날리게 되는 것(flush) 입니다.

 

실험
이러한 과정을 코드를 통해 확인해 봅시다.

public class JpaMainV1 {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin(); // 트랜잭션 시작

        // 비영속
        Member member1 = new Member(1L, "유재석");

        // 영속성 컨텐스트에 저장, 쓰기 지연 SQL 저장소에 SQL 저장
        em.persist(member1);

        // 1차 캐시에서 조회
        Member findMember = em.find(Member.class, 1L);
        findMember.setName("박명수");

        tx.commit(); // 커밋(쿼리 발생)
    }
}

 

결과

Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* update
        com.study.purejpa.entity.Member */ update
            Member 
        set
            name=? 
        where
            id=?

 

UPDATE 쿼리가 생성된것을 확인 할 수 있습니다.
또한 1차 캐시에서 조회했기 때문에 SELECT 쿼리가 발생되지 않음을 확인 할 수 있습니다.

 

지연 로딩(lazy loading)

https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/

 

JPA Hibernate 프록시 제대로 알고 쓰기

JPA…

tecoble.techcourse.co.kr

 

지연 로딩은 JOIN과 같은 SQL에서 주로 활용됩니다.
JPAJOIN 되는 엔티티를 함께 조회 하는지 안 하는지에 대한 옵션을 제공합니다.
무슨 말이지? 당연히 조회해야하는거 아냐? 라고 생각할 수 있는데 JPA는 이러한 것을 옵션으로 제공합니다.

 

예를 들어, 반드시 Member를 조회할 때 Team도 함께 조회해야할까요?
아무리 연관관계가 걸려있다고 해도 Team과 관련없는 로직을 사용한다면 손해 입니다..
이러한 문제를 JPA지연 로딩을 사용해서 해결합니다.(프록시로 엔티티를 조회하는 방법)

 

지연 로딩(lazy loading)은 Member를 조회할 때 Team을 조회하지 않습니다.
어떻게 조회하지 않나요? 바로 Team프록시로 조회하기 때문입니다.

 

그러다가 실제로 애플리케이션에서 Team을 사용할때 DB에서 조회하게 됩니다.

 

 

이를 코드로 확인해 봅시다.

 

실험
Member

@Entity
@Getter @Setter
public class Member {
    @Id // PK
    private Long id;
    private String name;
      // 지연 로딩 설정
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member() {
    }

    public Member(Long id, String name, Team team) {
        this.id = id;
        this.name = name;
        this.team = team;
    }
}

 

Team

@Entity
@Getter @Setter
public class Team {

    @Id
    private Long id;
    private String name;

    public Team() {
    }

    public Team(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

JpaMainV2

public class JpaMainV2 {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Team team = new Team(1L, "홍철없는 홍철팀");
            Member member1 = new Member(1L, "유재석", team);
            em.persist(team);
            em.persist(member1);

            em.flush(); // 영속성 컨텍스트 DB 반영
            em.clear(); // 영속성 컨텍스트 비움

            // 지연 로딩
            Member findMember = em.find(Member.class, 1L);
            System.out.println("findMember.getTeam()="+ findMember.getTeam().getClass());

            tx.commit(); // 커밋
        } catch (Exception e) {
            tx.rollback(); // 롤백
        } finally {
            em.close(); // DB Connection을 사용하여 작업함
        }
        emf.close(); // 애플리케이션이 종료되면 닫아야함
    }
}

 

결과

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
findMember.getTeam()=class com.study.purejpa.entity.Team$HibernateProxy$rxsaNGou

findMember.getTeam()=class com.study.purejpa.entity.Team$HibernateProxy$rxsaNGou를 보면 지연로딩 설정으로 인해 프록시 객체를 로딩한것을 알수 있습니다.

 

그렇다면 아까 언급한 실제로 애플리케이션에서 Team을 사용할때 DB에서 조회하게 되는지 확인해 봅시다.

 

JpaMainV2

public class JpaMainV2 {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("pureJpa");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Team team = new Team(1L, "홍철없는 홍철팀");
            Member member1 = new Member(1L, "유재석", team);
            em.persist(team);
            em.persist(member1);

            em.flush(); // 영속성 컨텍스트 DB 반영
            em.clear(); // 영속성 컨텍스트 비움

            // 지연 로딩
            Member findMember = em.find(Member.class, 1L);
            System.out.println("실제로 사용 전 findMember.getTeam()="+ findMember.getTeam().getClass());

            System.out.println("실제로 Team 엔티티를 사용하면?");
            findMember.getTeam().getName();

            tx.commit(); // 커밋
        } catch (Exception e) {
            tx.rollback(); // 롤백
        } finally {
            em.close(); // DB Connection을 사용하여 작업함
        }
        emf.close(); // 애플리케이션이 종료되면 닫아야함
    }
}

 

결과

실제로 Team 엔티티를 사용하면?
Hibernate: 
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

 

실제로 사용할때 SQL 쿼리가 나가는것을 확인 할수 있습니다...!!

플러시(flush)

지연 로딩(Lazy Loading)을 설명하기 위해서 직접 flush()를 호출 한 것을 볼수 있습니다.

왜 호출했을까요? 🤔
영속성 컨텍스트 내용을 DB에 반영하기 위해서 호출했습니다.
(추가로 em.clear()를 실행해야 em.find()를 호출할때 1차 캐시에서 조회하지 않기 때문입니다.
em.flush()만 호출하게 되면 커밋 전까지 1차 캐시에 영속성 컨텍스트로 남아있습니다.
)

 

플러시는 쓰기 지연 SQL 저장소의 쿼리(등록, 수정, 삭제)를 데이터베이스에 전송하는 것을 의미합니다.
다시 말해서 영속성 컨텍스트의 변경 내역을 DB와 동기화하는 것 입니다.

 

플러시 할 수 있는 방법은 무엇일까요?

  • 직접호출
    • em.flush()
  • 플러시 자동 호출
    • 트랜잭션 커밋
    • JPQL 쿼리 실행
      • JPQLSQL로 번역 후 실행되기 때문에 향후 장애 방지(DB에 데이터가 없는 경우)를 위해 flush를 호출합니다.

 

플러시 모드 옵션은 기본으로 em.setFlushMode(FlushModeType.AUTO)으로 설정되어 있습니다.
(개발하면서 실제로 변경할 일을 없다고 봐도 무방합니다.)

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

 

정리

  • 영속성 컨텍스트를 비우는것 아님
  • 영속성 컨텍스트의 변경내용을 DB에 동기화
  • 커밋 직전에만 동기화 하면 된다.(트랜잭션이라는 작업 단위가 중요!)

준영속 상태

영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 상태를 말합니다.
준영속 상태가 되면 영속성 컨텍스트가 제공하는 기능을 사용하지 못합니다.

 

준영속 상태 만드는 방법

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

 

준영속 상태는 주로 테스트 케이스를 작성할 때 사용 합니다.(em.clear(), em.close())

 

 

 

 

728x90
반응형