JPA

[JPA] 영속성 컨텍스트

수수한개발자 2022. 5. 11.
728x90

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

영속성 컨텍스트에 대해 알아보기 전에 엔티티 매니저 팩토리와 엔티티 매니저에 대해서 알아보겠습니다.

엔티티 매니저 팩토리는 생성되는 시점에 DB 커넥션 풀을 생성해 둔 후, 요청이 들어올 때마다 엔티티 매니저를 생성합니다. 엔티티 매니저는 DB 연결이 필요한 시점( 보통 트랜잭션이 시작되는 경우)에 connection을 얻습니다.

일반 적인 웹 애플리케이션에서의 엔티티 매니저 팩토리와 엔티티 매니저 구동방식

 

엔티티 매니저 팩토리가 엔티티 매니저를 사용하는 이유 

엔티티 매니저 팩토리 엔티티 매니저
생성 시점에 DB 커넥션 풀을 생성하는 등
생성 비용이 크기 때문

DB 를 하나만 사용하는 어플리케이션이라면
일반적으로 하나만 만든다.
엔티티 매니저 팩토리를 이용해서 생성한다. 
생성 비용이 거의 들지 않으므로
필요에 따라 생성하여 사용할 수 있다.
스레드 세이프하기 때문에 여러 스레드가 동시 접근해도 안전하다. 여러 스레드가 동시에 접근하면 동시성 문제가 발생하기 때문에 공유해서는 안된다.

EntityManagerFactory

JPA는 EntityManagerFactory를 만들어야 한다.

application loding 시점에 DB당 하나만 생성되어야한다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

(생성)

emf.close();

 

DB가 종료되는 시점에 EntityManagetFactory를 닫는다.

그래야 내부적으로 Connection pooling에 대한 Resource가 Release 된다.

 

EntityManager

실제 Transaction 단위를 수행할 때마다 생성한다.

즉, 고객의 요청이 올 때마다 사용했다가 닫는다.

thread 간에 공유하면 안된다.(사용하고 버려야함)

EntityManager em = emf.createEntityManager();//database 커넥션 하나 받았다 생각하면됨.
em.close();

Transaction 수행 후에는 반드시 EntityManager를 닫는다.

그래야 내부적으로 DB Connection을 반환한다.

 

EntityTransaction

Data를 '변경' 하는 모든 작업은 반든시 Transaction 안에서 이루어져야 한다.

단순한 조회는 상관 없다.

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

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

 

tx.begin(); : Transaction 시작

tx.commit(); : Transaction 수행

tx.rollback(); : 작업에 문제가 생겼을 시 

 

영속성 컨텍스트(Persistence Context)

JPA를 이해하는데 있어서 가장 중요한 용어이다.

논리적인 개념으로 눈에 보이지 않는다.

"Entity"를 영구 저장하는 환경이라는뜻

EntityManager.persist(entity) 의 동작 설명

실제로는 DB에 저장하는 것이 아니라 영속성 컨텍스트를 통해서 Entity를 영속화한다는 뜻이다.

정확히 말하면 persist() 시점에는 Entity를 영속성 컨텍스트에 저장하는 것이다.

EntityManager를 통해서 영속성 컨텍스트에 접근한다.

EntityManager가 생성되면 1:1로 영속성 컨텍스트가 생성된다.

 

Entity의 생명주기 (Entity LifeCycle)

비영속(new/transient)

영속성 컨텍스트와 전혀 관계가 없는 상태

객체를 생성만한 상태

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

영속(managed)

영속성 컨텍스트에 저장된 상태

Entity가 영속성 컨텍스트에 의해 관리되는 상태

// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
// 객체를 저장한 상태 (영속)
entityManager.persist(member);

EntityManager.persist(entity)

-영속 상태가 된다고 바로 DB에 쿼리가 날라가지 않는다.(즉, DB저장 X)

transaction.commit()

-트랜잭션의 commit 시점에 영속성 컨텍스트에 있는 정보들이 DB에 쿼리로 날라간다.

 

준영속(detached)

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

영속성 컨텍스트에서 지운 상태

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

삭제(removed)

실제 DB 삭제를 요청한 상태

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

영속성 컨텍스트의 이점

Application과 DB 사이의 중간 계층의 영속성 컨텍스트가 존재하는 이유

버퍼링, 캐싱 등의 이점

 

1. 1차캐시

영속성 컨텍스트에는 1차 캐시가 존재한다.

1차 캐시를 영속성 컨텍스트라고 이해해도 된다.

