Java - SpringJPA

Spring JPA (3) - JPA 개념 잡기 : 영속성 컨텍스트, 플러시, 준영속 상태

TerianP 2022. 10. 1.
728x90

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

- 고객으로부터 요청이 올때마다 EntityManagerFactor 가 EntityManager 를 생성하고 생성된 EntityManager 이 DB 에 쿼리를 날려서 요청을 처리하게 된다.

 

2. 영속성 컨텍스트

- JPA 를 이해하는데 가장 중요한 용어로 "엔티티를 영구 저장하는 환경" 이라는 뜻이다.

- 영속성 컨텍스트는 논리적인 개념으로 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하게 된다.

- 영속성 컨텍스트란 쉽게 이야기해서 EntityManager 안에 생성되는 논리적인 공간?

 

3. 엔티티의 생명주기

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
    • 그냥 일반적으로 엔티티 객체만 만들어 둔 후 따로 DB 와 관련된 어떠한 코드도 실행하지 않은 상태
// Entity 객체만 생성한 상태
Member member = new Member();
member.setId(2L);
member.setName("second");
  • 영속(managed) : 영속성 컨텍스트에 의해 관리되는 상태
    • Entity 객체가 EntityManager 안에 있는 영속성 컨텍스트 공간에 올라오게 되고, 곧 EntityManager 가 해당 객체를 관리하게 된다 => 영속성 상태
    • 영속성 상태일 때 바로 DB 저장이 되는 것이 아니다!!! 저장은 EntityTransaction commit 을 사용하는 순간! DB 에 반영 된다
// em.persist 를 사용해서 이전에 생성된 비영속 상태에 해당하는 객체를 영속 상태로 변경한다
// 즉 Entity 객체가 EntityManager 안에 있는 영속성 컨텍스트 공간에 올라오게 되고,
// 곧 EntityManager 가 해당 객체를 관리하게 된다 => 영속성 상태
em.persist(member);
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태
// detach 는 해당 엔티티 객체를 영속성 컨텍스트에서 분리 한 상태 => 영속성 상태 -> 준영속상태
// 즉 EntityManager 에서 관리하는 하던 엔티티 객체가 더는 관리되지 않는 상태?
em.detach(findMem);

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

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// 엔티티 매니저 생성
EntityManager em = emf.createEntityManager();

// 엔티티 트랜잭션 시작
em.getTransaction().begin();

try {
    // 비영속
    Member member = Member.builder()
            .id(3L)
            .name("persist")
            .build();

    // 영속 컨텍스트 1차 캐시에 저장
    System.out.println("===== Before ====");
    em.persist(member);
    System.out.println("===== After =====");

    em.getTransaction().commit();

} catch (Exception e) {
    em.getTransaction().rollback();
}
em.close();
emf.close();

commit() 후에 query 가 날아간다


4. 영속성 컨텍스트의 이점

1) 조회(select) : 1차 캐시

- 영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이를 1차 캐시라고 한다. 영속성 상태인 엔테티 객체들, 즉 EntityManager 가 관리하는 엔티티 객체들은 모두 여기에 저장된다.

- 쉽게 이야기하자면 영속 컨텍스트 안에 Map 을 갖고 있는데 이 Map 을 1차 캐시라고 부르고, key 는 Entity 객체의 PK 이며 value 는 Entity 객체를 갖는다.

- JPA 는 엔티티 객체 조회 시 영속 컨텍스트 안에 있는 1차 캐시를 먼저 조회한다. 여기에 엔티티 객체가 있다면 그걸 그대로 가져다 쓰고(DB 조회X), 엔티티 객체가 없다면 DB 조회 후 1차 캐시에 해당 정보를 저장해두고 사용한다.

- 다만, 애플리케이션 전체에서 공유하는 캐시는 아니고, Db 트랜잭션 안에서만 사용하기 때문에 크게 엄청난 이득은 없다고...

// 여기서 조회해올때는 1차 캐시에 있는 내용을 먼저 조회함
// 따라서 select 문이 실행되지 X => 즉 쿼리문은 1번만 실행됨
Member findMem1 = em.find(Member.class, 3L);
Member findMem2 = em.find(Member.class, 3L);

 

2) 조회(select) : 영속 엔티티의 동일성 보장

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

- 쉽게 이야기해서 같은 트랜잭션 안에서는 자바 컬렉션에서 객체를 저장하고, 객체를 꺼내와서 비교하는 것처럼 비교가 가능하도록 만듦 => '==' 비교 가능!!!

// 여기서 조회해올때는 1차 캐시에 있는 내용을 먼저 조회함
// 따라서 select 문이 실행되지 X => 즉 쿼리문은 1번만 실행됨
Member findMem1 = em.find(Member.class, 3L);
Member findMem2 = em.find(Member.class, 3L);

System.out.println(findMem1.equals(findMem2)); // true
System.out.println(findMem1 == findMem2); // true

 

3) 등록(insert) : 트랜잭션을 지원하는 쓰기 지연

- 엔티티 매니저는 트랜잭션을 커밋하기 전까지 DB 에 엔티티를 반영하지 않고, 엔티티 매니저 안 내부 쿼리 저장소 라는 곳에 insert SQL 들을 모아 둔다. 이후 트랜잭션을 커밋하는 순간 SQL 을 DB 로 보낸다. 이것을 쓰기 지연 이라고 한다.

