읽기전용 EntityManagerFactory 생성 시 EntityManger 의 빈 우선순위 설정이 안돼요

은찬·2024년 12월 22일
0

Java, Spring

목록 보기
10/10

안녕하세요. 이번 포스팅에서는 제목에서 말한 내용 그대로, 읽기전용 EntityManagerFactory 생성 시 EntityManger 의 빈 우선순위 설정이 안되는 문제에 대해서 포스팅하려 합니다.

제가 실제로 겪었던 문제였고, 제가 수행한 환경과 설정한 코드와 함께 문제를 해결한 과정을 얘기해보겠습니다 💁‍♂️


내가 하려던 것

우선 제가 어떤걸 하려고 시도하다가 문제를 직면했는지 알아야겠죠

저는 스프링 배치 환경에서 ItemReader가 읽기전용 DB서버를 바라볼 수 있도록 설정을 하려했습니다.

개발자의 코드에 따라서 ItemReader가 Reader DB, Master DB를 볼 수 있도록 구현하는게 목적이었죠.

우여곡절이 있었지만, 잘 마쳤고 테스트까지 완료했으며 이제 다른 배치 프로젝트에도 똑같이 적용하는 일만 남았습니다.

위 내용에 대한 자세한 내용이 궁금하다면 해당 글을 읽어주시면 좋을 것 같습니다

선두로 설정을 적용한 프로젝트 환경은 [Kotlin + 스프링 부트 2.7.5] 환경이었고, 해당 설정과 테스트를 마친 후 다른 프로젝트인 [Java + 스프링 부트 2.5.0] 환경에도 같은 설정을 적용하고자 코드를 짠 후 테스트를 수행했습니다.


문제 상황 발생

No qualifying bean of type 'javax.persistence.EntityManager' available: expected single matching bean but found 2: 
org.springframework.orm.jpa.SharedEntityManagerCreator#0,
org.springframework.orm.jpa.SharedEntityManagerCreator#1

제가 직면한 에러 메세지입니다. 메세지가 뜻하는 바는 아래와 같습니다.

스프링 빈에 등록된 EntityManager가 두개다! 근데 나는 어떤 EntityManager를 선택해야하는지 모른다! 악!!

이상합니다. 제가 한 설정 코드를 보시죠

@RequiredArgsConstructor
@Configuration
@EnableJpaRepositories(
    basePackages = PACKAGE,
    entityManagerFactoryRef = MASTER_ENTITY_MANAGER_FACTORY,
    transactionManagerRef = MASTER_TX_MANAGER
)
public class BatchEntityManagerConfig {
    public static final String PACKAGE = "com.my.project";
    public static final String MASTER_ENTITY_MANAGER_FACTORY = "masterEntityManagerFactory";
    public static final String MASTER_TX_MANAGER = "masterTransactionManager";
    public static final String READER_ENTITY_MANAGER_FACTORY = "readerEntityMangerFactory";

    private final JpaProperties jpaProperties;
    private final HibernateProperties hibernateProperties;
    private final ObjectProvider<Collection<DataSourcePoolMetadataProvider>> metadataProviders;
    private final EntityManagerFactoryBuilder entityManagerFactoryBuilder;

    @Primary
    @Bean(name = MASTER_ENTITY_MANAGER_FACTORY)
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(DataSource dataSource) {
        return EntityManagerFactoryCreator.builder()
                                          .properties(jpaProperties)
                                          .hibernateProperties(hibernateProperties)
                                          .metadataProviders(metadataProviders)
                                          .entityManagerFactoryBuilder(entityManagerFactoryBuilder)
                                          .dataSource(dataSource)
                                          .packages(PACKAGE)
                                          .persistenceUnit("master-emf")
                                          .build()
                                          .create();
    }

    @Primary
    @Bean(name = MASTER_TX_MANAGER)
    public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
    }

    @Bean(name = READER_ENTITY_MANAGER_FACTORY)
    public LocalContainerEntityManagerFactoryBean readerEntityManagerFactory(@Qualifier(SLAVE_DATASOURCE) DataSource dataSource) {
        return EntityManagerFactoryCreator.builder()
                                          .properties(jpaProperties)
                                          .hibernateProperties(hibernateProperties)
                                          .metadataProviders(metadataProviders)
                                          .entityManagerFactoryBuilder(entityManagerFactoryBuilder)
                                          .dataSource(dataSource)
                                          .packages(PACKAGE)
                                          .persistenceUnit("reader")
                                          .build()
                                          .create();
    }
}

분명히 Master DataSource를 가지는 EntityMangerFactory에는 @Primary 어노테이션을 달아주었고, Slave DataSource를 가지는 EntityMangerFactory에는 이름만 명시해줬습니다.

