JPA(Java Persistence API)

[JPA] Part 4. 영속성 컨텍스트

cwchoiit 2023. 10. 17. 15:20
728x90
반응형
SMALL
728x90
반응형
SMALL

영속성 컨텍스트는 JPA에서 가장 중요한 개념 중 하나이다. 이 PersistenceContext를 이해해야만 JPA를 이해할 수 있다고 봐도 무방하다. 

 

공부하는데 도움을 받은 김영한 강사님의 "자바 ORM 표준 JPA 프로그래밍" 추천합니다.

출처: https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

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

초급자를 위해 준비한 [웹 개발, 백엔드] 강의입니다. JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자

www.inflearn.com

 

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

우선 영속성 컨텍스트를 알기 전 엔티티 매니저 팩토리와 엔티티 매니저를 그림으로 이해해보자. 

애플리케이션이 실행되면 딱 한번 엔티티 매니저 팩토리가 만들어 지는데, 이 팩토리로부터 엔티티 매니저가 생성된다. 언제? 고객의 요청이 들어오면. 

고객의 요청 하나에 하나의 엔티티 매니저가 생성된다고 보면 되고 그 엔티티 매니저는 커넥션풀에서 커넥션을 사용한다. 그렇기에 엔티티 매니저가 할 일을 다했으면 반드시 엔티티 매니저를 닫아줘야 한다는 것이다. 사용하지 않는데 커넥션을 계속 잡고 있으면 이후에 들어오는 고객의 요청은 응답할 수 없을테니까.

 

 

영속성 컨텍스트

그럼 영속성 컨텍스트는 무엇이며 어디에 있을까? 영속성 컨텍스트는 엔티티를 영구 저장하는 환경 또는 컨텍스트인데 엔티티 매니저와 1:1 매핑이 된다. 즉, 엔티티 매니저 하나가 생성되면 그 엔티티 매니저가 가지는 영속성 컨텍스트 하나가 생성된다.

 

영속성 컨텍스트로부터 엔티티는 생명 주기를 가지는데, 4단계로 구분된다.

- 비영속 (new/transient): 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

- 영속 (managed): 영속성 컨텍스트에 관리되는 상태

- 준영속 (detached): 영속성 컨텍스트에 저장되었다가 분리된 상태

- 삭제 (removed): 삭제된 상태 

 

 

말만해서는 이해가 안될것 같으니 코드로 생각해보자.

비영속

// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setName("member1");

객체를 생성했지만 영속성 컨텍스트에는 전혀 관계가 없는 상태이다. 영속성 컨텍스트에 담는 코드는 없기 때문에.

위에서 영속성 컨텍스트는 곧 엔티티 매니저와 1:1 매핑이 된다고 했는데 엔티티 매니저를 영속성 컨텍스트라고 생각해보자.

그럼 아래와 같은 이미지로 볼 수 있는 것이다.

 

영속

// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setName("member1");

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

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

영속은 이제 엔티티 매니저에 담는것이라고 보면 된다. 어디서 많이 본 메서드인 persist()가 보인다. 이 메소드를 호출해서 멤버를 담는게 바로 아래와 같은 그림이 되는것이다.

⭐️ 그러니까 결국 persist() 메서드는 데이터베이스에 저장하는게 아니다. 영속성 컨텍스트에 영속시키는 것이다. 이 코드만으로 데이터베이스에 절대 절대 저장되지 않는다.

그럼 이 전 파트에서는 persist() 메서드를 호출하고 CREATE 부분을 끝냈는데 어디서 데이터베이스에 저장되는 걸까?

바로 트랜잭션의 커밋이다. 트랜잭션 커밋을 수행하면 쓰기 지연 SQL문이 실행되어 데이터베이스에 실제로 데이터가 저장된다.

 

못 믿겠다면 실제 코드를 수행해보자.

package org.example;

import org.example.entity.Member;

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

