카테고리 없음

[JPA] EntityManager와 트랜잭션의 관계

연유뿌린빙수 2025. 10. 14. 05:21

스프링 프레임워크에서 직접적으로 EntityManager를 조작하는 경우가 많지는 않다.
일반적으로는 JpaRepository를 이용하기 때문에 직접적으로 데이터베이스에 대하여 작업하지는 않는다.

그래도 @PersistenceContext 어노테이션을 통하여 EntityManager를 코드에 주입받아 사용을 하기도한다.



EntityManager는 프록시

스프링에서 @PersistenceContext 를 통해 주입받는 EntityManager는 실제의 EntityManager의 인스턴스가 아니라 프록시 객체이다.
이 프록시 객체는 스레드 로컬(Thread Local)변수를 사용하여 각 트랜잭션 마다 서로 다른 EntityManager 인스턴스를 제공한다.

@PersistenceContext
private EntityManager em;

 

위와 같이 EntityManager를 주입받은 후에 다음과 같은 방식으로 사용이 가능하다.

em.getReference();




그러면 Transaction과는 어떤 관계?

다음은 가장 기본적인 형태의 트랜잭션 코드 예시이다.

@Transactional
public void updatePerson(Long id, String name) {
	
    Person found = personRepository.findById(id).orElseThrow();
    person.setName(name);
    personRepository.save(person);
}

이 메서드를 실행할때마다 스프링에서는 Transaction이 새로 열린다.

 

이에 대하여 EntityManager는 트랜잭션이 새로 생성될 때마다

 

해당 트랜잭션에 연결된 새로운 EntityManager 인스턴스를 생성한다.
그리고 이에 대하여 Transactio이 종료되어 커밋이 완료되면 자연스럽게 EntityManager도 닫힌다.

 

도식화하면 다음과 같다.

특정 스레드에서 Service를 실행하면서 Transaction이 실행되면,
이에 대한 EntityManager도 자연스럽게 인스턴스가 생성되고,
그리고 transaction이 종료되어 커밋될 때 자연스럽게 사라지는 것이다.




그렇다면 스레드가 다를 때에는?

위의 설명 내용은 가장 일반적인 동기식 패턴이었다.

 

그렇다면 비동기에서는 스레드가 다를텐데 어떻게 될까?

 

\이 경우, EntityManager가 트랜잭션과 스레드에 종속되어 있기 때문에 기존 트랜잭션 컨텍스트를 그대로 공유할 수 없다.

 

@Transactional
public CompletableFuture<?> updatePerson(Long id, String name) {

    // 생략
    CompletableFuture.runAsync(serviceB::getName);
}

예를 들어, 처음에 ServiceA에서 해당 코드를 호출했다고 해보자.


이 시점에서 서비스 내부에서 트랜잭션이 시작되며
사용 중인 스레드(threadA)의 ThreadLocal에는 EntityManager가 저장된다.

 

 

그런데 서비스 메서드 내부에서 비동기 메서드를 실행한다고 하자
위의 코드처럼 메서드 내부에서 CompletableFuture.runAsync()를 호출하면
이 비동기 작업은 새로운 스레드에서 실행된다.(이걸 B라고 해보자)

 

 

문제는 이 새로운 스레드에는 기존 스레드의 ThreadLocal 값이 공유되지 않는다는 점이다.

새로운 Thread에는 어떤 값도 전달이 안된다.

 

따라서 이 스레드에서 EntityManager를 사용하려 하면, ThreadLocal에 아무 값도 존재하지 않기 때문에 즉시 오류가 발생하게 된다.

 

도식화하자면 다음과 같다.

기존의 Service 코드가 ServiceA이고, 새로운 스레드를 생성하면서 외부의 ServiceB를 호출하면서 비동기 작업을 하면,
해당 스레드는 새로 생성된 것이기 때문에 기존의 EntityManager를 사용할 수 없다.

 

 

그렇다면 어떻게 처리해주면 될까??

간단하다.

Transaction의 시점을 비동기 시점에 적용해주면 된다.

@Async
@Transactional
public void doAsyncTask() {
    // 새 스레드지만 새 트랜잭션을 자동으로 시작함
    entityRepository.save(...);
}

이렇게 비동기 스레드에 대해서도 Transactional을 별도로 연다면 새로 EntityManager가 생성되어 정상적으로 동작된다.
(스프링이 트랜잭션을 만나면 같은 스레드면 재사용하고, 아니면 바로 @Transactional에 대하여 알아서 생성해주기 때문에 트랜잭션 무한호출의 경우는 걱정하지 않아도된다!)


혹은 세션을 최대한 분리하고 부모 스레드의 EntityManager는 건드리지 않은 채 완전히 분리해서 작성하는 것도 하나의 방법이다.

위의 경우를 도식화 하면 다음과 같다.

EntityManager가 새로운 스레드와 트랜잭션에 대하여 새로 생성되는 것이다.



 

 

결론

EntityManager는 트랜잭션과 스레드에 종속된 프록시 객체이며,
동기 트랜잭션에서는 자동으로 관리되지만,
비동기 작업에서는 새로운 트랜잭션을 명시적으로 열어야 정상적으로 동작한다.

 

 


REF

- https://gunkim.github.io/2024/07/20/entity-manager/