스프링

영한님 책에서 볼 수 없는 JPA (Hibernate) 내부 코드 살펴보기

쿠쿠s 2023. 3. 14. 16:24

몇 달 전 JPA를 공부를 하는데 책에서 말하는 엔티티매니저, 쓰기지연저장소 등등 이런 용어들은 코드로 어떻게 되어있을까 궁금해져 JPA 관련 검색을 해보니 대부분 글들이 영한님의 PDF 강의 자료로 이루어져 있었습니다.  

하지만 저는 강의나 책에 있는 기본적인 개념, 예제 말고 실제로 어떻게 코드로 구현되어 있고, 어떤 자료구조를 사용했을까 등등.. 궁금했습니다.

 

그래서 제가 궁금해서 왜? 라는 궁금증에 대한 해답을 스스로 찾아보고 정보를 공유하면 좋겠다고 생각이 들어 위해 삽을 들고 삽질을 시작했습니다. 

 

*현재 저는 실무자도 아니고 취준생이기 때문에 분석 실력은 살짝 감안하셔서 봐주시면 감사하겠습니다.

뭔가 이상하거나 틀린 부분이 있다면 댓글 남겨주시면 확인 후 반영하도록 하겠습니다!

 

 


JPA 그리고 Hibernate

 

앞서 들어가기 전에 JPA자바에서 객체-관계 매핑을 위한 사양 또는 인터페이스이며, Hibernate는 해당 사양의 구현체입니다.

즉, JPA는 일련의 규칙과 요구 사항을 정의하는 ORM용 표준 API인 반면, 하이버네이트는 JPA를 구현하고 추가 기능을 제공하는 ORM 프레임워크입니다. 따라서 하이버네이트는 JPA 사양을 구현하는 여러 ORM 프레임워크 중 하나이며 개발자는 특정 요구와 선호도에 따라 하이버네이트 또는 기타 JPA구현(DataNucleus, EclipseLink)을 선택할 수 있습니다. 

 

 


무작정 코드 뜯어보기

 

버전 정보는 org.hibernate:hibrenate-core: 5.6.10.Final  입니다.

 

가장 널리 사용되고, 인기 있는 구현체 Hibernate에 대해서 한번 파보도록 하겠습니다. 하이버네이트는 구현체니까 어떤 인터페이스를 구현하고 있는지 찾아봤습니다. 엔티티매니저를 통해 관리가 된다는 것을 토대로 검색을 해봤습니다.

 

 

javax.persistence 패키지의 EntityManager interface

persistence context와 상호 작용하는 데 사용되는 인터페이스입니다. EntityManager 인스턴스가 persistence context와 연결되어 있습니다. persistence context는 모든 persistent entity ID에 대해 고유한 엔티티 인스턴스가 있는 엔티티 인스턴스의 집합입니다. persistence context 내에서 엔티티 인스턴스와 해당 라이프사이클이 관리됩니다.
EntityManager API는 영구 엔터티 인스턴스를 생성 및 제거하고, 기본 키를 기준으로 엔터티를 찾고, 엔터티를 쿼리 하는 데 사용됩니다. 지정된 EntityManager 인스턴스에서 관리할 수 있는 엔티티 집합은 persistence unit에 의해 정의됩니다. persistence unit는 응용프로그램별로 관련되거나 그룹화된 모든 클래스 집합을 정의하며, 단일 데이터베이스에 대한 매핑에서 이 클래스 집합을 공동으로 배치해야 합니다.

 

익숙한 문구인 persistenec context 가 나오는군요! 위의 정의한 대로 구현체들이 구현이 되었음을 수 알 수 있습니다.

추가로 인터페이스 왼쪽의 화살표(↓)를 누르면 구현체들의 목록을 볼 수 있습니다.

 

구현체인  SessionImpl 내부로 들어가 봅시다!

 

 

SessionImpl Class

 

