자바 동적 프록시 기술을 알고 읽으시는 게 좋습니다!!!
JPA를 처음에 배우면 엔티티 매니저는 여러 스레드가 동시에 접근하므로 스레드 간 공유하면 절대 안된다고 학습한다.
그러나 스프링 부트 애플리케이션을 돌려보면 처음에 엔티티 매니저가 스프링 빈으로 등록되는 것을 확인할 수 있다.
'스프링 부트 자동 구성을 통해 엔티티 매니저가 스프링 빈으로 등록, 즉 싱글톤으로 사용이 되는 거 같아'
위와 같은 의문점이 들었고, 이에 관련해서 구글에 찾아보기 시작했다.
결론을 말하면 스프링 빈으로 등록되는 것은 엔티티 매니저 가짜 객체고 실제 애플리케이션에서 로직이 수행될 때 진짜 엔티티 매니저가 사용된다는 것이다. 이러면 쓰레드 세이프하다고 말할 수 있다.
단순히 그렇구나 하고 넘어간 것을 오늘은 눈으로 확인해보려고 한다.
작성한 초간단 테스트는 아래와 같다. 엔티티 매니저 인스턴스 이름을 em이라고 정하고 진행했다.
@SpringBootTest
public class Tests{
@Autowired
EntityManager em;
@Transactional
@Test
void test(){
Survey e = SurveyFactory.create(); //엔티티 생성
em.persist(e);
}
}
테스트 코드 em.persist()에서 디버깅을 시작해보자.
그럼 맨 처음 아래와 같은 디버깅 창을 확인할 수 있다.
h 인스턴스 변수는 자바 동적 프록시 클래스 에서 InvocationHandler를 가리키는 변수다.
현재 상황을 정리하면 스프링 빈으로 등록된 엔티티 매니저는 프록시로 등록되고, 프록시가 어떤 InvocationHandler를 호출하는 것이다.
그 어떤 Invocation Handler가 바로 SharedEntityManagerCreator 클래스 내부에 있는
private static 클래스 SharedEntityManagerInvocationHandler다.
일단 SharedEntityManagerCreator와 SharedEntityManagerInvocationHandler를 정리해보자.
SharedEntityManagerCreator
- EntityManagerFactory로부터 공유 가능한 EntityManager를 만들기 위해 사용되는 대리자를 의미한다.
- 현재 트랜잭션 EntityManager에 대한 모든 호출을 위임하고, 그렇지 않은 경우에는 각각의 작업마다 새로운 EntityManager를 생성한다.
- 공유 트랜잭션 EntityManager의 동작 방식에 대한 행동 정의는 javax.persistence.PersistenceContextType.TRANSACTION과 JPA 명세서에서 논의하는 대로 작동한다.
- 애노테이션 기반 javax.persistence.PersistenceContext.type()의 기본값으로 사용된다.
SharedEntityManagerInvocationHandler
- 현재 트랜잭션이 존재하면 모든 호출을 해당 트랜잭션에 속한 EntityManager로 위임한다.
- 만약 그렇지 않으면 각각의 작업마다 새로운 EntityManager를 생성하는 호출 핸들러(Invocation handler)를 말한다.
- 즉, 핸들러는 EntityManager의 wrapper 역할을 하며, EntityManagerFactory를 사용하여 EntityManager를 생성한다.
- 현재 트랜잭션이 활성화된 경우, 핸들러는 해당 트랜잭션에 속한 EntityManager를 반환한다.
- 그렇지 않은 경우, 핸들러는 새로운 EntityManager를 생성하여 각각의 작업에 사용한다.
개념을 정리했으니 디버깅을 계속 진행해보자. InvocationHandler의 invoke 메서드를 따라가자.
그럼 아래와 같은 구간에서 멈춘다.
참고로 현재 targetFactory도 InvocationHandler지만 내부에 타겟은 LocalContainerEntityManagerFactoryBean 인스턴스다.
그럼 다시 내부를 탐색해보자.
위 코드에서 트랜잭션 동기화 매니저에서 EntityManager를 가지고 있는 EntityManagerHolder 인스턴스를 반환한다.
이 인스턴스에 EntityManager 구현체인 SessionImpl이 들어가있다.
여기에서 우리가 원하는 진짜 EntityManager 구현체를 가져온 것이다.
타고 온김에 EntityManger가 쿼리를 실행하는 부분이 궁금해졌다.
계속해서 persist() 메서드를 호출하는 부분을 찾아보자.
코드를 타고 내려오면서 method.invoke() 내부로 들어간다.
그럼 SessionImpl의 클래스의 persist 메서드에 도달한다.
사실 여기까지 도달하면 끝났다고 생각할 수 있지만, 계속 파고 들어가봤다.
그러다가 아래와 같이 saveWithGeneratedId라는 메서드를 만났다.
이 메서드를 타고 들어가면 먼저 임시 엔티티를 만들어서 변경 감지를 위한 추적을 세팅해주고, 엔티티에 할당할 id를 먼저 생성한다.
그리고 내부를 더 타고 들어가다보면 영속성 컨텍스트에 엔티티를 저장하는 코드가 나온다.
이후에 엔티티를 DB에 저장하기 위한 코드가 나온다. addInsertAction() 메서드를 살펴보겠다.
addInsertAction에서 엔티티 저장과 관련된 정보가 담긴 EntityInsertAction을 만들고,
이 액션을 EntityManager의 구현체인 SessionImpl의 ActionQueue에 insert 이벤트를 넣어두는 것이다.
이제 flush 흐름을 따라가면 엔티티, 콜렉션을 더티 체킹과 같은 flush와 연관 있는 코드가 나온다.
ActionQueue에서 insert 이벤트를 꺼내서 실행한다. 코드는 아래와 같다.
actionQueue.executeActions();
그럼 이제 데이터베이스에 반영이 된다.
참고
현재는 EntityManger의 메서드인 persist 내부를 확인하는 중이다.
EntityManager 인스턴스 생성은 테스트 코드를 처음 시작할 때 초기화하는 과정을 확인했다.
이렇게 Spring Bean으로 등록되는 EntityManager와 EntityManager가 쿼리를 실행하는 내용을 살펴보았습니다.
틀린 부분이나 피드백 환영입니다!!! 감사합니다.
댓글