JPA 를 사용하면서, 엔티티 매니저에 대한 개념이 부족한 것 같아, 관련 개념들을 정리한 글이다.
EntityManagerFactory
엔티티 매니저 팩토리는 엔티티 매니저를 만드는 팩토리로 여러 스레드가 동시에 접근해도 안전하게 사용할 수 있도록 설계되었다. 따라서, 엔티티 매니저 팩토리를 여러 군데에서 @PersistenceUnit 으로 주입 받아도 인스턴스 객체는 항상 같다.
EntityManagerFactory 를 인스턴스화 할 때 생성비용이 크기 때문에 한개만 만들어 애플리케이션 전체에서 공유한다. 생성 비용이 크다는 것은 EntityManagerFactory 를 인스턴스화할 때 여러 가지 초기화 작업이 많이 필요하다는 것을 의미한다. 아래와 같은 초기화 작업이 이루어지며, 리소스와 시간이 많이 소모되기 때문에, EntityManagerFactory 를 여러 번 생성하는 것은 비효율적이므로 하나만 생성해 공유할 수 있도록 스레드 안전성(thread-safety)을 보장한 불변(immutable) 객체로 설계되었다.
1. JPA 메타데이터 처리
- 엔티티 분석: EntityManagerFactory 를 생성할 때, JPA 구현체(Hibernate 등)는 애플리케이션의 모든 엔티티 클래스(@Entity 로 표시된 클래스)를 분석하고, 이들을 기반으로 메타데이터를 구성한다. 이 과정에서 엔티티의 매핑 정보, 관계 설정, 식별자, 컬럼 매핑, SQL 생성 전략 등이 처리된다.
- 매핑 파일 처리: 엔티티 클래스 외에도, persistence.xml 과 같은 매핑 파일이 있으면 이를 로드하고 처리해 JPA 구현체가 올바르게 데이터베이스와 상호작용할 수 있도록 한다.
2. 데이터베이스 연결 설정
- 데이터 소스 초기화: 데이터베이스 연결을 설정하고, 이를 관리하는 DataSource 와 관련된 설정을 초기화한다. 이 과정에서 커넥션 풀(Connection Pool)이 설정되고, 데이터베이스와의 기본 연결이 설정된다.
- 방언(Dialect) 설정: JPA 구현체는 데이터베이스와 상호작용하기 위해 해당 데이터베이스의 특정 방언(Dialect)을 사용해야 한다. EntityManagerFactory 를 생성할 때, 이 방언이 결정되고, SQL 생성 규칙 등이 설정된다.
3. SQL 생성 및 캐시 초기화
- SQL 및 쿼리 캐시 설정: SQL을 생성하기 위한 규칙과 캐시 설정을 초기화한다. 애플리케이션이 데이터베이스와 상호작용할 때 필요한 SQL 문을 효율적으로 생성하고, 쿼리 성능을 최적화하기 위한 설정이다.
- 캐시 초기화: Hibernate와 같은 JPA 구현체는 2차 캐시를 설정하고 초기화한다. 이는 엔티티 데이터를 메모리에 캐시하여 데이터베이스 접근을 최소화하고 성능을 향상시키기 위함이다.
보통 스프링 환경에서 EntityManagerFactory 를 직접 사용할 일은 거의 없는데, Spring 컨테이너가 트랜잭션 관리와 EntityManager 의 생명 주기를 자동으로 관리하기 때문이다.
여기서 LocalContainerEntityManagerFactoryBean 이라는 개념이 등장하는데, 스프링에서 JPA 의 EntityManagerFactory 를 설정하고 관리하기 위한 편의 클래스다. 여기서 DataSource, JPA공급자(Hibernate 같은), JPA 속성, 엔티티 패키지 등 설정이 가능하다. 이렇게 이루어진 설정을 바탕으로, 스프링이 EntityManagerFactory 를 초기화한다. 애플리케이션에서 사용하는 데이터베이스가 하나라면 엔티터 매니저 팩토리도 하나만 필요하다.
아래는 LocalContainerEntityManagerFactoryBean 클래스에서 엔티티 매니저 팩토리를 생성하는 부분이다.
protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException {
Assert.state(this.persistenceUnitInfo != null, "PersistenceUnitInfo not initialized");
PersistenceProvider provider = this.getPersistenceProvider();
if (provider == null) {
String providerClassName = this.persistenceUnitInfo.getPersistenceProviderClassName();
if (providerClassName == null) {
throw new IllegalArgumentException("No PersistenceProvider specified in EntityManagerFactory configuration, and chosen PersistenceUnitInfo does not specify a provider class name either");
}
Class<?> providerClass = ClassUtils.resolveClassName(providerClassName, this.getBeanClassLoader());
provider = (PersistenceProvider)BeanUtils.instantiateClass(providerClass);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Building JPA container EntityManagerFactory for persistence unit '" + this.persistenceUnitInfo.getPersistenceUnitName() + "'");
}
EntityManagerFactory emf = provider.createContainerEntityManagerFactory(this.persistenceUnitInfo, this.getJpaPropertyMap());
this.postProcessEntityManagerFactory(emf, this.persistenceUnitInfo);
return emf;
}
기본적인 속성들은 아래와 같으며, 실제 코드를확인해보면 설정하는 부분이 나와있다.
- dataSource: Spring의 DataSource 빈을 주입받아 설정
- packagesToScan: JPA 엔티티 클래스가 위치한 패키지를 지정
- jpaVendorAdapter: Hibernate 또는 다른 JPA 공급자를 위한 설정을 지정
- jpaProperties: JPA 공급자(Hibernate 등)에 대한 추가적인 속성을 설정
더 많은 속성들에 대한 설명은 공식문서를 참고하면 좋을 것 같다.
J2SE(JavaSE) 처럼 독립적으로 실행되는 Java 애플리케이션 환경에서는 EntityManagerFactory 를 직접 코드에서 생성하고 관리한다.
이때 Persistence.createEntityManagerFactory() 를 이용해 명시적으로 생성하고 닫을 수 있다.
public class J2SEExample {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
// 엔티티 매니저 팩토리에서 엔티티 매니저 생성
EntityManager em = emf.createEntityManager();
// 트랜잭션 시작
em.getTransaction().begin();
// 엔티티 작업 수행
// 트랜잭션 커밋
em.getTransaction().commit();
em.close();
emf.close();
}
}
EntityManager
엔티티매니저는 엔티티를 저장, 수정, 삭제, 조회 등 엔티티와 관련된 모든 일을 처리하는 곳으로, 영속성 컨텍스트를 통해 데이터의 상태 변화를 감지하고 필요한 쿼리를 자동으로 수행한다.
엔티티매니저 자체는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하기 때문에 공유해서는 안된다.
스프링에서는 EntityManager를 직접 빈으로 등록하지 않고, EntityManagerFactory에서 관리하는 방식으로 EntityManager를 이용한다.
주입 방식 (@Autowired vs @PersistenceContext)
애플리케이션이 실행될 때 SpringBoot가 JPA 관련 빈들을 자동으로 구성하고 등록하는 과정에서 EntityManagerFactory 가 빈으로 등록되므로, 애플리케이션에서 별도로 EntityManager 를 빈으로 등록하지 않더라도 애노테이션을 사용해 EntityManager 를 주입받을 수 있다.
이때 두 가지 방식이 있는데 @PersistenceContext 와 @Autowired 이다.
@PersistenceContext 는 JPA 표준으로 각 트랜잭션에 대해 고유한 EntityManager 가 제공된다.
스프링 컨테이너가 초기화되면서 @PersistenceContext 로 주입받은 EntityManager 를 Proxy 로 감싼다. 그리고 EntityManager 호출 시 마다 Proxy 를 통해 EntityManager 제공해 thread-safety 를 보장한다. 이렇게 EntityManager 를 제공할 때 Transaction 에 의해 기존에 생성된 EntityManager 가 있다면 반환하고, 없다면 EntityManagerFactory 에서 새로운 EntityManager 를 생성한다.
이후 트랜잭션이 종료되면 Spring 은 해당 EntityManager 를 정리 후, 트랜잭션이 새로 시작되면 새로운 EntityManager 가 제공된다.
@Autowired 는 스프링에서 의존성을 주입하기 위해 사용되는 애노테이션으로 스프링에서는 @Autowired 로 주입한 엔티티매니저 또한 프록시 객체로 관리된다. 관련해서는 이 글을 읽어보면 좋을 것 같다.
결론적으로, Spring Boot 환경에서 @Autowired를 사용하여 EntityManager를 주입받는 것이 반드시 잘못된 것은 아니다. Spring의 프록시 메커니즘 덕분에 안전하게 사용할 수 있기 때문이다. 그러나 JPA 표준을 따라 개발하려면 @PersistenceContext를 사용하는 것이 좋을 것 같다.
롬복으로 주입받을 경우에는?
private final 로 선언 후 롬복의 @RequiredArgsConstructor 를 사용하면 final 필드만을 매개변수로 받는 생성자를 자동으로 만들어준다. 이 경우 해당 클래스에서 사용하는 의존성을 자동으로 주입받을 수 있다.
@RequiredArgsConstructor 는 단순히 생성자 주입 방식이기 때문에, EntityManager 가 @Autowired 를 통해 주입된다.
위에서 설명한 것 처럼 스프링프레임워크는 실제 EntityManager 를 주입하는 것이 아니라, 프록시를 주입하기 때문에 동시성 이슈에 대한 고민 없이 개발이 가능하다.
위 설명은 인프런 김영한님 답변을 참고해 작성했다.
커넥션, 커넥션풀 획득 시점
커넥션 풀은 데이터베이스 연결을 미리 만들어두고 필요할 때마다 이를 할당하고 반납받는 역할을 한다.
JPA 구현체는(Hibernate 같은) EntityManagerFactory를 생성할 때, 데이터베이스와의 연결을 효율적으로 관리하기 위해 커넥션 풀도 함께 생성한다. 일반적으로 커넥션풀 라이브러리 (HikariCP같은) 를 사용해 설정된다.
커넥션 획득 시점은 트랜잭션이 시작되거나, 실제로 데이터베이스와 상호작용하는 작업이 필요할 때 커넥션을 획득한다.
EntityManager 는 커넥션 풀에서 하나의 커넥션을 얻어 이용하는데, 이는 트랜잭션이 종료될 때까지 유지된다.
'JPA' 카테고리의 다른 글
| [JPA] @Embeddable 에서 모든 필드가 null 일 경우 NullPointerException 발생 케이스와 해결 방안 (0) | 2025.04.26 |
|---|---|
| [Hibernate] Hibernate6.2 부터 변경된 @Enumerated(EnumType.STRING) 매핑방식 (0) | 2025.04.26 |