들어와 보니 Session의 구체적인 구현체라고 설명이 되어있고, 해당 클래스는 스레드에 안전하지 않다는 설명이 눈에 들어옵니다.

아래 필드에 선언된 변수를 보니 익숙한 이름 "persistenceContext" 도 보이고 익숙하지 않은 변수들도 많이 보입니다.

 

우선! Session의 구체적인 구현체라 하니 Session 은 무엇인지, 어떤 스펙인지 확인하러 가보겠습니다.

 

 

Session Interface

Java 응용 프로그램과 Hibernate 간의 기본 런타임 인터페이스입니다. 이것은 지속성 서비스의 개념을 추상화하는 중앙 API 클래스입니다. 세션의 생명 주기는 논리적 트랜잭션의 시작과 끝에 의해 제한됩니다.(긴 트랜잭션은 여러 데이터베이스 트랜잭션에 걸쳐 있을 수 있습니다.)

세션의 주요 기능매핑된 엔티티 클래스의 인스턴스에 대한 생성, 읽기 및 삭제 작업을 제공하는 것입니다.
인스턴스는 다음 세 가지 상태 중 하나로 존재할 수 있습니다:
1. transient: 영속 상태가 아님, 어떤 세션과도 연결되지 않음
2. persistent: 고유한 세션과 연결
3. detached: 이전에는 persistent였으며 세션과 연결되지 않았습니다

save(), persist() 또는 saveOrUpdate()를 호출하여 Transient instances를 persistent(영속화) 할 수 있습니다.
delete를 호출하여 Persistent instances를 일시적으로 변경할 수 있습니다. get() 또는 load() 메서드에서 반환되는 모든 인스턴스는 persistent입니다.

update() saveOrUpdate(), lock() 또는 replicate(복제)를 호출하여 Detached를 persistent으로 만들 수 있습니다.

transient 또는 detached 인스턴스의 상태는 SQL INSERT, DELETE, UPDATE에서 update, merge를 호출하여 새로운 영구 인스턴스로 영구화할 수도 있습니다.

persistent instances에 대한 변경 사항은 플러시 시간에 감지되며 SQL UPDATE가 생성됩니다. saveOrUpdate() 및 replicate()는 INSERT 또는 UPDATE가 생성됩니다. 구현자가 스레드 세이프가 되도록 의도된 것은 아닙니다.

대신 각 스레드/트랜잭션은 SessionFactory에서 자체 인스턴스를 가져와야 합니다.

세션에서 예외가 발생하면 트랜잭션을 롤백하고 세션을 삭제해야 합니다. 예외가 발생한 후 세션의 내부 상태가 데이터베이스와 일치하지 않을 수 있습니다.

 

 

번역을 보니 익숙한 설명이 많이 있지 않나요? 책 또는 강의에서 설명된 내용들이 클래스에서 어떻게 이루어져 있고 정의되어 있는지 직접 눈으로 보는 건 또 다른 느낌입니다.

 

인터페이스를 확인했으니 다시 구현체인 SessionImpl로 돌아가보겠습니다.

 

SessionImpl.class로 다시 돌아가보니... 해당 클래스는 총 3897 라인으로 이렇게 긴 코드를 작성할 일도 거의 없고, 이 장황한 코드를 어디서 어떻게 봐야 할지 감도 안 와서 기준을 정해서 찾아보려 했습니다.

 

책이나 강의를 보셨다면 영속성 컨텍스트의 장점이라고 알고 계실 것 같습니다. 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기지연, 변경 감지, 지연 로딩 이런 장점이 있는 것을 생각하면서 저의 관심사를 기준으로 6가지를 선정하여 조사를 하였습니다.

 

1. 쓰기지연저장소

2. PersistContext

3. Save

4. Find

5. Clear

6. Flush

 

코드가 장황하고 지루할 수 있지만 한번 보시는 것도 나쁘지 않을 것 같다고 생각을 합니다! 

 


