본문 바로가기
웹 개발/jpa

[jpa] 영속성 컨텍스트란 with 1차 캐시 #2

by dani0312 2024. 1. 11.

본 글에 앞서 영속성 컨텍스트에 대해 잘 모른다면 아래 글을 먼저 참고하시는 것이 좋습니다.  

2023.12.23 - [웹 개발/jpa] - [jpa] JPA 영속성 컨텍스트란? with EntityManager

 

이전 글에서는 영속성 컨텍스트가 무엇인지에 대해 알아보았다. 영속성 컨텍스트는 엔티티를 관리하는데 사용되는 중요한 개념이며 엔티티를 영구 저장하는 환경이었고, EntityManager의 persist()라는 메서드를 이용해 엔티티를 영속성 컨텍스트안에 저장하게 만드는 것이었다. 이 때 db에 저장하는 것이 아닌 영속성 컨텍스트에 저장을 한다는 것이 주의할 점이었다.

 

본 글에서는 영속성 컨텍스트가 구체적으로 어떻게 동작하는지, JPA에서 엔티티를 조회 시 이 영속성 컨텍스트에서 무슨 일이 일어나는지에 대해 알아볼 것이다.

 

 

📌JPA에서 객체(Entity) 조회 시 일어나는 일

◾영속성 컨텍스트의 1차 캐시

엔티티 조회 시 영속성 컨텍스트 안에서 무슨 일이 일어나는지에 앞서 1차 캐시에 대해 알아야한다.

영속성 컨텍스트는 내부에 `1차 캐시`란 것을 가지고 있다. 아래 그림과 같이 1차 캐시 안에는 Id와 Entity이름이 저장이 되어있다. Id는 우리가 db에서 pk로 매핑한 것이 key가 되는 것이고 값은 이 엔티티 자체가 값이 된다. 

 

 

우리가 지난 시간 객체를 엔티티매니저를 이용해 `em.persist()` 로 저장하면 영속성 컨텍스트 안에 저장되는 것이라 하였는데, 구체적으로 이야기하여 이 영속성 컨텍스트 안의 1차 캐시에 위와 같은 형태로 저장이 되는 것이었다.

 

 

그렇다면 1차 캐시에 객체를 저장하게 되면 어떤 이점이 있을까?

아래 코드를 보자 

 

