JPA를 쓰려면 먼저 EntityManagerFactory(EMF)를 만들어야 한다. 순수 JPA는 META-INF/persistence.xml을 읽어 Persistence.createEntityManagerFactory("unit")를 호출하는 방식이다. 그러나 이렇게 만든 EMF는 Spring의 DataSource·엔티티 스캔·예외 변환과 연결되지 않고, 트랜잭션 EM 공유 기능도 없다.
이 챕터는 Spring이 EMF를 FactoryBean으로 만들고 프록시로 감싸서 싱글톤 빈으로 노출하는 과정을 다룬다. 관련 타입은 orm.jpa의 AbstractEntityManagerFactoryBean·LocalContainerEntityManagerFactoryBean·EntityManagerFactoryInfo와 orm.jpa.persistenceunit 패키지 전체다.
┌──────────────────────────────────────────────┐
│ AbstractEntityManagerFactoryBean │
│ implements SmartFactoryBean<EMF>, │
│ EntityManagerFactoryInfo, │
│ PersistenceExceptionTranslator, │
│ InitializingBean, DisposableBean │
│ - 공통: 프록시 생성, 벤더어댑터 적용, │
│ 비동기 부트스트랩, 예외변환 │
│ - abstract createNativeEntityManagerFactory()│
└───────────────┬──────────────────────────────┘
│ extends
┌─────────────────┴───────────────────┐
▼ ▼
┌────────────────────────┐ ┌──────────────────────────┐
│ LocalEntityManager- │ │ LocalContainerEntity- │
│ FactoryBean │ │ ManagerFactoryBean │
│ - SE 스타일 │ │ - 컨테이너 스타일(권장) │
│ - Persistence.create- │ │ - PersistenceUnitInfo로 │
│ EntityManagerFactory │ │ provider.createContainer│
│ - DataSource 주입 불가 │ │ - DataSource/스캔/LTW 통합│
└────────────────────────┘ └─────────────┬────────────┘
│ 사용
┌─────────────▼────────────┐
│ orm.jpa.persistenceunit │
│ DefaultPersistenceUnit- │
│ Manager │
│ → PersistenceUnitInfo │
└──────────────────────────┘
AbstractEntityManagerFactoryBean: 모든 부트스트랩의 공통 골격. native EMF를 만든 뒤 프록시로 감싸고, 공유 EM·EntityAgent까지 노출한다.PersistenceExceptionTranslator도 구현하므로 그 존재만으로 예외 변환이 활성화된다.LocalContainerEntityManagerFactoryBean(이하 LCEMFB): 실무 표준.persistence.xml을 직접 파싱하거나 패키지 스캔으로PersistenceUnitInfo를 조립하고, SpringDataSource·LoadTimeWeaver를 거기에 끼워 넣은 뒤PersistenceProvider.createContainerEntityManagerFactory()를 호출한다.EntityManagerFactoryInfo: Spring이 만든 EMF 프록시에 캐스팅하면 얻는 메타데이터 인터페이스.getNativeEntityManagerFactory(),getDataSource(),getJpaDialect(),createNativeEntityManager()등을 노출한다. 트랜잭션 매니저·EM 생성기들이 이 인터페이스로 프록시에서 정보를 캐낸다.
AbstractEntityManagerFactoryBean.afterPropertiesSet()(소스 367행~)이 부트스트랩의 심장이다.
afterPropertiesSet()
│
├─ 1. JpaVendorAdapter 적용 (있으면)
│ persistenceProvider, jpaPropertyMap, EM/EMF 인터페이스,
│ entityAgentInterface, jpaDialect 를 어댑터 기본값으로 채움
│
├─ 2. native EMF 생성
│ bootstrapExecutor 있으면 → Future로 백그라운드 생성
│ 없으면 → buildNativeEntityManagerFactory() 동기 실행
│ └─ createNativeEntityManagerFactory() (하위 클래스)
│ └─ jpaVendorAdapter.postProcessEntityManagerFactory()
│
├─ 3. EMF 프록시 생성 createEntityManagerFactoryProxy(native)
│ → 모든 EMF 인터페이스 + EntityManagerFactoryInfo 를 구현하는 JDK 프록시
│
├─ 4. 공유 EntityManager 프록시 생성
│ SharedEntityManagerCreator.createSharedEntityManager(프록시)
│
└─ 5. (JPA 4.0) 공유 EntityAgent 프록시 생성 (있으면)
여기서 1단계의 벤더 어댑터 병합이 핵심이다. HibernateJpaVendorAdapter를 꽂아두면 provider 클래스, Hibernate 프로퍼티, HibernateJpaDialect가 자동으로 채워진다(05장). 사용자가 명시한 값이 우선이고 빈 칸만 어댑터가 채운다(if (!this.jpaPropertyMap.containsKey(key))).
2단계의 비동기 부트스트랩: setBootstrapExecutor()로 Executor를 주면 EMF 초기화를 백그라운드 스레드에서 돌리고, 프록시는 즉시 반환한다. 첫 실제 호출이 들어오면 getNativeEntityManagerFactory()가 Future.get()으로 블로킹한다. 단, 6.2부터는 afterSingletonsInstantiated()에서 컨텍스트 리프레시 완료 전에 강제로 완료를 기다린다(소스 427행).
3단계에서 만든 프록시는 단순 위임이 아니다. ManagedEntityManagerFactoryInvocationHandler → invokeProxyMethod()(소스 512행)가 호출을 분기한다.
emfProxy.someMethod(args)
│
├─ EntityManagerFactoryInfo 메서드? → this(FactoryBean)에 위임
│ (getNativeEntityManagerFactory, getDataSource, getJpaDialect ...)
│
├─ createEntityManager(SYNCHRONIZED, ...) ?
│ → native EMF로 raw EM 생성 → postProcessEntityManager()
│ → ExtendedEntityManagerCreator.createApplicationManagedEntityManager(
│ raw, this, synchronized=true) ← "확장 EM 프록시" 반환
│
├─ 인자에 Query 프록시 있으면 unwrap (addNamedQuery 대비)
│
└─ 그 외 → native EMF에 위임. 반환이 EntityManager면
역시 확장 EM 프록시로 감싸서 반환
즉 이 프록시를 거쳐 만든 모든 EM은 "확장 EM 프록시"가 되어 진행 중인 트랜잭션에 자동 합류한다(03장). 애플리케이션이 직접 emf.createEntityManager()를 호출해도 Spring 트랜잭션을 인식하는 EM이 나오는 이유다.
LocalContainerEntityManagerFactoryBean.afterPropertiesSet()(소스 383행)은 부모 호출 전에 PersistenceUnitInfo를 준비한다.
LCEMFB.afterPropertiesSet()
│
├─ PersistenceUnitManager 결정
│ 외부 주입 없으면 internalPersistenceUnitManager(DefaultPersistenceUnitManager) 사용
│ internal.afterPropertiesSet() ← persistence.xml 스캔 + 엔티티 스캔
│
├─ determinePersistenceUnitInfo(manager)
│ unitName 있으면 obtainPersistenceUnitInfo(name)
│ 없으면 obtainDefaultPersistenceUnitInfo()
│
├─ SmartPersistenceUnitInfo면 provider 루트 패키지 설정
├─ qualifier 어노테이션 → 빈 정의에 AutowireCandidateQualifier 추가
│
└─ super.afterPropertiesSet() ← 공통 골격(흐름 1)
└─ createNativeEntityManagerFactory() (아래)
그리고 native EMF 생성(소스 422행)은:
createNativeEntityManagerFactory()
├─ PersistenceProvider 결정 (명시값 → 없으면 PUI의 providerClassName 인스턴스화)
└─ provider.createContainerEntityManagerFactory(persistenceUnitInfo, jpaPropertyMap)
setDataSource()/setJtaDataSource()(소스 313/331행)는 DefaultPersistenceUnitManager에 SingleDataSourceLookup과 기본 DataSource를 설정한다. 그래서 persistence.xml에 JDBC 설정을 적지 않고 Spring DataSource 빈을 그대로 쓸 수 있다 — 이것이 LCEMFB가 SE 스타일 LocalEntityManagerFactoryBean보다 강력한 핵심 이유다.
DefaultPersistenceUnitManager는 다음을 담당한다(소스 56~91행).
- persistence.xml 스캔: 기본 위치
classpath*:META-INF/persistence.xml을PersistenceUnitReader로 파싱해MutablePersistenceUnitInfo를 만든다. - Spring 기반 엔티티 스캔:
setPackagesToScan(...)을 쓰면persistence.xml없이도@Entity클래스를 클래스패스에서 찾는다. 내부적으로PersistenceManagedTypesScanner가 동작. PersistenceManagedTypes: 관리 대상 클래스명/패키지명 목록을 표현하는 인터페이스.setManagedTypes()로 미리 만든 목록을 주면 런타임 스캔을 건너뛴다.
PersistenceManagedTypesBeanRegistrationAotProcessor(소스 68행)는 빌드 타임에 PersistenceManagedTypes 빈을 발견하면, 런타임 스캔 대신 하드코딩된 클래스명/패키지명 목록으로 치환하는 코드를 생성한다. 동시에 각 엔티티에 대해 리플렉션 힌트(생성자·@EntityListeners·@Converter·콜백 메서드 등)를 등록해, GraalVM 네이티브 이미지에서 JPA가 동작하도록 한다. 즉 "스캔은 빌드 타임에 한 번, 런타임엔 결과만"이라는 AOT 원칙을 JPA에 적용한 것이다.
| 메서드 | 위치 | 하는 일 |
|---|---|---|
afterPropertiesSet() |
AbstractEntityManagerFactoryBean |
벤더어댑터 병합 → native EMF 생성 → 프록시·공유 EM 생성 |
createEntityManagerFactoryProxy() |
〃 | EMF 인터페이스 + EntityManagerFactoryInfo를 구현하는 JDK 프록시 생성 |
invokeProxyMethod() |
〃 | createEntityManager 가로채 확장 EM 프록시로 변환, Info 메서드 분기 |
createNativeEntityManager() |
〃 | raw EM 생성 + postProcessEntityManager 적용 (트랜잭션 매니저가 호출) |
createNativeEntityManagerFactory() |
LocalContainerEMFB |
provider.createContainerEntityManagerFactory 호출 |
determinePersistenceUnitInfo() |
〃 | unit 이름으로 PUI 선택(또는 디폴트) |
- 프록시 이중화: EMF를 프록시로 감싸 (a)
EntityManagerFactoryInfo믹스인을 주입하고 (b) 거기서 나오는 EM을 자동으로 트랜잭션 인식 프록시로 만든다. 부트스트랩 단계에서 이 두 장치를 심어 두기에, 이후 모든 EM 사용이 투명하게 Spring 트랜잭션과 연동된다. SmartFactoryBean로 EM 직접 주입: 7.0부터 EMF 팩토리빈이 공유EntityManager도 노출한다(getObject(EntityManager.class)). 덕분에@Autowired EntityManager가 가능해졌다.- 확장점:
postProcessEntityManagerFactory(emf, pui)(LCEMFB),setEntityManagerInitializer()(모든 새 EM 커스터마이즈),PersistenceUnitPostProcessor(PUI에 엔티티/jar 추가).
EMF 부트스트랩은 AbstractEntityManagerFactoryBean이 공통 골격을, LocalContainerEntityManagerFactoryBean이 컨테이너 스타일 조립을 맡는 2층 구조다. 핵심 산출물은 EntityManagerFactoryInfo를 겸하는 EMF 프록시이며, 이 프록시가 만들어 내는 EM은 자동으로 트랜잭션을 인식한다. persistenceunit 패키지는 persistence.xml 파싱과 엔티티 스캔, 그리고 AOT 최적화를 담당해, 런타임 의존을 최소화한다.