1. 쓰기지연저장소는 어떻게 구현되어 있을까? 

 

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 DB에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아두고, 트랜잭션을 커밋할 때 모아둔 쿼리를 DB에 보내는데 이것을 트랜잭션을 지원하는 쓰기지연(transactional-write-behind) 이라 한다.

-영한님 JPA책 100P-

 

 

어떻게 쿼리를 모아두고 커밋직전에 모은 쿼리를 DB에 보낼 수 있을까?

순서도 Insert, update, delete로 보내야 할 텐데 어떻게 이게 가능하도록 만들었을까?

어떤 자료구조로 이루어져 있을까?  등등  책, 강의에 담기지 않은 내부 구현이 궁금했습니다.

 

하지만 구현체의 이름조차 모르니 처음에는 막연하게 SessionImpl에 선언된 필드를 살펴봤습니다.

 

 

SessionImpl Class

 

프로퍼티는 Map으로 설정된 설정값일 것 같고, persistenceContext 위에 선언된 ActionQueue 가 있어 우선 해당 클래스에 들어가 봤습니다.

 

 

이벤트와 관련된 작업 대기열을 유지 관리합니다.
작업 대기열은 세션 transactional-write-behind semantics의 일부로 대기 중인 DML 작업을 보관합니다.
DML 작업은 플러시가 데이터베이스에 대해 강제로 실행될 때까지 여기서 대기열에 저장됩니다.

 

 

아까 본 transactional-write-behind (쓰기지연) 그리고 DML 작업을 보관한다는 내용, 아래의 필드에 선언된 insert, delete, update를 보아 해당 클래스(ActionQueue)가 쓰기지연저장소임을 알았습니다.

 

 

ActionQueue 의 인스턴스 변수 중

 

쿼리랑 직접적으로 관련된 필드가 눈에 들어오는데 ExecutableList 타입으로 선언이 되어있습니다. 또 위의 설명을 보니 insert, update, delete 가 올바른 순서대로 수행돼야 한다고 적혀있습니다. 어떻게 이를 보장해 줄까요? 아직 그 의문은 사라지지 않았습니다.

 

ExecutableList 객체로 들어가 보겠습니다!

 

 

ExecutableList Class

각 Executable list와 관련된 상태의 특수 캡슐화.
executalbes 정렬 관리 (lazily)
list에서 executables의 영향을 받는 querySpaces를 관리하고 이를 캐시 합니다.

 

 

ExecutableList는 내부에 ArrayList를 선언하여 insert, update, delete를 리스트의 형태로 관리를 하는 객체입니다.

다시 ActionQueue 클래스로 돌아가서 조금만 휠을 내려보면 LinkedHashMap으로 선언된 EXECUTABLE_LISTS_MAP 이 보입니다.

 

 

ActionQueue Class

실행 순서에 삽입된 모든 ExecutableLists에 대한 providers가 포함된 LinkedHashMap

 

 

특이하게 LinkedHashMap으로 구현이 되어있습니다. 왜 LinkedHashMap 일까? 생각하면서 휠을 내려보니 금방 그 이유를 알았습니다.

 

 

ActionQueue - insert
ActionQueue - update
delete

 

 

Insert , Update, Delete 순서대로 LinkedHashMap에 put을 해줍니다. 입력된 순서대로 Key의 순서가 보장되는 자료구조이기 때문에 쿼리의 순서가 보장이 될 수 있었습니다. 그래서 insert, update, delete 가 올바른 순서대로 수행될 수 있었습니다!

 

 

그리고 JPA를 사용하다 보면 쿼리의 로그를 보신 적이 있을텐데 해당 쿼리가 PK 순서대로 나가는 로그를 보신 적이 있지 않았나요?

 

 

ActionQueue Class

 

 