- 트랜잭션을 커밋하면 영속성 컨텍스트는 지금까지 내부 쿼리 저장소에 저장된 insert sql 을 플러시(flush) 한다. 여기서 플러시란 영속성 컨텍스트의 변경 내용을 DB 에 동기화하는 작업(반영하는 작업)을 의미한다.

여기서 flush 의 개념은 Buffer 에서 사용되는 flush 와 비슷하다고 생각된다. BufferedString 이나 BufferedReader 에서도 출력할 내용을 append 해두고, 저장해두다가 한번에 flush 해서 출력하는 것처럼 여기서도 '내부 쿼리 저장소' 에 차곡차곡 쌓아두다가 commit 시 한번에 flush 해서 DB 에 반영하는 것처럼 말이다.
// 트랜잭션 쓰기 지연
Member memberA = Member.builder()
        .id(6L)
        .name("persistA")
        .build();

Member memberB = Member.builder()
        .id(7L)
        .name("persistB")
        .build();
System.out.println("===== persist 전 ====");

em.persist(memberA);
em.persist(memberB);

System.out.println("===== persist 후 ====");
System.out.println("===== commit 전 =====");

et.commit();
System.out.println("===== commit 후 =====");

commit 되는 시점에 qeury 가 실행된다

4) 수정 - 변경 감지 Dirty Checking

- EntityManager 이 관리하는 영속성 컨텍스트에 해당하는 객체의 변경을 감지하게 됨

- DB 에 커밋하는 순간 EntitiyManager 는 1차 캐시 안에 저장된 엔티티 객체의 스냅샷과 현재 커밋하는 엔티티 객체를 비교한다. 이때 만약 변경된 내용이 있다면, 커밋하면서 flush 되는 시점에 변경된 내용에 대한 update 쿼리를 쓰기 지연 SQL 저장소에 저장해 두었다가 변경된 내용이 있는 update query 를 먼저 반영 한 후 다른 내용을 DB 에 commit 하게 된다.

쉽게쉽게 순서를 다시 정리하자!
1. 트랜잭션 커밋 -> 플러시 호출
2. 현재 엔티티와 스냅샷 엔티티를 비교 => 변경된 내용을 찾음
3. 변경된 내용에 대한 수정 쿼리 update query 를 생성 -> update query 는 쓰기 지연 SQL 저장소에 보관
4. 쓰기 지연 SQL 에 저장되어있던 내용을 DB에 전송
5. DB 트랜잭션 커밋
// 엔티티 수정 - 변경 감지
// 영속 엔티티 조회
Member memberC = em.find(Member.class, "6L");

// 영속 엔티티 수정
memberC.setName("hi");
memberC.setId(8L);

// 아래와 같은 update 코드 없어도 됨됨
// em.updae(memberC);

// 다만!! commit 은 필수
et.commit();

5) 삭제 - 엔티티 삭제

- 엔티티를 삭제하기 위해서는 삭제 대상 엔티티를 조회하고 엔티티 매니저에 삭제 대상 엔티티를 넘겨주어야 한다. 이때 엔티티 등록과 동일하게 삭제 쿼리 delete query 를 쓰기 지연 SQL 저장소에 등록한 뒤 트랜잭션을 커밋해서 flush 를 호출하는 시점에 DB 에 삭제 쿼리를 전달, 반영한다.

 

5. 플러시 flush

- 영속성 컨텍스트의 변경 내용과 DB 를 맞추는 작업 => DB 와 동기화 하는 작업

- 플러시 시에도 1차 캐시는 지워지지 X => 영속성 컨텍스트를 비우지 않음

- 플러시 발생 -> 수정된 엔티티에 대한 update sql 을 쓰기 지연 SQL 저장소에 등록 -> 쓰기 지연 SQL 저장소의 쿼리를 DB 에 전송(등록, 수정, 삭제 쿼리) ===> 커밋 진적에만 동기화 화면 됨

// 플러시 하는 방법
// 1. 강제로 플러시
em.flush();

// 2. commit 시
et.commit();

// 3. JPQL 쿼리 실행 시 => 자동 호출
List<Member> list  = em.createQuery("select m from Member m", Member.class).getResultList();

 

6. 준영속 상태

- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)

- 영속성 컨텍스트가 제공하는 기능을 사용 X => EntityManager 가 관리 X

/* 준영속 상태로 만드는 방법 */
// 영속성 상태
Member memberD = em.find(Member.class, 6L);

// 1. detach 사용 => 특정한 엔티티 하나만 준영속 상태로 변환3
em.detach(memberD);

// 아래처럼 엔티티를 변경해도 update query 가 생성되지 않음
memberD.setId(99L);
et.commit();

// 2. clear => 영속성 컨텍스트를 완전히 초기화, 즉 1차 캐시 전체 초기화
em.clear();

// 3. close => 영속성 컨텍스트를 종료
em.close();

 

 

- Reference

https://doublesprogramming.tistory.com/259

 

JPA - 영속성 관리

ch03-persistence-context.md 본 글은 자바 ORM 표준 JPA 프로그래밍를 읽고 개인적으로 학습한 내용 복습하기 위해 작성된 글로 내용상 오류가 있을 수 있습니다. 오류가 있다면 지적 부탁 드리겠습니다.

doublesprogramming.tistory.com

 

댓글