Map<Key,value>로 1차캐시에 저장된다.

key : @Id로 선언한 필드 값(DB pk)

value : 해당 Entity 자체

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
/* 영속 상태 (Persistence Context 에 의해 Entity 가 관리되는 상태) */
// DB 저장 X, 1차 캐시에 저장됨
entityManager.persist(member); 
// 1차 캐시에서 조회
Member findMember = entityManager.find(Member.class, "Member1");

1차 캐시에 Entity가 있을 때의 이점

조회

EntityManager.find()를 하면 DB보다 먼저, 1차 캐시를 조회한다.1차캐시에 해당 Entity가 존재하면 바로 반환한다.

1차 캐시에 조회하고자 하는 Entity가 없다면?
1) DB에서 조회한다.

2) 해당 Entity를 DB에서 꺼내와서 1차 캐시에 저장한다.

3) 이후에 다시 해당 Entity를 조회하면 1차 캐시에 있는 Entity를 반환한다.

그러나 사실 1차 캐시는 큰 성능 이점을 가지고 있지 않다.

EntityManager는 Transaction 단위로 만들고 해당 DB Transaction이 끝날 때 (사용자의 요청에 대한 비즈니스가 끝날 때) 같이 종료된다.

즉, 1차 캐시도 모두 날라가기 때문에 굉장히 짧은 찰나의 순간에만 이득이 있다.(DB의 한 Transaction안에서만 효과가 있다.)

비즈니스로직이 굉장히 복잡한 경우에는 효과가 있다.

 

2. 동일성 보장(Identity) 

Member a = entityManager.find(Member.class, "member1");
Member b = entityManager.find(Member.class, "member1");
System.out.println(a == b); // 동일성 비교 true

• 영속 Entity의 동일성 (==비교)을 보장한다.

-즉 == 비교가 true 임을 보장한다.

-member1에 해당하는 Entity를 2번 조회하면 1차 캐시에 의해 같은 Reference를 인식된다.

-하나의 Transaction 안에서 같은 Entity 비교시 true.

• 1차 캐시로 반복 가능한 읽기등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.

 

영속 엔티티와,비영속 혹은 준영속 엔티티의 비교

Member memberA = em.find(Member.class, 1L);
em.clear();//있으면 false, 없으면 true
Member memberB = em.find(Member.class, 1L);
System.out.println(memberA == memberB);//동일성 비교 false

동일성을 보장하지 않는다.

Member memberA = em.find(Member.class, 1L);
Member memberB = em.find(Member.class, 1L);

em.clear();

System.out.println(memberA == memberB);

만약 위의 코드를 실행하면 어떨까?

답은 true이다.

첫번째 find를 통해 DB에서 Member를 조회한다. 그리고 조회한 Member를 1차캐시에 등록한다.

두번째 find는 1차 캐시에서 값을 확인했는데 있다. 그러면 DB를 조회할 이유조차 없기때문에 

1차캐시에 존재하는 값을 가져온다.

그 이후 영속성 컨텍스트를 비우더라도, 이미 memberB는 1차 캐시에 존재하는 member의 참조주소를 가지고 있기에 true가 출력된다.

 

3. 엔티티 등록시 쓰기 지연

EntityManager entityManager = emf.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
// EntityManager는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin(); // Transaction 시작

entityManager.persist(memberA);
entityManager.persist(memberB);
// 이때까지 INSERT SQL을 DB에 보내지 않는다.

// 커밋하는 순간 DB에 INSERT SQL을 보낸다.
transaction.commit(); // Transaction 커밋

entityManager.persist()

  JPA가 insertSQL을 계속 쌓고 있는 상태

• transaction.commit()

  커밋하는 시점에 insertSQL을 동시에 DB에 보낸다.

  동시에 쿼리들을 확 보낸다. (쿼리를 보내는 방식은 동시 or 하나씩 옵션에 따라)

 entityManager.persist(memberA)

  1) memberA가 1차캐시에 저장된다.

  2) 1)과 동시에 JPA가 Entity를 분석하여 insert Query를 만든다.

  3) insert Query를 쓰기 지연 SQL 저장소 라는 곳에 쌓는다.

  4) DB에 바로 넣지 않고 기다린다.

  5) memberB도 동일

• transaction.commit()

  6) 쓰기 지연 SQL 저장소에 쌓여있는 Query들을 DB로 날린다 (flush())

     -flush()는 1차캐시를 지우지는 않는다. 쿼리드을 DB에 날려서 DB와 싱크를 맞추는 역할을 한다.

  7) flush() 후에 실제 DB Transcation이 커밋된다 (commit())