ActionQueue(쓰기지연저장소)에서 정렬을 제공하기 때문에 작업을 하면서 따로 쿼리의 순서를 정렬하지 않아도 내부에서 이미 정렬을 해주는 사실을 알 수 있었습니다.  그렇다면 실제로 해당 메서드가 어디에 쓰이는지 사용되는 곳을 확인해 보겠습니다.

 

 

AbstractFlushingEventListener Class

 

 

엔티티를 Flush 할 때 사용이 되는 사실을 알 수 있고, 메서드 위의 설명을 보니 더티체킹이라는 문구도 보이네요. 뭔가 하나씩 연결되는 느낌입니다. 이 부분은 Flush를 조사할 때 다시 알아보도록 하겠습니다.

 

이렇게 살펴보니 쓰기지연저장소가 어떻게 구현되어 있고 어떤 기능을 제공하는지 어느 정도 파악할 수 있었습니다.

다른 기능이 궁금하면 저와 같은 방법으로 제일 궁금한 부분부터 찾아보시면 좋을 것 같습니다. 

 

ActionQueue(쓰기지연저장소)는 이 정도로 살펴보고 나머지 궁금한 부분들을 또 찾아보러 가겠습니다. 

 

 


2. PersistenceContext (영속성 컨텍스트) 내부구조 살펴보기

 

들어가기 전 개념 다시 보기

Persistence Context는 엔티티 인스턴스에 대한 변경 사항을 추적하고 필요한 경우 해당 변경 사항을 데이터베이스에 다시 전파하는 역할을 합니다. 엔티티 인스턴스가 Persistence Context 내에서 수정되면 EntityManager는 적절한 SQL 문을 자동으로 생성하여 데이터베이스의 해당 행을 업데이트합니다.
Persistence Context는 응용 프로그램이 모든 작업에 대해 데이터베이스와 직접 상호 작용할 필요 없이 관리 엔티티 인스턴스의 캐시로 작업할 수 있도록 해주기 때문에 JPA에서 중요한 개념입니다. 이렇게 하면 성능이 향상되고 응용프로그램의 비즈니스 로직을 실행하는 데 필요한 데이터베이스 쿼리 수가 줄어듭니다.

 

 

 

SessionImpl.class

 

이전 SessionImpl 클래스에서 ActionQueue 밑에 선언되어 있던 persistenceContext 내부는 어떻게 구현이 되어있는지 확인해보겠습니다.

 

 

StatefulPersistenceContext.class

 

 

영속성콘텍스트의 1차 캐시, 다들 아시겠지만 위의 그림처럼 Key, Value의 형태로 데이터가 저장이 되어있다고 보셨을 텐데 실제로 확인해 보니 HashMap으로 구현되어 있는 것을 볼 수 있습니다. Key값이 EntityKey라는 객체인데 알고 있는 배운 지식을 토대로 접근하면, 해당 객체는 Entity의 아이디를 저장을 해야 할 텐데 내부도 그렇게 이루어져 있을 것이라 추측을 해볼 수 있을 것 같습니다.

 

실제로 그렇게 구현이 되어있는지 확인해 보겠습니다.

 

EntityKey.class

식별자로 특정 세션에서 엔티티 인스턴스를 고유하게 식별합니다.
세션 범위 내에서만 안전하게 사용할 수 있습니다: 예를 들어 테넌트 ID는 동일성 정의의 일부로 고려되지 않습니다.

고유성을 결정하는 데 사용되는 정보는 엔티티 이름과 식별자 값으로 구성됩니다. (see equals)

성능 고려 사항: 이 유형의 많은 인스턴스가 런타임에 생성됩니다. 필수적인 것만 저장하여 각각의 것이 가능한 한 작아지도록 하십시오.

 

 

클래스의 설명과 더불어 생성자의 설명을 보면 "Construct a unique identifier for an entity class instance" 즉 엔티티 클래스 인스턴스의 고유 식별자를 생성한다고 적혀있습니다. 

 

 

