EntityManager(EM)는 JPA의 영속성 컨텍스트 핸들이지만 스레드 안전하지 않다. 그래서 "트랜잭션마다 하나를 열고, 같은 트랜잭션 안에서는 그것을 공유하고, 끝나면 닫는다"는 규칙을 누군가 강제해야 한다. JPA 스펙은 컨테이너가 이를 해 주는 것을 전제하는데(@PersistenceContext로 주입되는 "공유 트랜잭션 EM"), Spring은 컨테이너 없이도 이 의미론을 제공한다.
이 챕터는 그 메커니즘 — 공유 EM 프록시, 확장 EM 프록시, 스레드 바인딩 — 를 다룬다. 관련 타입은 SharedEntityManagerCreator, ExtendedEntityManagerCreator, EntityManagerFactoryUtils, EntityManagerHolder, 그리고 support 패키지의 PersistenceAnnotationBeanPostProcessor다.
┌─────────────────────────────────────────────────────────────┐
│ EntityManagerFactoryUtils (정적 헬퍼) │
│ doGetTransactionalEntityManager(emf, props, synced) │
│ → TransactionSynchronizationManager에서 EMF로 Holder 조회 │
│ → 없으면 새 EM 생성 + 동기화 등록 + 스레드 바인딩 │
│ convertJpaAccessExceptionIfPossible() → DataAccessException │
└─────────────────────────────────────────────────────────────┘
▲ 사용 ▲ 사용
│ │
┌──────────────────────┐ ┌─────────────────────────────────┐
│ SharedEntityManager- │ │ ExtendedEntityManagerCreator │
│ Creator │ │ application-managed / │
│ "공유(트랜잭션 스코프)"│ │ container-managed "확장 EM" │
│ 프록시. 호출마다 현재 │ │ 프록시. EM이 트랜잭션에 │
│ 트랜잭션 EM 위임, │ │ join/자동참여, 수명은 길게 유지 │
│ 없으면 일시 EM 후 close│ │ (스코프 객체에 묶임) │
└──────────────────────┘ └─────────────────────────────────┘
│ 둘 다 위임 대상
▼
┌──────────────────────┐
│ EntityManagerHolder │ ResourceHolderSupport 상속.
│ 스레드에 묶이는 EM │ transactionActive, savepointManager,
│ + (JPA4.0)EntityAgent│ refCount 등 보유
└──────────────────────┘
두 종류의 프록시가 있다는 점이 중요하다.
- 공유(shared) EM:
@PersistenceContext의 기본 타입(TRANSACTION). 자기 상태가 없다. 호출이 들어올 때마다 "지금 스레드에 묶인 트랜잭션 EM"을 찾아 위임하고, 없으면 일회용 EM을 만들어 한 번 쓰고 닫는다. - 확장(extended) EM:
PersistenceContextType.EXTENDED또는 application-managed EM. 자기만의 영속성 컨텍스트를 트랜잭션을 넘어 유지하고, 트랜잭션이 시작되면 거기에 합류(join)한다.
SharedEntityManagerInvocationHandler.invoke()(소스 287행)가 공유 EM의 모든 호출을 처리한다.
sharedEm.persist(entity) ← 주입받은 공유 EM 프록시
│
├─ 1. 메서드명 선처리 분기
│ equals/hashCode/toString → 프록시 자체로 처리(타깃 안 건드림)
│ getEntityManagerFactory/getCriteriaBuilder/getMetamodel
│ → EMF에서 바로 (EM 생성 회피)
│ isOpen → 항상 true, close → 무시,
│ getTransaction → IllegalState (공유 EM에 트랜잭션 직접 제어 금지)
│
├─ 2. 현재 트랜잭션 EM 획득
│ target = EntityManagerFactoryUtils
│ .doGetTransactionalEntityManager(emf, props, synced)
│
├─ 3. 트랜잭션 필수 메서드(persist/merge/remove/flush/refresh/joinTransaction)?
│ → target 없거나 실제 트랜잭션 없으면 TransactionRequiredException
│
├─ 4. target == null 이면 (트랜잭션 밖 읽기 등)
│ 일회용 EM 새로 생성 (newTarget = true)
│
└─ 5. invokeMethod(method, args, target, newTarget)
→ target.persist(entity) 실제 위임
→ 반환이 Query면: newTarget일 때 DeferredQueryInvocationHandler로
감싸 "쿼리 실행 시점까지 EM을 살려두고, 끝나면 닫음"
→ newTarget이면 finally에서 closeEntityHandler(target)
3단계가 JPA 스펙 준수의 핵심이다. persist 같은 쓰기 연산은 트랜잭션이 없으면 조용히 무시되어 버그가 되므로, 트랜잭션이 없으면 즉시 예외를 던진다. 반대로 단순 조회는 트랜잭션 없이도 일회용 EM으로 처리한다(4단계).
5단계의 deferred query가 영리하다. 트랜잭션 밖에서 createQuery()를 호출하면 일회용 EM이 만들어지는데, 쿼리를 바로 실행하지 않고 나중에 getResultList()를 부를 수도 있다. 그래서 Query 객체를 프록시로 감싸 두고, getResultList/getSingleResult 같은 종료 메서드(queryTerminatingMethods, 소스 84행)가 호출될 때 비로소 일회용 EM을 닫는다.
EntityManagerFactoryUtils.doGetTransactionalEntityManager()(소스 201행)는 공유 EM의 "현재 트랜잭션 EM"을 찾아 주는 함수다. 두 가지 시나리오를 처리한다.
doGetTransactionalEntityManager(emf, props, synchronizedWithTransaction)
│
├─ EMF로 스레드에서 EntityManagerHolder 조회
│
├─ [A] Holder 있음 (JpaTransactionManager가 이미 바인딩한 경우)
│ synced 요청인데 holder가 아직 미동기화면:
│ → em.joinTransaction() (JTA에 합류 시도)
│ → prepareTransaction() + TransactionalEntityManagerSynchronization 등록
│ → setSynchronizedWithTransaction(true)
│ holder.requested() (refCount 증가)
│ return holder.getEntityManager()
│
├─ [B] Holder 없음 + 동기화 비활성 → return null (트랜잭션 밖)
│
└─ [C] Holder 없음 + 동기화 활성 (JTA/다른 매니저 트랜잭션에 참여)
새 EM 생성 → EntityManagerHolder 래핑
→ 동기화 콜백 등록 → bindResource(emf, holder)
→ return em
[A]가 JpaTransactionManager로 트랜잭션을 시작한 일반적 경우다. 매니저가 이미 EM을 만들어 스레드에 묶어 뒀으므로, 공유 EM은 그것을 그대로 받아 쓴다. [C]는 JTA처럼 JPA 매니저가 아닌 트랜잭션에 JPA가 참여하는 경우로, 여기서 EM을 직접 만들고 동기화 콜백(TransactionalEntityManagerSynchronization)을 등록해 커밋 시 flush, 완료 시 close를 보장한다.
동기화 콜백의 순서: ENTITY_MANAGER_SYNCHRONIZATION_ORDER = CONNECTION_SYNCHRONIZATION_ORDER - 100(소스 79행). EM 정리를 JDBC Connection 정리보다 먼저 하도록 우선순위를 100 앞당긴다 — flush가 Connection을 써야 하기 때문이다.
ExtendedEntityManagerCreator는 application-managed/container-managed 확장 EM 프록시를 만든다. 차이는 "트랜잭션 자동 합류 모드"와 "수명"이다. JPA 2.1 이후 양쪽 모두 자동/수동 join을 지원하므로, 차이는 기본 join 모드와 컨테이너 EM의 제한된 수명(주입된 객체에 종속)으로 좁혀진다(소스 56~66행).
@PersistenceContext/@PersistenceUnit 주입은 PersistenceAnnotationBeanPostProcessor(이하 PABPP)가 처리한다.
빈 생성 시 PABPP가 주입 메타데이터 스캔
│
├─ @PersistenceUnit 필드/메서드
│ → EntityManagerFactory 주입
│ (Spring 빈 또는 JNDI persistence-unit-ref)
│
└─ @PersistenceContext 필드/메서드
type=TRANSACTION (기본)
→ SharedEntityManagerCreator.createSharedEntityManager(emf)
공유 EM 프록시 주입
type=EXTENDED
→ ExtendedEntityManagerCreator.createContainerManagedEntityManager(...)
확장 EM 프록시 주입
즉 @PersistenceContext EntityManager em;이 주입받는 것은 실제 EM이 아니라 공유 EM 프록시다. 이 프록시가 호출 시점마다 흐름 1을 거쳐 진짜 트랜잭션 EM에 위임하기에, 싱글톤 빈 필드에 EM을 넣어도 멀티스레드에서 안전하다.
SharedEntityManagerBean(support 패키지)은 같은 공유 EM을 XML/Java 설정에서 명시적 빈으로 노출하고 싶을 때 쓰는 FactoryBean 래퍼다.
| 메서드 | 위치 | 하는 일 |
|---|---|---|
createSharedEntityManager() |
SharedEntityManagerCreator |
공유 EM 프록시(JDK proxy) 생성 |
invoke() |
SharedEntityManagerInvocationHandler |
호출마다 트랜잭션 EM 위임, 없으면 일회용+close |
doGetTransactionalEntityManager() |
EntityManagerFactoryUtils |
스레드 바인딩 EM 조회/생성, 동기화 등록 |
closeEntityHandler() |
〃 | EM/EntityAgent를 안전하게 close (예외 로깅) |
createApplicationManagedEntityManager() |
ExtendedEntityManagerCreator |
확장 EM 프록시 생성 |
- JDK 동적 프록시 + InvocationHandler: 두 EM 생성기 모두
Proxy.newProxyInstance로 만든 프록시에 핸들러를 붙인다. 메서드 이름 기반switch로 의미를 부여(getTransaction금지,close무시 등)하는 패턴이 일관되게 쓰인다. - refCount 기반 추적:
EntityManagerHolder는ResourceHolderSupport의 참조 카운트로 "애플리케이션이 동기화 EM을 요청했는지"를 추적한다(소스 230행requested()). 이는 unsynchronized EM 업그레이드 로직(소스 235~250행)에서 쓰인다. - EntityAgent (JPA 4.0): 7.0.4부터 새
EntityAgentAPI를 리플렉션으로 감지해 동일한 공유/확장 메커니즘으로 지원한다(doGetTransactionalEntityAgent). API가 없는 JPA 3.2에서는 전부 무시된다.
EM 관리의 본질은 "실제 EM 대신 프록시를 주입하고, 프록시가 호출 시점에 스레드 바인딩된 트랜잭션 EM을 찾아 위임"하는 것이다. 공유 EM은 트랜잭션 스코프(상태 없음)로 @PersistenceContext의 기본이고, 확장 EM은 자체 영속성 컨텍스트를 유지하며 트랜잭션에 합류한다. 스레드 바인딩과 동기화 등록은 EntityManagerFactoryUtils가, 실제 묶임은 EntityManagerHolder가 담당하며, 이 모든 것이 TransactionSynchronizationManager(spring-tx) 위에서 작동한다.