[JPA]영속성 컨텍스트의 기능 4가지

Sunghun Kim·2024년 11월 4일

Jpa

목록 보기
1/10

영속성 컨텍스트(Persistence Context)

Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다.

  • 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요.

    EntityManager는 이름 그대로 Entity를 관리하는 관리자입니다.

  • 개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제할 수 있습니다.
  • EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있습니다.

1. 1차 캐시 저장소

  • 영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있습니다.
  • 우리가 저장하는 Entity 객체들이 1차 캐시 즉, 캐시 저장소에 저장된다
  • 캐시 저장소는 Map 자료구조 형태로 되어있습니다.
    • key에는 @Id로 매핑한 기본 키 즉, 식별자 값을 저장합니다.
    • value에는 해당 Entity 클래스의 객체를 저장합니다.
    • 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리한다.

1차캐시의 장점(객체 동일성 보장)

- **'1차 캐시'** 사용의 장점
1. DB 조회 횟수를 줄임
2. **'1차 캐시'**를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)
- 객체 동일성 보장
    
    ```sql
    @Test
    @DisplayName("객체 동일성 보장")
    void test4() {
        EntityTransaction et = em.getTransaction();
    
        et.begin();
        
        try {
            Memo memo3 = new Memo();
            memo3.setId(2L);
            memo3.setUsername("Robbert");
            memo3.setContents("객체 동일성 보장");
            em.persist(memo3);
    
            Memo memo1 = em.find(Memo.class, 1);
            Memo memo2 = em.find(Memo.class, 1);
            Memo memo  = em.find(Memo.class, 2);
    
            System.out.println(memo1 == memo2);
            System.out.println(memo1 == memo);
    
            et.commit();
        } catch (Exception ex) {
            ex.printStackTrace();
            et.rollback();
        } finally {
            em.close();
        }
    
        emf.close();
    }
    ```
    
    - 같은 값을 조회하는 memo1과 memo2는 == 결과 true를 반환합니다.
    - memo1과 다른 값을 조회하는 memo는 == 결과 false를 반환합니다.
    
    <aside>
    💡
    
    보통 Java에서  == 표시로 객체를 비교하면 주소가 다르다고 판단해 false가 나오는데?
    find로 1번을 가져오면 1차 캐시에 있는 값을 동일하게 받아와 동일하게 판단한다. 
    객체가 한개당 하나라고 인정을 해준다.
    
    </aside>

영속성 컨텍스트가 1차 캐시 저장소 활용하는 방법

Ex. EntityManager로 transaction 환경을 만들었다는 가정.
  1. Entity 저장
  • persist 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장한다.
  1. Entity 조회
    find메서드 호출로 조회를 해온다.
  • find 메서드 호출시?
  • 1차 캐시저장소에 조회하는 Id 값이 없으면?
    • DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환
  • 1차 캐시저장소에 조회하는 Id 값이 있으면?
    • 캐시 저장소에 식별자 값과 Entity 타입 값이 있는지 조회 후 반환
  1. Entity 삭제
  • 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB 조회해서 1차 캐시 저장소 저장
    • remove 메서드 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL DB에 요청

2. 쓰기 지연 저장소(ActionQueue)

  • JPA는 트랜잭션처럼 SQL을 모아서 한번에 DB에 반영하는데 JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영합니다.

  • 쓰기 지연이 발생하는 시점

    • flush() 동작이 발생하기 전까지 최적화한다.
    • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 DB에 반영만 가능하다.
  • 쓰기 지연 효과

    • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.

    • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.

    • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.

    • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.

      💁‍♂️ 키 생성전략이 `generationType.IDENTITY` 로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.
    • why? 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.

    • 트랜잭션 commit 후 쓰기 지연 저장소의 SQL들이 한번에 요청됨을 확인했습니다.

  • 사실 트랜잭션 commit 후 추가적인 동작이 있는데 바로 em.flush(); 메서드의 호출입니다.

  • flush 메서드는 영속성 컨텍스트의 변경 내용들을 DB에 반영하는 역할을 수행합니다.

    • 즉, 쓰기 지연 저장소의 SQL들을 DB에 요청하는 역할을 수행합니다.
  • flush() 동작 확인을 위해 직접 호출해보겠습니다.

@Test
@DisplayName("flush() 메서드 확인")
void test7() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(4L);
        memo.setUsername("Flush");
        memo.setContents("Flush() 메서드 호출");
        em.persist(memo);

        System.out.println("flush() 전");
        em.flush(); // flush() 직접 호출
        System.out.println("flush() 후\n");
        

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}
💡 flush 메서드가 호출되었을때 "flush() 후" 바로 전에 쿼리가 날라간다. commit 메서드 호출해도 쿼리가 날라가지 않는다.
  • em.flush(); 메서드가 호출되자 바로 DB에 쓰기 지연 저장소의 SQL이 요청되었습니다.
  • 이미 쓰기 지연 저장소의 SQL이 요청 되었기 때문에 더 이상 요청할 SQL이 없어 트랜잭션이 commit 된 후에 SQL 기록이 보이지 않습니다.

Flush() 메서드

  • Commit이 일어날 때 flush가 내부적으로 먼저 호출된다.

