[Spring JPA] 영속성 컨텍스트 시리즈 (1) - 영속성 컨텍스트와 1차 캐시
JOINED
를 이용해 상속받은 JPA Entity
는 어떻게 저장될까라는 궁금증으로 시작했다.
Repository
에서 저장 될 때 기본적인 엔티티가 어떻게 저장되는지가 궁금했고, 이왕 알아보는 김에 Hibernate
에서 정확하게 어떻게 구현이 되었는지 살펴보는 것까지 목표로 한다.
서론
결국 문제는 단순 휴먼 에러였다. 왜냐하면, 보이는 것처럼 추측되는 Account
엔티티를 저장하지 않은 채로, AUTO INCREMENT
값을 불러오지 않은채 바로 다른 곳에 사용했기 때문에 일어난 일이었다.
하지만, 이런 정보를 공부하면서, 데이터베이스 상속과 JPA
의 잘못된 사용에 따른 문제인지, 아니면 JpaRepository.save()
함수를 잘못사용한 문제인지를 스스로 알아내지 못한 점은 분명히 내가 영속성 컨텍스트를 알지 못하기 때문에 일어난 일이다
이에 나는 이번 기회에 영속성 컨텍스트에 대해 자세히 살펴보기로 했다.
영속성 컨텍스트
이론 살펴보기
사실 이번에 JPA 구현체를 찾아보면서 느낀건데, 단순히 검색했을 땐 제대로 된 공식 문서가 존재하지 않았다. 오히려 구현 스펙에 대한 사이트만 찾을 수 있었고, 이게 무엇인지에 대해서 제대로 설명해주지 않아서 찾는데 복잡했다.
그래서 이번에 AI
검색을 활용했고, 관련된 유용한 공식 문서를 찾을 수 있게 되었다.
글 하나로 이론적인 부분을 모두 담을 수 없는 점을 고려하여 더욱 흥미롭게 느껴진다면 아래 글들을 읽어보면 좋을 것이다.
Java EE 5 공식문서 영속성 항목 Hibernate 영속성 컨텍스트 공식문서
정의
영속성 컨텍스트란 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드 간 동기화를 도와주며, 엔티티 객체를 보관하는 기능을 가진 객체이다.
이때, 엔티티가 영속성 컨텍스트로 들어오게 되면 해당 엔티티는 영속 객체 PersistObject
라고 부르게 된다.
Spring Data JPA 가 아닌 일반 JPA
레포지토리 코드에서 사용하는 entityManager
는 영속성 컨텍스트에 접근하기 위한 수단이다.
엔티티 인스턴스를 보존하기 위해서는 실제로 위의 언급처럼, entityManager 에서 persist 가 호출될때 트랜잭션이 완료 된 후 엔티티를 저장하게 된다.
영속성 컨텍스트의 4개의 생명주기
Java EE 5 에서는 영속성 컨텍스트와 연관된 엔티티의 4가지의 생명주기를 정의한다.
New entity
식별이 되지 않은 엔티티를 의미하며, 영속성 컨텍스트와 연관이 아직 없는 상태이다.
Managed entity
식별이 되고 있는 엔티티이며 영속성 컨텍스트와 연결되어있는 상태이다.
Detached entity
영속적으로 식별이 되고 있는 엔티티 이지만, 현재 영속성 컨텍스트와 연결되지 않는 상태를 의미한다.
Removed entity
영속적으로 식별이 되며, 영속성 컨텍스트와 연결이 되어있다. 하지만, data store(데이터 소스) 등에 의해서 삭제가 예정되어있는 상태를 뜻한다.
의의
이러한 영속성 컨텍스트를 사용함으로써 얻는 이점은 뭘까?
엔티티와 데이터베이스의 객체 지향적 연결
우리는 흔히 JPA 를 ORM
이라고 부른다. Object Relational Mapping 이라고 부르며, 객체와 관계간의 매핑을 뜻한다.
실제로 위 문서 내용을 보면, 엔티티 데이터와 데이터 베이스의 동기화를 목표로 하고 있는 만큼, 영속성 컨텍스트는 자바 객체(엔티티)와 데이터 베이스의 중간다리 역할을 함으로써, 보다 객체지향적인 관점으로 데이터베이스를 관리할 수 있게 끔 도와준다.
1차 캐시
영속성 컨텍스트는 우리가 흔히 부르는 1차 캐시
로써의 역할을 한다. 하지만 이 단어에 대해 의문을 가져본 사람은 있는가? 대부분의 백엔드 개발자라면 의문을 가지고 그 이유를 알 것이지만, 나는 백엔드에 입문한지 얼마 되지 않았으므로 해당 단어가 엄청 생소하게 다가왔다.
도대체 왜 1차일까?
- CPU의 관점을 보자.
이름을 1차, 2차로 나누는 것에 대해서 나는 도저히 이해가 안갔다. 하지만, 컴퓨터 지식
에서 해당 언어와 아주 밀접한 관련이 있는 도메인이 있다. 바로 CPU
이다.
CPU 에는 L1, L2, L3 캐시가 존재한다. 이와 같은 레벨에서 따온건 아닐까 하는 추측이다. 결론적으론 틀렸지만, 우리는 여기서 생각해볼 수 있는 점이 하나 있다.
CPU 에서 1차 캐시는 어떤 것인가? CPU
와 가장 가까운 캐시를 뜻한다. 이 뜻은 사실은 가장 자주 참조되는 가장 중요한 캐시
라는 뜻이다. 따라서 실제로 2차 캐시로 불리는 것들은 해당 1차 캐시, 즉 영속성 컨텍스트보다는 덜 중요한 개념이 된다.
쉽게 말해, 영속성 컨텍스트는 애플리케이션의 사실상 CPU인 비즈니스 로직과 가장 가까운 위치에 존재하는 캐시 역할을 하고 있다고도 볼 수 있다.
이가 실제로 일어나는 원리에 빗대어 설명하면 어떻게 될까.
우리가 실질적으로 데이터베이스에 persist
, 즉, 영속 상태로 전환하여 데이터베이스에 등록하지 않는한 우리가 설정한 엔티티는 바뀌지 않는다. 다시 말해, 우리가 영속하지 않는 한 애플리케이션 레벨 내에서 대부분의 작업이 이루어지고 DB에 영향이 가지 않는다. 즉, 캐싱이다!
1
2
3
4
5
6
7
Application Logic
<->
First Level Cache (영속성 컨텍스트) -- 가장 기본적으로 참조되는 캐시로써의 역할이 됨
<->
Second Level Cache -- 영속성 컨텍스트 내에 캐시가 존재하지 않으면 부가적으로 필요할 경우 애플리케이션 전체레벨에서 캐싱이 됨
<->
Database -- 그래도 캐싱이 의미없는 상황이라는 의미에서 DB가 가장 마지막으로 참조될 수 있다는 것
- 2차 캐시와 구별하기 위함이다.
그렇다면 2차 캐시는 무엇일까?
Jakarta EE 공식문서에서 간단하게나마 살펴볼 수 있다.
2차 캐시는 엔티티를 아우르는 전체적인 애플리케이션 레벨에서 캐싱하는 캐시이다. JPA 2.0 부터 공식적으로 API 에 추가 되었다. 즉, 애플리케이션 전역에서 엔티티 데이터를 공유하여 데이터베이스 접근을 최소화하는 캐시 계층이라고 볼 수 있는데..
@Cachable(true)
등으로 캐싱을 시킬 수 있으며, Cache 에 관해서는 인터페이스를 개발자가 구현하여 캐시 구현을 각자에게 맡길 수도 있다. 이 구현 또한 Hibernate 가 해주며, JPA 공식 표준 (Jakarta EE) 에 등재되어 있다.
또 다른 Jakarta EE 공식문서 에 의하면 애플리케이션 단 개발자는 캐시 사용을 하는 지 모른채 엔티티 매니저에 저장하는 식으로 자연스럽게 캐싱을 구현할 수 있도록 도와주는 스펙이라는 의미이다.
즉, 영속성 컨텍스트에서 저장하는 그 이후까지의 단계를 바라보기 위한 캐시이므로, 구별이 되어야한다. 1차적으로 영속성 컨텍스트에서 영속 명령 (persist
) 를 한 이후 상황의 캐싱을 바라보기때문의 2차적인 캐시라고 부르는 것이 아닐까 추측하게 됐다.
- 트랜잭션 범위 내에서 유효하기 때문이다.
이게 가장 큰 차이점인데, 2차 캐시의 경우 모든 영속성 컨텍스트에서 공유되는 캐시이므로, 단계 자체가 다르다.
우리가 일반적으로 애플리케이션 비즈니스 로직에서 사용하는 캐시라고 생각하면 되는데, 영속성 컨텍스트만으로는 이런 문제를 해결하기에는 어렵기 때문이다.
위에서 언급한대로 영속 명령 이후의 상황을 보기 때문에 트랜잭션 단계 외부적으로 캐싱을 진행하고 있다.
이 의미는 2차 캐시에 비해 1차적인 단계의 단순 캐싱을 의미하기 때문에 구별의 필요성이 있다.