참고로 스프링 빈에 EntityMangerFactory를 등록하게 되면 JPA AutoConfiguration에 의해서 EntityManager가 EntityMangerFactory 빈의 이름과 같은 이름으로 스프링 빈으로 등록됩니다.

그리고 중요한 것이, EntityMangerFactory에 빈 우선순위를 명시하면 생성되는 EntityManger에도 똑같이 우선순위가 적용된다는 점입니다.

그런데 빈 우선순위가 적용되지 않았고, 내부적으로 스프링 빈에 등록된 EntityManger를 사용하는 JpaRepository 혹은 QueryDSL 컴포넌트에서 위 에러를 뱉어댔습니다.

다른 코틀린 프로젝트에선 분명히 됐는데 말이죠…😢


실제 EntityManger에 우선순위가 적용되는지 테스트

같은 설정에서 잘 동작하는 코틀린 프로젝트에서 해당 설정이 틀린건 아닌지, 테스트를 해보겠습니다.

위 코드를 디버깅 모드로 실행하면

Master, Slave 두개의 EntityManager가 등록되는걸 볼 수 있습니다.

빈 우선순위도 잘 적용되는지 봅시다! 우선 Master EntityManager 입니다.

하나의 빈만 잘 들어오는걸 볼 수 있죠. 해당 빈은 Master EntityManger 빈 입니다.

그럼 마지막으로 Reader EntityMangerFactory의 빈 이름으로 Reader EntityManager도 잘 로드 되는지 봅시다!

네! 역시 잘됩니다. Reader EntityMangerFactory의 빈 이름으로 Reader EntityManager 빈이 잘 등록되었다는 것이죠.


문제 해결 과정

먼저 문제 해결하기 위해선 스프링 빈에 등록된 EntityManager에 우선순위를 줘야했습니다.

Master EntityManager를 직접 생성 후 등록!

그래서 처음 생각한 방법 이 직접 Master EntityManger를 @Primary로 빈에 등록하는 것이었습니다.

아래와 같은 방법을 처음에 시도했습니다.

@Primary
@Bean
public EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
    return entityManagerFactory.createEntityManager();
}

하지만 해당 방식은 굉장히 위험한데요,

이유는 생성된 EntityManger의 생명주기의 관리가 안되기 때문입니다.

자세히 알기 위해선 스프링의 스프링의 EntityManager 관리 방식을 알아야합니다.


스프링의 EntityManager 관리 방식

스프링은 EntityManger의 생명주기를 트랜잭션의 단위로 관리합니다.

다들 잘 알고 계실겁니다. 트랜잭션이 시작하면 EntityManger의 영속성 컨텍스트가 시작되고, 트랜잭션이 닫힐때, flush()와 clear()가 이루어지며 EntityManger도 함께 종료됩니다.

우리는 스프링 빈에 등록된 EntityManger 하나만 들고 사용합니다. 하지만 여러 동시적인 요청과 그에 따른 트랜잭션이 무수히 발생할텐데
스프링은 어떻게 트랜잭션 각각의 영속성 컨텍스트와 EntityManger를 제공하고 생명주기를 관리할까요?

정답은 SharedEntityManagerCreator에 있습니다.

스프링은 내부적으로 entityManagerFactory.createEntityManager(); 방식으로 EntityManager를 생성하지 않습니다.

SharedEntityManagerCreator를 통해서 EntityManger를 생성하는데요, 이때 만들어지는 EntityManager는 프록시 객체입니다.

그리고 만들어진 프록시는 아래의 역할을 합니다.

  • 실제 EntityManager에 대한 호출을 가로채서 처리
  • 트랜잭션 컨텍스트에 따라 적절한 EntityManager를 생성하거나 재사용
  • 여러 스레드에서 안전하게 사용할 수 있도록 지원

실제 SharedEntityManagerCreator 코드 일부분을 보면 Proxy 객체를 생성하는걸 볼 수 있습니다.


SharedEntityManagerCreator를 통해 Master EntityManger를 등록하자!

최종적으로 내린 결론이자 해결방법입니다.

바로 코드로 보시죠.

@Primary
@Bean
public EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
    return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory);
}

이렇게 직접 Master EntityManger를 @Primary로 직접 등록했습니다.


해결 및 결과

이제 똑같이 자바 환경에서 테스트 빈 설정 후 EntityManger를 주입받아보면..!

에러없이 스프링 부트도 잘 뜨며, 성공적으로 하나의 Master EntityManger만 로드하는 걸 볼 수 있습니다👏

그리고 실제로 SharedEntityManagerCreator를 통해 EntityManger를 생성했기 때문에, EntityManger 생명주기도 문제없이 관리됩니다!

긴 글 읽어주셔서 감사합니다 🙇‍♂️

profile
`강한` 백엔드 개발자라고 해두겠습니다

0개의 댓글