다시 StatefulPersistenceContext 클래스로 돌아가서...

 

 

StatefulPersistenceContext.class 의 인스턴스 변수

 

 

위에서 확인한 정보를 토대로 1차 캐시, 프록시, 스냅샷에 사용되는 키는 엔티티의 고유 식별자를 가지며, HashMap의 자료구조를 사용하는 것을 직접 확인을 할 수 있습니다!

 

 

위 사진에 선언된 Key들의 역할을 조금 더 구체적으로 설명하겠습니다.

 

1. entityByKey:

  • 기본 키로 엔터티를 추적하는 데 사용됩니다.
  • Entity의 primary key로 키가 지정되며 해당 Entity 인스턴스를 포함합니다.

 

2. entityByUniqueKey:

  • entitiesByKey와 유사하지만 unique key(있는 경우)로 엔티티를 추적하는 데 사용됩니다.
  • unique key는 column의 각 값이 테이블의 모든 행에서 고유하도록 하는 제약 조건입니다.
  • entitiesByUniqueKey 맵은 고유 키 값으로 키가 지정되며 해당 엔티티 인스턴스를 포함합니다.

 

3. proxiesByKey:

  • 기본 키로 엔티티 프록시(또는 지연 로드된 엔티티 참조)를 추적하는 데 사용됩니다.
  • Entity의 primary key로 키가 지정되며 해당 프록시 인스턴스를 포함합니다.
  • 프록시는 실제로 필요할 때까지 엔터티 데이터의 로드를 지연하는 데 사용됩니다.

 

4. entitySnapshotsByKey:

  • 이 맵은 엔티티가 persistence context에 처음 로드될 때 엔티티의 원래 상태를 추적하는 데 사용됩니다.
  • Entity의 primary key로 키가 지정되며 Entity 상태의 스냅샷을 포함합니다.
  • Entity 어떤 변경 사항이 적용되었는지 확인하고 필요한 경우 변경 사항을 롤백하는 유용합니다.

 

 

이제 어떻게 동작하고, 이루어져 있는지는 알겠는데 그래서 1차 캐시에는 언제, 어떻게 저장이 되는 것일까 궁금해집니다. StatefulPersistenceContext 클래스를 탐색하다보면 아래와 같은 메서드를 발견할 수 있습니다. 

 

 

StatefulPersistenceContext.class 에서 제공하는 메서드

 

 

PersistenceContext에서는 addEntity라는 메서드를 제공해 주는데 말 그대로 해당 메서드는 1차 캐시에 엔티티를 저장하는 메서드입니다. 이 메서드를 통해서 알 수 있는 사실이 있습니다. 1차 캐시에는 엔티티의 고유 식별자가 Key로 등록이 됩니다.

 

엔티티를 식별자로 조회하거나, 엔티티의 특정 필드값으로 조회를 해도 결국 1차캐시에는 엔티티의 고유 식별자가 Key 로 등록이 되는 것입니다.

 

TwoPhaseLoad Class

 

 

아래에서 추후 설명드리겠지만 짧게 설명을 드리면, 조회의 과정에서 1차 캐시에 없을 경우 결국 DB에서 조회를 하게 되는데 해당 row를 읽어오면, 위의 메서드가 실행되어 1차 캐시에 등록을 하게 됩니다.

 

여기까지 알게 된 사실로 영속성 컨텍스트의 이점 중 하나인 동일성 보장을 설명할 수 있을 것 같습니다.

 

 

영속 엔티티의 동일성 보장 - 영한님 PDF 중

 

 

많이 보셨을 내용일 것 같습니다. 동일성 보장이 왜 가능하게 되는지 직접 코드를 파헤쳐봤기 때문에 더 선명하게 그림이 그려질 것 같습니다.

 

