들어가며
이 포스팅은 김영한 강사님의 자바 ORM 표준 JPA 프로그래밍 강의와 저서를 주로 참고하여 작성하였음을 미리 밝힌다.
Spring data JPA를 사용하여 개발을 진행하기 전에 JPA와 하이버네이트에 대한 이해가 부족하다고 판단하여 공부를 하기 위해 작성된 포스팅이므로 잘못된 정보가 있을 수 있기에 김영한 강사님의 저서와 강의를 직접 듣고 공부하는 걸 추천한다.
엔티티 매니저 팩토리와 엔티티 매니저
엔티티 매니저는 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 등 엔티티와 관련된 모든 일을 처리한다. 쉽게 생각하면 엔티티 매니저는 엔티티를 저장하는 '가상의 데이터 베이스'라고 볼 수 있다.
엔티티 매니저 팩토리는 이 엔티티 매니저를 만드는 '공장'인데, 공장을 만드는 건 현실에서든 개발에서든 비용이 크게 드는 일이기 때문에 한 개만 만들어서 어플리케이션 전체에서 공유하도록 설계되어 있다.
생성 비용이 큰 엔티티 매니저 팩토리는 요청이 올 때마다 생성 비용이 거의 없는 엔티티 매니저를 생성하게 되는데, 간단히 말하자면 엔티티 매니저 팩토리라는 '공장'에서 엔티티 매니저라는 '관리자'를 여러명 두는 셈이다.
이때, 알아둬야하는 점은 엔티티 매니저 팩토리는 여러 스레드(요청)가 동시에 접근해도 안전하기 때문에 여러 스레드에서 동시에 접근해도 안전하지만, 엔티티 매니저는 Thread Safe하지 않으므로 여러 스레드가 동시에 접근하면 동시성 문제가 발생한다. 따라서 요청 별로 한개씩만 할당하고 다른 스레드 간에 공유는 절대 하면 안된다고 한다.
영속성 컨텍스트(Persistence Context)
영속성 컨텍스트(Persistence Context)는 JPA를 이해하는 데 가장 중요한 용어로, '엔티티를 영구 저장하는 환경' 혹은 '엔티티를 담고 있는 집합' 정도로 설명할 수 있다.
엔티티 매니저로 저장하거나 조회한 엔티티는 영속성 컨텍스트에 보관되고, 엔티티 매니저에 의해 관리된다. 즉, 영속성 컨택스트는 직접 접근이 불가능하고 엔티티 매니저를 통해서만 접근이 가능하다.
참고로 Spring Data JPA를 사용하면 기본으로 엔티티 매니저가 활성화 되어있는 상태라고 한다.
대략적으로 영속성 컨텍스트가 뭘 하는 녀석인지는 알게 되었다. 하지만 사실상 영속성 컨텍스트가 왜 존재하는 지 알기가 어렵다.
눈에 보이지도 않고, 잘못 사용하면 에러도 내뱉는 영속성 컨텍스트가 꼭 필요한 것인가 하는 의문이 들 수도 있다.
그냥 바로 쿼리를 날려서 DB에 저장하면 되는거 아닌가? 왜 어플리케이션이랑 DB 사이에 쟤를 두어서 한 단계를 더 거치지?
당연하겠지만 영속성 컨텍스트가 엔티티를 관리했을 때 얻을 수 있는 이점이 분명히 존재하기 때문이다.
그 이점으로 크게 5가지를 뽑을 수 있는데, 들어가기에 앞서 엔티티의 생명 주기를 먼저 보고 나서 알아보자.
엔티티의 생명주기
- 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속 : 영속성 컨텍스트에 저장된 상태
- 준영속 : 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 더 이상 영속성 컨텍스트가 관리하지 않는 상태
- 삭제 : 엔티티를 삭제한 상태
영속성 컨텍스트의 이점
1. 1차 캐시로 성능상 이점을 얻을 수 있다.
영속성 컨텍스트는 내부에 캐시를 가지고 있다. 이게 바로 1차 캐시인데, 영속 상태의 엔티티는 모두 이 곳에 저장이 되고, 엔티티를 조회할 때 영속성 컨텍스트에 1차 캐시가 존재하지 않는다면 DB에서 값을 조회 후 해당 엔티티를 생성해 1차 캐시에 저장한다. 그리고 나서 영속 상태인 (영속성 컨텍스트에 저장된) 엔티티를 반환한다.
여기서 영속성 컨텍스트는 엔티티를 식별자값(@Id 값)으로 구분하기 때문에 1차 캐시에는 아래 사진과 같이 이 식별자 값과 엔티티 두가지를 마치 Map과 같은 형태로 저장한다.
반대로 영속성 컨택스트에 1차 캐시가 존재하는 경우, DB에 값을 조회하는 대신 1차 캐시를 조회해서 해당하는 값을 반환해준다. 이로써 DB에 SELECT 쿼리를 날리는 단계가 줄어들게 되므로 성능상 이점을 누릴 수가 있게 된다.
2. 영속 상태의 엔티티의 동일성을 보장한다.
영속성 컨텍스트는 엔티티를 여러번 조회하는 경우에도 1차 캐시에 저장되어있는 값을 반환해주기 때문에, 몇 번을 조회하든 그 값의 참조값이 동일한 엔티티를 얻을 수 있다.
3. 트랜잭션을 지원하는 쓰기 지연
조금 이해하기 어려운 부분이지만 차근차근 따라가보자. 해당 부분은 Spring Data JPA를 가지고는 이해하기 어려운 부분이기 때문에 하이버네이트와 함께 이해해보기로 한다.
엔티티를 등록할 때, 하이버네이트는 엔티티 매니저를 생성하고, 트랜잭션을 시작한 다음, 엔티티를 영속화시키고 마지막으로 트랜잭션을 커밋하는 과정을 거친다. 아래 코드와 같이 말이다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
여기서 주목해야할 부분은 '트랜잭션을 커밋하기 전까지 INSERT 쿼리를 데이터 베이스에 보내지 않는다는 점' 이다.
다시 말하면 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT 쿼리를 차곡차곡 모아둔다.
이게 바로 '쓰기 지연'이다. DB에 INSERT 즉, 데이터를 '쓰는' 과정을 잠깐 미뤄둔다는 의미이다.
이제 쿼리 저장소에 memberA와 memberB의 INSERT 쿼리가 저장되었다. 이 저장소에 저장되어있는 데이터는 언제 최종적으로 DB에 저장되냐면 transaction.commit();
함수가 호출되는 순간이다. 트랜잭션이 커밋되는 시점에 엔티티 매니저는 영속성 컨텍스트를 플러시(flush)한다.
플러시(flush)는 간단하게 말하면 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업으로, 플러시를 했을 때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다.
영속성 컨텍스트가 플러시를 해서 엔티티의 변경 내용이 데이터베이스에 동기화 된 후에 실제 데이터베이스에 트랜잭션을 커밋한다.
트랜잭션을 커밋한다의 의미는 뭘까?
트랜잭션과 커밋의 의미를 파악하고 넘어갈 필요가 있다.
우선 트랜잭션이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위를 뜻한다. 커밋(commit)은 트랜잭션 내에서 변경된 데이터들을 DB에 저장해달라고 요청하는 명령으로, 커밋 명령이 수행되어야 변경사항이 DB에 반영된다.
보통 커밋이라는 용어를 깃(Git)을 사용할 때 처음으로 마주칠 것이다. 우리가 코드를 작성하거나 변경한 후에 깃허브에 커밋을 하기 위해서 add 명령로 커밋할 내용을 로컬 repo에 추가한 다음 commit 명령어로 작업내용에 원하는 메시지와 함께 로컬 repo에 이를 반영하고, push 명령를 이용해서 원격 저장소에 최종적으로 작업내용을 업로드 해준다.
이런 식으로 JPA는 persist() 함수 호출로 엔티티를 저장소에 차곡차곡 담아두었다가 트랜잭션을 커밋함으로써 이를 DB에 반영해주는 과정을 거친다.
물론 JPA와 Git이 데이터를 저장하는 과정이 정확히 일치하는 지는 확실하지 않지만 이런 식으로 이해하면 더 쉬울 것같다.
여기서 궁금한 점이 하나 생긴다. 위에서 트랜잭션을 커밋을 해야 DB에 변경내용이 반영된다고 했는데, 트랜잭션의 커밋 명령어는 SQL 문법으로 존재한다. 그렇다면 JPA(Hibernate) 뿐만 아니라 Mybatis에서도 이러한 트랜잭션을 지원하는 쓰기 지연이 가능한게 아닐까? 그렇다면 쓰기 지연이 영속성 컨택스트의 이점이 될 수 있을까?
궁금한건 참을 수 없어 구글링을 해본 결과, Mybatis는 쓰기 지연을 하지 않는다는 결론을 도출했다.
다음 글을 보자
JPA의 쓰기지연 기능 확인해보기 (transactional write-behind)
JPA의 특징중 하나는 영속성 컨텍스트 (Persistence Context)내에서 쓰기지연(transactional write-behind)이 발생한다는 것입니다. 이 글에서는 JPA와 Mybatis를 이용해 쓰기지연을 했을때와 안했을 때를 비교해.
soongjamm.tistory.com
이 글을 읽으면서 쓰기 지연을 정리하고 있었음에도 제대로 이해하지 못했다는 걸 깨달을 수 있었다.
일단 쓰기 지연의 가장 큰 이점은 DB와의 커넥션을 '단 한번'만 연결함으로써 쿼리문을 날릴 수 있다는 점이다. DB에 쓰기를 하는 과정은 비용적으로 비효율적이기 때문에 이러한 쓰기 지연이 없다면 쿼리를 날릴 때마다 DB를 왔다갔다 해야하기 때문에 성능이 다소 떨어진다는 것이다.
4. 변경을 자동으로 감지한다.
JPA로 엔티티를 수정할 때에는 단순히 엔티티를 조회해서 데이터만 변경시키면 된다. 따로 update() 메소드를 호출하거나 할 필요가 없다는 말이다. 이렇게 엔티티의 변경사항을 DB에서 자동으로 반영하는 기능을 변경 감지(Drity Checking)이라고 한다.
JPA는 어떻게 자동으로 변경을 감지해서 수정사항을 반영해줄까?
위에서 JPA는 1차 캐시에 조회한 엔티티를 저장한다고 했다. 그때 사실은 엔티티가 조회된 최초 상태를 복사해서 저장해두게 되는데 이를 '스냅샷(Snapshot)'이라고 부른다.
우리가 조회한 엔티티를 수정한 다음 다시 커밋을 요청하면 내부에서 플러시를 호출하게 되는데, 이때 엔티티 매니저는 우리가 수정한 엔티티와 스냅샷을 비교하게 된다. 비교 후에 변경된 엔티티가 있음을 깨달은 엔티티 매니저는 수정 쿼리를 생성해서 쓰기 지연 저장소에 쿼리를 저장하고, 다시 플러시를 호출, 트랜잭션을 커밋하는 과정을 거친다.
이때 주의해야 할 점은, 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다는 점이다. 비영속, 준영속 처럼 영속성 컨텍스트의 관리를 받지 못하는 엔티티는 값을 변경해도 영속성 컨텍스트 내부의 1차 캐시와 비교를 하지 않기 때문에 DB에 반영되지 않는다.
마치며
사실 영속성 컨텍스트의 이점 5번째로 지연로딩이 있다. 이 부분은 조금 내용이 복잡하기도 하고 내용이 길어지므로 다음 포스팅에서 다뤄보기로 한다.
출처 및 참고
'Study > Spring boot' 카테고리의 다른 글
[Spring boot] WebClient 사용해보기 - 모듈화 1-1 (0) | 2023.08.25 |
---|---|
[JPA] 프록시(Proxy), 지연 로딩(LAZY Loading), 즉시 로딩(EAGER Loading) (0) | 2022.06.23 |
[JPA] JPA, Hibernate, Spring Data JPA의 차이점 (0) | 2022.06.20 |
[Spring boot] RestControllerAdvice를 이용한 예외 처리 (0) | 2022.06.13 |
[Spring boot] 스프링 프레임워크(Spring Framework) (0) | 2022.06.09 |