◾1차 캐시에서 객체 조회 시

        try {
            Member member = new Member(10L,"member10");
            em.persist(member); // ① 1차 캐시에 저장됨

            Member findMember = em.find(Member.class, 10L); // ② 1차 캐시에서 조회
            System.out.println(findMember.getName());
            tx.commit();

 

`member10`라는 객체를 생성하여   `em.persist()`로 엔티티를 영속성 컨텍스트 안의 1차 캐시에 저장한다. 

 

② 객체를 `find()` 메서드를 이용해 이 객체를 다시 찾아오려고 한다. 여기서 한 가지 의문이 생길 수 있다. ' `em.persist()`를 하게되면 객체를 db에 저장하는 것이 아니라 우선 영속성 컨텍스트에 저장을 한다고 하였는데 아직 db에 반영이 안된 상태일텐데 `find()`로 조회를 할 수 있을까?' 결론적으로 말하면 가능하다.  이유는 1차 캐시에 저장하였기 때문이다.


 jpa는 `find()`를 이용하여 객체를 조회 시에 우선적으로 1차 캐시를 먼저 조회한다. db를 탐색하기 전에 1차캐시에 우선 우리가 찾고자 하는 객체가 있는지를 먼저 탐색하는 것이다. 1차 캐시를 탐색하였을 때 우리가 찾고자하는 객체가 있으면 1차캐시에서 조회를 해온다. 이 경우 db까지 가지 않는다. 과정을 정리하면 아래와 같다. 

 

 

객체 조회 흐름

1. find()  → 영속성 컨텍스트의 1차 캐시 탐색🔎 →  1차 캐시에 존재 ⭕ → 1차 캐시에서 조회해옴

2. find() → 영속성 컨텍스트의 1차 캐시 탐색🔎 → 1차 캐시에 존재 ❌  → DB를 탐색 🔎→ 영속성 컨텍스트에 저장 → 엔티티 반환

 

 

즉 위의 작성한 코드는  1번과 같기 때문에 db를 굳이 조회하지 않는다.  그렇기 때문에 아래처럼 코드를 실행한 결과를 보아도 SELECT 쿼리가 출력되지 않았다. 만일 1차 캐시에서 객체를 찾았을 때 객체가 조회되지 않았다면 db에서 값을 탐색해야하니 SELECT 쿼리가 출력되었을 것이다. 

 

 

만일 2번과 같이 1차 캐시에 존재하지 않는 경우는 SELECT쿼리를 이용해 db를 탐색한다. 만일 db에서 찾았다면 영속성 컨텍스트에도 그 객체를 저장한다. 

 

getName()코드가 실행된 후에 INSERT 쿼리가 출력되는 것은 `em.persist()`를 통해 영속성 컨텍스트에 객체를 먼저 저장해두었다가 트랜잭션이 커밋될 때 그 시점에 실제로 db에 반영이 되기 때문에 그때서야 쿼리가 나가는 것이다. 본 단락을 요약하면 아래와 같다.

 

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

💡em.persist()호출 시 객체는 1차 캐시에 저장된다.

💡 JPA는 객체를 조회 시 영속성 컨텍스의 1차 캐시를 먼저 조회한다. 1차 캐시에서 객체를 찾았다면 DB까지 조회하지 않는다. 

 

 

◾1차 캐시 조회의 이점?

1차 캐시에 저장된 것은 db를 조회하지 않는다는 이점이 있다는 것을 알게 되었다. 그런데 이것이 사실상 유의미한 도움은 되지 않는다. 왜일까?

 

엔티티 매니저는 데이터베이스 트랜잭션 단위로 보통 만들고 트랜잭션이 끝날 때 같이 종료된다. 즉 한 명 고객의 요청이 들어와 서비스를 하고 종료되면 이 영속성 컨텍스트를 지우는 것이다. 이 안의 1차 캐시도 물론 날아간다. 그렇기 때문에 1차 캐시의 이점도 굉장히 찰나의 순간에 일어나는 일이다. 고객 한 명당 트랜잭션이 시작되고 종료되므로 여러 명의 고객이 1차캐시를 같이 사용하는 그런 개념이 아닌 것이다. 

 

애플리케이션 전체에서 공유되는 캐시는 JPA나 Hibernate에서는 2차 캐시라고 한다. 1차 캐시는 데이터베이스의 한 트랜잭션 안에서만 효과가 있기 때문에 성능상 아주 큰 장점은 없다고 본다. 비즈니스 로직이 아주 복잡한 경우는 도움이 될 수도 있다. 이에 대한 예시는 아래에 있다. 그러나 현업에서는 이것이 크게 도움을 주지는 않는다고 한다. 

 


예시 ( 같은 객체를 두 번 조회한다면 )

로직이 복잡하여 1차 캐시가 장점이 되는 경우를 예로 든다면,

10번이라는 객체가 db에 저장이 되어있는 상황이며 이것이 영속성 컨텍스트에는 저장이 되어있지 않은 상태라고 가정한다. 그리고 아래와 같이 그 객체를 각각 `find()`로 2번 조회한다. 그러면 어떤 일이 일어날까? 

Member findMember1 = em.find(Member.class, 10L);
Member findMember2 = em.find(Member.class, 10L);

 

findMember1의 경우를 보자. 우선  JPA는 `find()`로 조회 시 1차 캐시에서 먼저 찾는다고 하였다 그런데 1차 캐시에 없으니 이제 db를 조회할 것이다. db에서 조회한 member를 1차 캐시에 저장한다. 그리고 member를 반환한다. 

 

findMember2의 경우를 보자. 1차 캐시에서 객체를 조회한다. 그런데 findMember1에서 1차 캐시에 저장해두었으므로 객체가 존재하므로 1차 캐시에서 조회해온다. 

 

즉 결과적으로 SELECT쿼리는 한 번만 나가게 된다. DB를 1번만 조회하였으므로. 이해가 잘 가지 않는다면 아래 흐름을 보면 도움이 될 것이다. 

 

 

조회 흐름 정리

1. findMember1를 find()로 조회 → 1차 캐시에 없음 → db조회 → 영속성 컨텍스트에 올린다.

2. findMember2를 find()로 조회 → 1차 캐시에 있음 → 결과 반환

 

 

GPT도 아래와 같은 설명을 해준다. 이 예시를 정리하는데 도움이 될 것이다.

 

🤖GPT : 쿼리를 실행하여 데이터베이스에서 가져온 엔터티는 그 후에는 1차 캐시에 존재하게 되므로 동일한 엔터티를 다시 조회할 때에는 데이터베이스에 다시 쿼리를 날리지 않고 1차 캐시에서 바로 반환될 수 있습니다. 이것이 JPA의 영속성 컨텍스트의 중요한 특징 중 하나입니다


 

 

 

 

 


참고

자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한 / 인프런 강의


/* 내가 추가한 코드 */ /* 내가 추가한 코드 끝끝 */