1차 캐시와 동일성 보장 쓰기지연 변경감지

이동영·2024년 3월 15일

JPA

목록 보기
2/8

1차 캐시

  • 영속성 컨텍스트에는 1차 캐시가 존재하며 Map[key : value] 형태로 저장이 된다.
  • entityManager.find() 호출 시 영속성 컨텍스트의 1차 캐시를 조회한다.
  • 1차 캐시에 엔티티가 존재할 경우 해당 엔티티를 반환하고 없으면 데이터베이스에서 조회후 1차 캐시에 저장 반환한다.

예제

  • 확인을 위하여 다음과 같이 find()를 호출하였더니 1차 캐시에 있는 엔티티를 조회한 결과이다.
  • find()를 사용하여 생성된 1번 맴버 앤티티가 1차 캐시에서 잘 조회되는것을 로그를 통해 확인할 수 있다.

정리 : 1차 캐시는 Map으로 존재하며 키벨류 형태로 영속성 켄텍스트에 저장이 된다. entityManager.find()를 사용하면 1차 캐시를 조회하게 되는데 이때 엔티티가 있으면 반환하고 없으면 데이터베이스에서 찾아서 1차캐시에 키벨류 형태로 저장하고 반환한다.

동일성 보장

같은 엔티티가 같은 객체로 인식되는 것을 의미한다. 쉽게 말하면 데이터베이스에서 같은 식별자를 가진 엔티티는 항상 같은 객체로 참조된다는 것을 의미한다.

고객 엔티티 : 고객 엔티티는 고객 식별자(고객 ID)를 가지고 잇다. 같은 고객 ID를 가진 고객 엔티티는 항상 같은객체로 참조된다. 더 쉽게 설명하자면 다음 예제가 있다.

Member findMember1 = entityManager.find(Member.class, 101L);
Member findMember2 = entityManager.find(Member.class, 101L);

System.out.println(findMember1 == findMember2);

PK값 101L을 가지는 엔티티에 대해서는 같은 객체인것을 보장해준다는 것을 의미한다. JPA에가 아닌 MyBatis에서는 조회 결과를 다시 인스턴스로 감싸서 다른 객체가 되어 결과는 false가 나오지만 JPA는 같은 참조값을 같도록 하여 true가 나온다.
참조 : https://jwdeveloper.tistory.com/287

하나의 트렌젝션에서 같은 키값으로 영속성 컨텍스트에 저장된 엔티티 조회시 같은 엔티티 조회를 보장한다. 바로 1차 캐시에 저장된 엔티티를 조회하기 때문에 가능하다.

1차 캐시는 도서관의 책장이다. 독자가 처음 책을 빌려야 할 때 도서관에서 책을 찾아서 빌려야 한다. 하지만 독자가 책을 두번째 빌리는 상황이라면 도서관에서 책을 찾을 필요 없이 바로 책장에 가서 책을 빌려갈 수 있다.
영속성 컨텍스트는 도서관 1차 캐시는 책장, 엔틴티는 책이라고 생각하면 된다.
정리 : 디비에서 가져온 엔티티를 1차 캐시에 저장하고 다시 조회시 1차 캐시에서 저장된 엔티티를 사용한다. 그렇기에 성능적 이점이 있다.

트랜젝션

  • 레스토랑 주문을 하는데 웨이터가 여러 음식 주문을 하나의 주문으로 처리한다. 예를들어 짜장면 탕수육 콜라를 주문하는 것은 하나의 식사 주문으로 묶인다.
  • 은행 직원이 여러 작업을 하나의 업무로 처리한다. 고객 A의 계좌에서 10만원을 인출하고 고객 B의 꼐좌에서 10만원을 입금하는 작업은 하나의 송금 업무로 묶인다. 트랜젝션은 이와 비슷하게 작동된다.
  • 여러 데이터베이스 작업을 하나의 묶음으로 처리한다.
  • 묶음 내의 모든 작업이 성공해야만 묶음이 성공적으로 처리된다.
  • 묶음중 하나라도 실패하면 묶음 전체가 실패하고 데이터베이스는 원래 상태로 돌아간다.

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

  • 영속성 컨텍스트에는 쓰기 지연 SQL 저장소가 존재한다.
  • entityManager.persist()를 호출하면 1차 캐시에 저장됨과 동시에 쓰기 지연 SQL저장소에 SQL문이 저장된다.
  • SQL을 쌓아두고 트랜잭션을 커밋하는 시점에 저장된 SQL문들이 flush되면서 데이터베이스에 반영된다. 이렇게 모아서 보내기에 성능의 이점이 있다.

memberA와 memberB를 1차 캐시에 저장하는 persist명령어를 사용하면 쓰기 지연 SQL저장소에 query문을 임시로 저장한다.

commit하는 시점에 쓰기 지연 SQL 저장소에 저장되어 있는 query들을 날려버린다.

코드로 확인하기

member1과 member2를 persist로 1차 캐시에 저장하고 commit을 하는 명령어이다.
persit와 commit 명령어 사이에는 ====으로 구분선을 출력

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

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

        // code
        Member member1 = new Member(150L, "A");
        Member member2 = new Member(160L, "B");

        em.persist(member1);
        em.persist(member2);

        System.out.println("===========================");

        tx.commit();
        em.close();
        emf.close();
    }
}


코드를 보면 원래 System.out.println("===========================");가 일어나기 전 insert가 되어야 하는것이 정상이다. 하지만 commit을 한 시점에서 한번에 쿼리문이 나가는 것을 볼 수 있다. 다시 말하지만 SQL저장소에 query문을 저장하고 commit시점에 모와서 날리기 때문이다.

참조 : https://daram.tistory.com/265

변경감지

JPA는 1차 캐시에 데이터베이스에서 처음 불러온 엔티티의 값을 갖고 있다. 그리고 1차 캐시에 저장된 엔티티의 값을 비교후에 변경내용이 있다면 UPDATE SQL문을 사용하여 쓰기 지연 SQL저장소에 담아둔다. 예를들어 중간에 Setter로 값을 변경하는 경우 update문이 쓰기 지연 SQL저장소에 저장된다. 그리고 데이터베이스 커밋 시점에 변경 내용을 자동으로 반영한다. 따로 update문을 호출할 필요 없다.


다음 예제는 persist하여 엔티티를 영속성 컨텍스트에 영구저장을 하고 있다. 그리고 setId를 통하여 값을 lee2로 변경한뒤 flush를 호출하면 insert문이 입력되고 그 다음에 update문이 일어난다. 변경이 없었다면 딱 한번의 insert문이 일어나지만 setter의 값변경 lee2로 변경하여 update문이 날라가게 된다. 그리고 나서 flush()하면 디비에 영구 반영이 된다.

비영속 : 엔티티가 영속성 컨텍스트에 등록되지 않은 상태
준영속 : 엔티티가 영속성 컨텍스트에 등록되어 있지만 아직 디비에 저장되지 않은 상태
영속 : 엔티티가 영속성 컨텍스트에 등록되어 있고 데이터베이스에 저장된 상태

  • em.persist()는 엔티티를 영속성 컨텍스트에 등록하고 준영속 상태로 만든다.
  • em.flush()는 em.persist()한 엔티티를 데이터베이스에 동기화 한다. 즉 영속 상태가 됨
  • em.flush()다음 em.persist(member)가 호출이 되는데 이 메서드는 무시가 된다. 왜냐하면 이미 엔티티가 영속성 컨텍스트에 등록이 되어 있기에 영속성 컨택스트에 영속화가 되지 않는다.
profile
가치를 제공하는 개발자

5개의 댓글