버퍼링 기능 이라고 할 수 있다.

  DB에 쿼리를 날리는 것에 대한 옵션 설정을 통한 최적화 기능

     JPA의 이런 기능을 잘 활용하면 성능 개선을 쉽게 할 수 있다.

  jdbc 일괄 처리 옵션 (jdbc batch option)
    persistence.xml에 아래와 같은 옵션을 줄 수 있다.
    commit 직전까지 insert query를 해당 사이즈 만큼 모아서 한번에 처리한다.
<property name="hibernate.jdbc.batch_size" value=10/>

위 옵션을 통해 한꺼번에 insert/update를 실행할 크기 (쓰기 지연 저장소에 저장할 SQL의 최대 개수)를 지정 할 수 있다.

 

4. 변경 감지(Dirty Checking)

Dirty Checking의 자세한 이야기 https://jojoldu.tistory.com/415 https://gmlwjd9405.github.io/2019/08/06/persistence-context.html

EntityTransaction transaction = em.getTransaction();
transaction.begin()://트랜잭션 시작
//엔티티 조회 -> 영속화
Member memberA = em.find(Member.class, 1L);
memberA.setUsername("신동훈");
memberA.setAge(22);
transaction.commit();//트랜잭션 커밋

위의 코드에서 update가 된다 이상하지 않은가?

일반적으로 생각하기에는 코드가 다음과 같아야 할 것이다.

EntityTransaction transaction = em.getTransaction();
transaction.begin();//트랜잭션 시작
//엔티티 조회 -> 영속화
Member memberA = em.find(Member.class, 1L);
memberA.setUsername("신동훈");
memberA.setAge(22);

em.update(memberA);//추가

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

update 메소드를 만들어 바뀐값을 넣어줘야 하는데 그렇지 않다!

JPA는 1차 캐시에 들어있는 엔티티가 변경된다면, 그 변경된 내용을 감지하여, 트랜잭션 커밋 시점에 변경된 내용을 DB에 반영한다.

 

변경 감지는 어떻게 가능할까?

1차 캐시에는 사실 @Id와 Entity 말고도 하나의 컬럼이 더 존재한다.(여기서는 이해하기 쉽게 컬럼이라고 표현했다.)

 

바로 " 스냅샷 " 이라는 컬럼이다.

엔티티가 1차 캐시에 저장될 때, 저장되는 시점의 상태를 스냅샷으로 만들어 1차 캐시에 보관한다.

 

트랜잭션이 커밋되는 시점에 엔티티와 스냅샷을 비교하는데, 이때 만약 엔티티가 변경되었다면 당연히 스냅샷과 차이가 있을 것이고, JPA는 이름 감지하여 DB에 반영한다.

 

중요한 것은 "커밋되는 시점"에 "엔티티와 스냅샷을 비교" 한다는 것이다.

 

그럼 커밋되기 전에 엔티티를 수정하여 사용하였다가, 커밋되는 시점에 다시 원상복구 시키면 어떻게 될까?

 

결론은 updqte 쿼리가 실행되지 않습니다.

 

코드로 살펴 보겠습니다.

 

EntityTransaction transaction = em.getTransaction();

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

//엔티티 조회 -> 영속화
Member memberA = em.find(Member.class, 1L);//이 시점에 username="김지수", age=27

memberA.setUsername("김형수");
System.out.println(memberA.getUsername());//바보 출력

memberA.setUsername("김지수");
System.out.println(memberA.getUsername());//신동훈 출력

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

위 코드를 실행하면 updqte 쿼리는 나가지 않습니다.

즉 하나의 트랜잭션 내에서는 엔티티를 어떻게 수정하여 사용하건 상관없이, 트랜잭션이 커밋되는 시점의 엔티티의 상태와 스냅샷을 비교하여 반영되므로, 이를 꼭 기억해야 한다.

 

5. 엔티티 삭제

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

em.remove(memberA); // 엔티티 삭제

Transaction의 commit 시점에 DELETE Query가 나간다.

 

Reference

https://www.inflearn.com/course/ORM-JPA-Basic#

728x90

'JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (0) 2022.06.14
[JPA] 플러시란?  (0) 2022.06.13
[JPA] 단방향 연관관계  (0) 2022.05.09
[JPA]기본 키 매핑  (0) 2022.05.08
[JPA] 기본 @Anotation 정리  (0) 2022.05.08

댓글