flush()는 데이터베이스에 변경된 객체의 상태를 동기화하는 작업으로, 아직 트랜잭션이 완료되기 전에 메모리 내의 객체를 데이터베이스와 일치시키는 역할을 합니다.

즉, 변경된 객체가 데이터베이스에 반영되고 나서, commit()이 호출되어 트랜잭션이 완전히 저장(persist)됩니다.

따라서 `commit()`을 호출하면:

1. *flush()**가 먼저 실행되어 변경 사항이 데이터베이스에 기록됩니다.(SQL 쿼리 날라간다.)
2. 그런 다음 **commit()**이 호출되어 트랜잭션을 확정하고, 이를 통해 변경 사항이 영구적으로 저장됩니다.
  • 추가) 트랜잭션을 설정하지 않고 flush 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생합니다.

    💡 - Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션 환경이 필요하다.

    (Select는 상관없다. ⇒ 필수는 아니지만 필요할 때가 있다.)
    필요할때는 FetchType이 Lazy이고 Select 할때 Transasctional 환경이 필요하다.

3. Dirty Checking(변경 감지)

💡 무조건 Transaction 환경이어야(Transaction이 있어야) 변경 감지 기능이 실행이 된다.
  • 영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면?
    • 하나의 Update SQL로 처리할 수 있는 상황을 여러번 Update SQL을 요청하게 되기 때문에 비효율적입니다.
  • 그렇다면 JPA는 어떻게 Update를 처리할까요?
    • em.update(entity); 같은 메서드를 지원할 것 같지만 찾아볼 수 없습니다.

JPA의 Update 처리

  • JPA에서는 Update를 어떻게 처리하는지 살펴보겠습니다.
  • JPA는 영속성 컨텍스트에 Entity를 저장할 때 Entity의 최초 상태(LoadedState)를 저장합니다.
    • 트랜잭션이 commit되고 em.flush(); 가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교합니다.
    • 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기 지연 저장소의 SQL을 DB에 요청합니다.
    • 마지막으로 DB의 트랜잭션이 commit 되면서 반영됩니다.
  • 따라서 변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영됩니다.
    • 이러한 과정을 변경 감지, Dirty Checking이라 부릅니다.

flush메서드 그냥 넣어서 호출 했다.(commit 메서드 호출해도 자동 호출 된다.)

@Test
@DisplayName("변경 감지 확인")
void test8() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        System.out.println("변경할 데이터를 조회합니다.");
        Memo memo = em.find(Memo.class, 4);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("\n수정을 진행합니다.");
        memo.setUsername("Update");
        memo.setContents("변경 감지 확인");
        
        System.out.println("flush 전");
        em.flush();
        System.out.println("flush 후");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

console 결과

변경할 데이터를 조회합니다.
Hibernate: 
    select
        m1_0.id,
        m1_0.contents,
        m1_0.username 
    from
        memo m1_0 
    where
        m1_0.id=?
memo.getId() = 6
memo.getUsername() = Tw4YomulpY
memo.getContents() = 메모1

수정을 진행합니다.
flush 전
Hibernate: 
    /* update
        com.sparta.entity.Memo */ update memo 
    set
        contents=?,
        username=? 
    where
        id=?
flush 후
트랜잭션 commit 전
트랜잭션 commit 후

4. 지연 로딩 & 즉시 로딩(Lazy, Eager)

  • JPA는 연관관계가 설정된 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 정할 수 있습니다.

    • 즉, 가져오는 방법을 정하게되는데 JPA에서는 Fetch Type이라 부릅니다.
    • Fetch Type의 종류에는 2가지가 있는데 하나는 *LAZY*, 다른 하나는 *EAGER 입니다.*
    • LAZY지연 로딩으로 필요한 시점에 정보를 가져옵니다.
    • *EAGER즉시 로딩으로 이름의 뜻처럼 조회할 때 연관된 모든 Entity의 정보를 즉시 가져옵니다.*
  • 기본적으로 @OneToMany 애너테이션은 Fetch Type의 default 값이 LAZY로 지정되어있고 반대로 @ManyToOne 애너테이션은 *EAGER로 되어있습니다.*

  • 다른 연관관계 어노테이션들도 default 값이 있는데 이를 구분하는 방법이 있습니다.

    • 어노테이션 이름에서 뒤쪽에 Many가 붙어있으면 설정된 해당 필드가 Java 컬렉션 타입일 것입니다.
      • 즉, 해당 Entity의 정보가 여러 개 들어있을 수 있다는 것을 의미합니다.
      • 따라서 효율적으로 정보를 조회하기 위해 지연 로딩이 default로 설정되어있습니다.
    • 반대로 이름 뒤쪽이 One일 경우 해당 Entity 정보가 한 개만 들어오기 때문에 즉시 정보를 가져와도 무리가 없어 즉시 로딩이 default로 설정되어있습니다.
  • 지연 로딩된 Entity의 정보를 조회하려고 할 때는 반드시 영속성 컨텍스트가 존재해야합니다.

  • ‘영속성 컨텍스트가 존재해야한다’라는 의미는 결국 SpringBoot에서 ‘트랜잭션이 적용되어있어야 한다’라는 의미와 동일합니다.

profile
BackEnd Developer!!

0개의 댓글