Kim Jinung
JPA Architecture 본문
1. JPA(Java Persistence API)
JPA는 자바 진영의 표준 ORM 기술이다. 자바 진영이 늘 그렇듯이 그 자체로 구현체를 제공하지 않고 인터페이스를 제공하고 다양한 구현체가 존재한다. 대표적으로 Hibernate가 있다.
2. JPA architecture
JPA는 생각보다 아키텍처에 대한 자료를 찾아보기가 힘들다.
클래스 레벨 아키텍처에서 중요한 클래스들은 Persistence, EntityManagerFactory, EntityManager, EntityTransaction, Query가 존재한다. 내부 코드를 뜯어보면 Persistence 클래스가 EntityManagerFactory 객체를 생성하고, EntityManagerFactory 클래스가 EntityManager를 팩토리 패턴으로 인스턴스를 생성한다. (다만 스프링은 persistence 객체가 직접 생성하지 않고 application.yml 파일에 Hibernate ORM을 설정한다.)
순서대로 하나씩 뜯어본다.
Persistence
Persistence 클래스는 EntityManagerFactory를 생성하는 스태틱 메서드를 포함하고 있다. 두 번째 스태틱 메서드에서 PersistenceProviderResolverHolder 클래스로부터 PersistenceProviderResolver 인스턴스를 받아온다. 그리고 받아온 인스턴스에서 JPA 구현체인 provider 목록을 받아온다. Persistence 객체를 사용해서 엔티티 매니저 팩토리를 생성하는 경우 가장 처음 찾은 구현체를 생성하고 반환한다.
Entity Manager Factory
EntityManagerFactory는 EntityManager를 생성해주는 역할을 담당한다.
Entity Manager
EntityManager 인터페이스에는 실제 디비와 상호작용을 위한 메서드에 대한 명세가 위치하고 있다. JPA를 사용하여 객체를 디비에 저장하는 경우 EntityManager 인스턴스에게 처리할 객체를 넘기게 되는 이유다.
이제 하이버네이트 구현체를 직접 확인 해본다.
Hibernate
하이버네이트 구현체만 보고 전체 맥락을 이해하기가 쉽지가 않다. 한 가지 예시를 가지고 JPA가 작동하는 흐름을 살펴본다. 우선 JPA의 모든 작업이 하나의 트랜잭션 범위 내에서 일어나는 점을 알고 있어야 한다.
다음과 같은 Service layer, 그리고 repository layer를 기반으로 정리해본다.
먼저 리포지토리는 스프링 부트로부터 엔티티 매니저 빈을 주입받아서 사용한다. 단순히 Member 타입의 객체를 받아서 persist하는 것이 전부다. 서비스는 스프링 부트로부터 리포지토리 빈을 주입받는다. 서비스 레이어는 saveMember 메서드에 Member 타입 객체를 넘겨주면 주입받은 리포지토리의 메서드를 이용해서 엔티티를 저장한다.
임의의 컨트롤러에서 saveMember 메서드를 실행하면, @Transactional 어노테이션에 의해 트랜잭션의 범위가 시작된다. 이때 트랜잭션을 관리하는 것은 TransactionManager 객체다. 해당 객체는 스프링 컨테이너에 빈으로 등록되어 있는 EntityManagerFactory를 통해 EntityManager 인스턴스를 생성한다. 이 인스턴스가 리포지토리에 주입된다. 즉 하나의 트랜잭션 범위 안에서 엔티티 매니저 인스턴스를 생성하고 사용 후에는 해당 인스턴스를 파괴한다.
saveMember 메서드가 호출되어 repository의 save 메서드를 실행한다. save 메서드는 스프링 부트가 주입 해준 엔티티 매니저 인스턴스를 가지고 아규먼트를 persist 메서드에 넘긴다. 이제 persist 메서드 내부에서 어떤 일이 일어나는지 확인한다.
Session Impl
하이버네이트는 EntityManager를 인터페이스를 상속하는 HibernateEntityManager 인터페이스를 가지고 있다. 그 아래로 HibernateEntityManagerImplementor, SessionImpl 구현체가 있다. SessionImpl 클래스는 EntityManager에서 명세하고 있는 메서드를 구현하고 있다.
위 코드에서 보이는 persistenctContext 인스턴스가 엔티티의 영속성을 관리하는 PersistenceContext다. 먼저 persist 메서드를 실행할 때 발생하는 과정을 따라가보면
persist & fire Persist method
persist 메서드에 저장할 객체를 넘기면 firePersist 메서드를 호출한다. 트랜잭션 싱크 등을 확인하고 onPersist에 이벤트를 넘긴다.
on Persist method
onPersist 메서드에서는 event 파라미터의 오브젝트가 하이버네이트 프록시 인스턴스인지 그리고 지연 로딩에 해당하는지 확인한다. 주로 전처리 및 예외 핸들링 작업을 하는 메서드로 보인다. 해당 메서드를 타고 내려가다보면 persistenctContext로부터 entity의 상태를 확인하고, 스위치문에 태워서 entityIs ~ 메서드를 호출한다.
entity Is methods
entityIs를 접두사로 가지는 메서드들은 PersistenceContext에 엔티티를 넘겨주고 상태에 따라 처리한다. 즉 persist 메서드를 실행하면 PersistenceContext에 해당 엔티티를 넘기고 PersistenceContext 인스턴스는 엔티티의 상태를 지속적으로 관리하는 역할을 담당한다. 엔티티 매니저는 엔티티를 넘겨받고, 영속성 컨텍스트는 엔티티의 상태를 지속적으로 체크한다.
요약해보면,
엔티티 매니저는 내부에 영속성 컨텍스트(Persist Context)를 두고있고, 해당 영속성 컨텍스트가 엔티티의 라이프 사이클을 관리한다.
Statueful Persistence Context class
해당 클래스는 하이버네이트에서 제공하는 PersistenceContext 구현체다. 생성자에는 entitiesByKey, entitySnapshotByKey 등의 필드를 해쉬맵 타입으로 가지고 있는 것을 확인할 수 있다. 해당 필드가 아마도 실제로 엔티티를 저장하는 역할을 할 것이다.
지금까지 과정을 요약해보면, persist 메서드에 저장할 객체를 넘기면 EntityManager 인스턴스가 요리조리 상태를 살펴보고, 필드로 가지고 있는 PersistenceContext에 해당 엔티티를 넘긴다. 그런데 직접적으로 디비와 상호작용하는 코드는 어디에도 없었다.
그렇다면 언제 디비에 저장할까?
다시 처음으로 코드로 돌아가서.. 리포지토리의 em 인스턴스에 엔티티를 넘기면 리포지토리 레이어는 정상 수행된다. 이후 서비스 레이어에서 호출한 saveMemer 메서드도 정상적으로 호출되고 종료될 것이다.
@Transactinal 어노테이션은 클래스의 메서드가 정상적으로 수행되고 종료되면 트랜잭션 매니저를 통해 커밋을 호출하고, 문제가 발생하면 롤백을 수행한다. 이를 통해 데이터 정합성을 보장한다. 하나의 서비스 단위로 데이터의 변동이 일어나는 일이 잦으므로 서비스 레이어에서 트랜잭션의 범위를 정의한 이유이기도 하다. 다시 본론으로 돌아가서, 서비스 레이어의 메서드가 정상 호출되었고 종료된다. 그러면 트랜잭션이 종료되고 트랜잭션 매니저가 커밋을 수행한다. 이때 트랜잭션 매니저는 엔티티 매니저 팩토리를 통해 생성한 엔티티 매니저 인스턴스를 가지고 영속성 컨텍스트(PersistenceContext)의 변경 사항을 체크한다. 변경 사항이 있는 경우 엔티티 매니저의 flush 메서드를 호출한다.
flush method
flush 메서드는 doFlush를, doFlush는 이벤트 리스너에 onFlush 실행하라고 넘긴다.
on Flush method
onFlush 메서드 내부에서는 flushEverythingToExecutions 메서드를 실행한다. flush session을 준비하는 단계다. 다음으로 performExecutions 메서드를 실행한다. 해당 메서드는 내부에서 쿼리 제너레이터를 호출하고, 디비에 변경사항을 반영하는 메서드다. 마지막으로 psotFlush 메서드를 실행해서 PersistenceContext 인스턴스와 관련된 사항을 정리한다.
performExecutions method
performExecution 메서드에서는 사전 작업을 먼저 수행한다. setFlushing 메서드에 true값을 주는 이유가 아마 Lock을 걸기 위한 사전작업으로 추측된다. 다음으로 perpareActions 메서드 그리고 excuteActions 메서드를 수행한다. 해당 부분이 쿼리 제너레이터를 이용해서 쿼리를 생성하고 디비에 반영하는 지점이다.
마지막으로 setFlushing 메서드를 통해 값을 토글하고 종료한다.
Summary
1. 트랜잭션이 시작되면 트랜잭션 매니저는 엔티티 매니저 팩토리로부터 엔티티 매니저 인스턴스를 받는다.
2. 엔티티 매니저 인스턴스를 통해 영속성 컨텍스트에 엔티티를 캐싱한다.
3. 모든 작업이 성공적으로 완료되고 트랜잭션이 종료되면 트랜잭션 매니저가 엔티티 매니저에게 더티 체킹을 수행하게 한다.
4. 엔티티 매니저는 영속성 컨텍스트의 스냅샷을 확인해서 변경 사항을 확인한다.
5. 변경 사항이 존재하는 경우 엔티티 매니저의 flush 메서드를 수행한다.
6. flush 메서드 내부에서는 쿼리 제너레이터 객체를 사용해서 쿼리를 생성하고 스테이트멘트를 생성해서 디비에 반영한다. (JDBC를 사용한다.)
'Computer Science > Database' 카테고리의 다른 글
DB 이론 (0) | 2023.07.27 |
---|---|
[JPA] Entity Mapping (0) | 2023.05.23 |
DBMS Architecture (0) | 2023.05.15 |
JDBC 그리고 ORM (0) | 2023.04.18 |
Flyway: database migration (0) | 2022.11.16 |