public class Main {
    public static void main(String[] args) {
        // ! EntityManagerFactory 이 녀석은 서비스가 띄워지면 딱 한개만 생성되어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // ! EntityManager 이 녀석은 어떤 디비에 작업을 할 때마다 하나씩 만들어지고 작업이 끝나면 버려져야 한다.
        EntityManager em = emf.createEntityManager();
        // ! 모든 데이터베이스에 대한 변경 작업은 트랜잭션 안에서 일어나야 한다. (조회는 꼭 트랜잭션 안에서가 아니더라도 상관없다)
        // ! 하나의 트랜잭션에서 원하는 작업을 끝내고 그 트랜잭션안에서 커밋을 해줘야 변경이 적용된다.
        EntityTransaction tx = em.getTransaction();

        // ! Transaction 시작
        tx.begin();

        try {
            /* CREATE */
            Member member = new Member();
            member.setName("helloD");
            em.persist(member);

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

위 코드처럼 persist() 메서드 호출 후 트랜잭션 커밋을 주석처리 해보자. 실행한 후 실제 데이터베이스에 helloD 라는 이름을 가지는 멤버가 있는지 보자.

없다. 즉, persist() 메서드는 영속 컨텍스트에 저장하는 메서드이지 데이터베이스에 저장하는 메서드가 아니다. 반드시 이 개념을 이해해야 한다. 그리고 또한, 데이터베이스에서 조회할 때도 영속 컨텍스트에 조회한 데이터도 영속된다. 

// 영속 컨텍스트에 영속시킴
Member member = em.find(Member.class, 1L);

이처럼 데이터베이스를 통해 조회를 할 때, 해당 데이터를 조회한 후 영속 컨텍스트에 영속시킨다. 그니까 영속되는 경우는 크게 두가지인거지. 데이터베이스로부터 조회한 경우와 persist() 메서드를 호출해서 영속시킨 경우.

 

준영속, 삭제

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

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

준영속은 꽤 중요한데 영속된 객체를 비영속으로 변경하는 경우를 말한다.

예를 들어, 데이터베이스를 통해 조회한 객체는 영속된다고 했는데 그 영속된 데이터를 영속 컨텍스트에서 관리할 필요가 없는 경우 준영속으로 변경할 수 있다. 

// 영속 컨텍스트에 영속시킴
Member member = em.find(Member.class, 1L);

// 영속 컨텍스트에서 제외시킴
em.detach(member);

위 코드를 보면 member는 detach() 메서드가 호출된 후 더이상 영속 컨텍스트가 관리하지 않는다.

이를 눈으로 확인해보기 위해선, 꺼낸 데이터를 변경한 후에 커밋하기 전 detach()를 호출해보면 알 수 있다.

// 영속 컨텍스트에 영속시킴
Member member = em.find(Member.class, 1L);

member.setName("BBB");

// 영속 컨텍스트에서 제외시킴
em.detach(member);

// 아무일도 일어나지 않음
tx.commit();

커밋하기 전 영속 컨텍스트에서 제외시켰으므로, setName()을 호출해서 이름을 변경해도 데이터베이스에 적용되지 않는다.

 

준영속으로 변경하는 방법은 크게 세 가지가 있는데, 한 가지는 위처럼 detach() 메서드를 호출하거나 clear() 메서드를 호출하거나 아예 엔티티 매니저를 닫는것이다.

em.detach(member); // 특정 엔티티를 준영속으로 변경
em.clear(); // 영속 컨텍스트를 완전 초기화
em.close(); // 영속 컨텍스트를 종료

 

영속성 컨텍스트의 이점

- 1차 캐시

- 동일성(identity) 보장

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

- 변경 감지(Dirty Checking)

- 지연 로딩(Lazy Loading)

 

 

1차 캐시

1차 캐시란, 영속 컨텍스트에 객체가 영속이 되면 그 객체를 가져올 때 데이터베이스로부터 조회하는 것이 아니라 영속 컨텍스트로부터 가져오는 기술을 말한다. 물론, 이 1차 캐시가 큰 의미가 있진 않은데 그 이유는 위에서도 말했지만 영속 컨텍스트는 엔티티 매니저와 1:1로 매핑된다고 했다. 즉, 엔티티 매니저가 닫히는 순간 1차 캐시도 의미가 없어지기 때문에 비즈니스 로직이 정말 정말 복잡해서 하나의 트랜잭션에서 여러번 같은 객체가 사용되거나 호출될 때만 의미가 있지 그렇지 않고서는 큰 의미는 없다만 그래도 알고 있어야 한다. 

 

// 엔티티 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setName("member1");

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

이렇게 영속 시킨 후 영속 컨텍스트에 1차 캐시에 해당 엔티티(객체)가 담긴다. 그렇게 담긴 이후에 해당 객체를 다시 조회하는 코드를 마주치면 데이터베이스로부터 객체를 조회하지 않고 1차 캐시에서 꺼내올 수 있다. 

// 엔티티 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setName("member1");

// 엔티티 영속 -> 1차 캐시에 저장됨 
em.persist(member);

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

 

그런데 이제 만약 위 사진처럼 "member1" 이라는 객체만 1차 캐시에 담겨있는 상태에서 "member2"를 조회하고자 한다면, 1차 캐시에 없기 때문에 데이터베이스에서 조회한다. 

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

 

동일성 보장

영속 컨텍스트에서 꺼내온 객체는 동일함을 보장한다는 뜻이다.

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

System.out.println(findMember1 == findMember2) // true

 

 

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

이는 트랜잭션 안에서 수행되는 모든 데이터베이스에 대한 변경 작업을 수행하기 위한 SQL문을 트랜잭션의 커밋이 호출되기 전까지 데이터베이스에 보내지 않고 모아둔 상태에서 커밋을 호출하는 순간 모아둔 SQL문을 보낸다는 뜻이다.

 

즉, 만약 내가 새로운 멤버를 두 명 만든다고 가정해보자.

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

// 트랜잭션 시작
transaction.begin();

em.persist(memberA);
em.persist(memberB);
// 여기까지가 영속 컨텍스트에 memberA, memberB를 영속시키고 INSERT SQL문을 쓰기 지연 보관소에 저장한다.
// 허나, 데이터베이스에 보내지는 않는 상태이다.

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

 

그래서 이 코드를 그림으로 살펴보면 다음과 같다.

 

persist() 메서드는 영속 컨텍스트에 객체를 저장. 그리고 저장과 동시에 INSERT SQL문을 쓰기 지연 저장소에 저장한다.

 

이 때까지는 데이터베이스에 해당 객체를 저장하지 않은 상태. 즉, SQL 쿼리도 날라가지 않은 상태.

그리고 트랜잭션 커밋이 호출되는 순간 쓰기 지연 SQL 저장소에 모아둔 쿼리문이 실행된다. 

이 부분에서 persistence.xml 파일에 속성으로 있던 batch_size가 이 내용과 관련이 있다. 한번에 보낼 수 있는 사이즈를 지정하는 속성값.

 

 

변경 감지(Dirty Checking)

업데이트를 할 때 업데이트한 후 뭔가 업데이트를 실행하는 코드가 있어야 할 것 같은데 없었다. 그럼 persist() 메서드라도 호출해야 할까? 그것도 아니다 왜냐하면 이제 우리는 안다. persist() 메서드는 영속 컨텍스트에 영속시키는 것일 뿐인걸. 즉, 이미 영속된 객체를 또 영속시킨다는 코드를 작성할 필요가 없단 말이다. 그럼 어떻게 업데이트한 걸 알까? 

 

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

transaction.begin();

Member memberA = em.find(Member.class, "memberA");
memberA.setName("Changed");

transaction.commit()

자, 이런 코드가 있고 이게 정말 업데이트의 끝이다. 근데 바꾸고 아무것도 안하고 커밋만 했단 말이지? 어떻게 변경 사항을 적용할까? 

사실 영속 컨텍스트에는 스냅샷 데이터가 존재한다.

 

그래서 객체와 스냅샷을 비교해서 값이 다르면 SQL 문을 수행하게 되는것이다. 

위 그림에서 flush()가 들어오면 엔티티와 스냅샷이 존재하는데 그 값을 비교한다. 비교 후 값이 다르다면 다른 값을 적용하기 위해 UPDATE SQL문을 생성해서 수행하게 되는것이다.

 

플러시란?

플러시란 변경, 수정, 삭제와 같은 데이터베이스의 데이터와 영속 컨텍스트의 데이터가 달라진 경우 데이터베이스에 달라지는 값을 맞춰주는 작업이다. 즉, 쉽게 말해 쓰기 지연 SQL 저장소의 쿼리가 데이터베이스에 날라가는 것이라고 보면 되는데 이 플러시를 호출하는 방법은 크게 세 가지가 있다. 

 

- em.flush(): 직접 호출

- 트랜잭션 커밋: 자동 호출

- JPQL 쿼리 실행: 자동 호출

 

플러시를 한다고해서 영속 컨텍스트에 데이터를 지운다거나 1차 캐시에 데이터를 지우는게 아니고 그냥 쓰기 지연 SQL 저장소의 쿼리가 날라가는 것 뿐이다.

 

 

728x90
반응형
LIST