정리를 하면 첫 번째 조회에서는 1차 캐시에서 가져올 대상이 없어 Entity의 고유 식별자가 1차 캐시에 Key로 등록이 되고 Value로는 해당 Entity가 들어가게 됩니다.  두 번째 조회에서는 해당 엔티티의 고유식별자(Key)를 사용해서 1차 캐시(Map)에서 가져오기 때문에 같은 Entity(Value)를 반환하게 되어 동일성이 보장됩니다.

 

 

제가 정리한 내용 외에 PersistenceContext 클래스에서 제공하는 더 많은 필드와 메서드가 있습니다.

마음 같아서 다 정리하고 싶지만 다음 파트도 있으니 우선 여기까지 하고 넘어가 보도록 하겠습니다!

 


간단하게(?) 스프링 데이터 JPA

스프링 데이터 JPA는 JPA를 정말 편리하게 사용하도록 도와주는 기술입니다. 

저도 프로젝트를 진행할 때 스프링 데이터 JPA를 사용을 했습니다. 많은 분들도 사용을 했을 것이라 생각을 하여 Save, Find 등등 이 어떻게 동작하는지 들어가기 전에 보면 도움이 되지 않을까 해서 적어보았습니다.

 

 

Example
Example

 

 

스프링 데이터 JPA 를 사용했다면 위와 같이 JpaRepository 인터페이스를 extends 하여 사용해야 합니다.

Save, Find, Delete... 등등 동작을 따로 정의하지는 않았지만 사용이 가능했습니다. 실제 내부에서는 어떻게 구현되어 있고 동작하는지 알아보겠습니다. 

 

우선 JpaRepository 인터페이스의 상속관계를 다이어 그램을 보면 아래와 같습니다.

 

JpaRepository 상속관계

 

 

JpaRepository는 다양한 인터페이스를 확장하여 사용하고 있습니다.

 

Repository Interface

중앙 리포지토리 마커 인터페이스입니다. 관리할 도메인 유형과 도메인 유형의 ID 유형을 캡처합니다. 일반적인 목적은 유형 정보를 보유하고 클래스 경로 검색 중에 이 정보를 확장하는 인터페이스를 검색하여 스프링 빈을 쉽게 만드는 것입니다.

이 인터페이스를 확장하는 도메인 리포지토리는 단순히 CrudRepository에서 선언된 것과 동일한 서명의 메서드를 선언함으로써 CRUD 메서드를 선택적으로 노출할 수 있습니다.

Type parameters: <T>  -  리포지토리가 관리하는 도메인 유형
                               <ID> -  리포지토리가 관리하는 엔티티의 ID 유형

 

 

JpaRepository를 사용할 때 제네릭 타입을 설정하는 규칙이 해당 인터페이스에 지정되어 있는 것도 확인이 가능합니다.

 

 

코드를 삽질하다보면 모르는 용어가 많이 나오는데 Central Repository Marker Inferface(CRMI) 라고 표현이 되어있어 여러분들도 궁금해하실 것 같아 제가 미리 찾아왔습니다.

 

CRMI(Central Repository Marker Interface)는 특정 클래스가 중앙 저장소임을 나타내기 위해 Java 개발에 사용되는 마커 인터페이스입니다. CRMI 인터페이스는 표준 자바 API의 일부는 아니지만 데이터를 관리하고 저장하기 위해 중앙 저장소에 의존하는 엔터프라이즈 자바 애플리케이션에서 종종 사용됩니다.

CRMI 인터페이스의 목적은 모든 중앙 저장소 클래스가 구현해야 하는 공통 메서드 집합을 정의하는 것입니다. 이러한 방법에는 저장소의 데이터를 추가, 제거 및 쿼리 하는 작업이 포함될 수 있습니다.

CRMI 인터페이스를 구현하면 클래스를 중앙 저장소로 쉽게 식별할 수 있으므로 다른 클래스와 구성 요소가 쉽게 상호 작용할 수 있습니다. 또한 마커 인터페이스를 사용하면 중앙 저장소를 사용하는 응용 프로그램의 여러 부분에 걸쳐 일관성과 표준화를 보장할 수 있습니다.

CRMI 인터페이스는 엔터프라이즈 Java 애플리케이션에서 데이터를 구성하고 관리하는 데 유용한 도구이지만 Java 개발에 필수적이거나 필수적인 구성 요소는 아닙니다.

 

 

다음은 CrudRepository 인터페이스 입니다.

 

CrudRepository Interface

특정 유형의 리포지토리에서 일반 CRUD 작업을 위한 인터페이스입니다.

 

크게 설명이 필요 없는 인터페이스인 것 같습니다. 편하게 사용했던 CRUD에 관한 기능들이 정의되어 있습니다.

여기서 궁금한 점은 @NoRepositoryBean 은 왜 붙어있을까? 궁금하여 마찬가지로 검색을 해봤습니다. 

 

이 어노테이션은 Spring Data 프로젝트에서 특정 인터페이스가 저장소 빈으로 인스턴스화되지 않아야 함을 나타내기 위해 사용됩니다.

Spring Data 프로젝트는 저장소 인터페이스를 검색할 때 일반적으로 기본 저장소 인터페이스를 확장하는 각 인터페이스에 대해 빈을 생성합니다. 이 기능은 여러 리포지토리에서 공유하려는 공통 메서드 또는 기능 집합이 있는 경우 유용합니다.

그러나 빈으로 인스턴스화되지 않는 중간 인터페이스가 있는 경우 @NoRepositoryBean으로 주석을 달 수 있습니다. 이렇게 하면 Spring Data가 해당 인터페이스에 빈을 생성하는 것을 방지할 수 있으며, 해당 메서드는 해당 인터페이스를 확장하는 구체적인 리포지토리 인터페이스에서만 사용할 수 있습니다.

 

한마디로 실제 사용되는 Repository 가 아니고 기능을 정의하기 위한 Repository 임을 나타내는 어노테이션입니다.

 

 

PagingAndSortingRepository Interface

페이지 분할 및 정렬 추상화를 사용하여 엔티티를 검색하는 추가 메서드를 제공하는 CrudRepository의 확장입니다.

 

 

 

QueryByExampleExecutor Interface

이 인터페이스는 Exmaple 인스턴스를 기반으로 쿼리를 실행할 수 있는 메서드를 정의합니다.

 

 

그래서 JpaRepository를 상속하여 사용하기만 해도 Save, Find 등등 모든 기능들이 사용이 가능했던 것입니다.

그 외 사용방법은 책, 강의를 통해 배우면 더 쉽고 빠르게 배울 수 있기에 여기서는 정리하지 않았습니다. 

이제 본격적으로 제가 궁금했던 Save, Find.. 등등 메서드의 동작이 어떻게 되는지 확인해 보도록 하겠습니다.

 

 


Entity를 Save 할 때 내부에서는 어떻게 동작을 할까?

(*해당파트는 스프링데이터 JPA을 사용한다는 전제로 설명드리겠습니다.)

 

JPA는 엔티티를 저장할 때 데이터베이스에 해당 엔티티가 있는지 없는지 어떻게 알고 insert와 update를 할까?

내부에 어떻게 코드로 구현되어 있길래 궁금해서 찾아보게 되었습니다.

 

 

위에 설명한 대로 JpaRepository는 다양한 인터페이스를 확장한 인터페이스입니다.

 

example
example

 

JpaRepository를 확장하여 사용하면 구현 클래스 없이 인터페이스만 작성해도 쉽게 개발이 가능해집니다.

해당 구현체는 스프링 데이터 JPA 가 생성을 해서 주입을 해주게 된다고 하는데 그 구현체가 무엇인지 확인해 보겠습니다.

 

CrudRepository Interface

 

제가 만든 repository의 save 메서드를 들어가 보니 전에 설명한 CrudRepository 인터페이스가 보입니다 왼쪽 화살표를 눌러 구현체를 확인해 보면 아래와 같이 두 가지가 있습니다.

CrudRepository Interface - save 구현체 목록
Debugger 화면

 

 

실제 User를 Save 할 때 디버깅을 화면의 스택을 보면 SimpleJpaRepository를 사용하고 있습니다. 따라서 해당 클래스로 가서 확인해 보겠습니다.

 

SimpleJpaRepository class

 

 

JPA가 Entity에 대하여 insert와 update를 알아서 해주는 답이 여기에 있었습니다.

isNew 메서드를 통해 Entity가  존재하는지 안 하는지 체크하여 persist를 할지 merge 구현되어 있습니다.

그럼 isNew 메서드는 어떻게 Entity를 체크할까요? 계속해서 디버깅을 해보았습니다.

 

AbstractEntityInformation class

 

isNew의 메서드는 Entity의 Id를 확인하여 Null or 0 인지 판단합니다. 

Entity의 ID가 Null 또는 0이라면 새로 만들어진 엔티티 이기 때문에 em.persist를 하게 되는 것입니다.

만약 조회를 하여 변경을 했다면 해당 엔티티에는 getID를 했을 때 ID 가 있으니까 merge를 하게 됩니다.

 

persist가 된다면 내부 이벤트를 발행하여 JPA의 구현체 Hibernate 가 처리하게 됩니다. 

그러면 어떻게 처리되는지 보여드리겠습니다. 

 

SessionImpl class
SessionImpl class

 

이벤트를 수신하는 측에서는 아래와 같이 엔티티의 상태를 체크하여 각각의 상태에 맞는 메서드를 호출하게 됩니다.

새로운 엔티티니까 상태는 TRANSIENT 입니다. ( 영속 상태가 아님, 어떤 세션과도 연결되지 않음 ) 

 

DefaultPersistEventListener class
DefaultPersistEventListener class

 

엔티티의 ID를 생성하여 저장합니다. 그 후에 이제 ActionQueue(쓰기지연저장소)에 쿼리를 저장을 하고 PersistContext에 엔티티를 등록합니다.

 

AbstractSaveEventListener class

 

ActionQueue class

 

ActionQueue class

 

ActionQueue Class

 

AbstractEntityInsertAction class
StatefulPersistenceContext class
StatefulPersistenceContext class

 

이렇게 해서 save를 호출하면 PersistContext에 Entity가 등록되는 하나의 과정을 보았습니다.

개발자가 간편하게 사용할 수 있게 얼마나 많은 일들을 해주는지 알 수 있었습니다.

여기서  flush 가 일어나게 되면 쿼리를 데이터베이스로 날리게 되는데 이 과정은 따로 flush 파트에서 설명하도록 하겠습니다.

 

 


1편을 마치며

아무래도 내부코드의 동작과정을 보여드리다 보니 설명이 부족한 점이 있었는데 내용을 좀 더 보충하도록 하겠습니다!

분량이 많아서 나머지 파트는 Find, Clear, Flush 할 때 내부에서 어떻게 동작하는지는 2편을 제작하도록 하겠습니다.

 

부족하고, 정말 긴 글 읽어주셔서 감사합니다.

댓글로 편하게 피드백을 주신다면 빠르게 반영하도록 하겠습니다!

 

 

Contact : audrn6689@gmail.com

 

[참고]

자바 ORM 표준 JPA 프로그래밍 - 김영한

 

 

 

 

'스프링' 카테고리의 다른 글

[HTTP] 로그아웃은 "GET" or "POST" ??  (0) 2022.07.12
assertJ - 공식문서 기반 간단 정리  (0) 2022.07.08
Builder Pattern (빌더 패턴)  (0) 2022.06.28
XSS 와 CSRF  (0) 2022.06.05
서블릿(Servlet)  (0) 2022.05.31