Spring Boot 프로젝트를 진행하다 보면 데이터베이스를 하나가 아닌 여러 개를 연결해야 하는 상황이 종종 발생합니다. 예를 들어, 레거시 DB와 신규 서비스 DB를 동시에 바라봐야 하거나, 읽기 전용과 쓰기 전용 DB를 분리하는 경우입니다.
하지만 이렇게 야심 차게 멀티 데이터 소스를 설정하고 애플리케이션을 실행했을 때, 예상치 못한 Application run failed 메시지와 마주하게 됩니다. 오늘은 Spring Boot 개발자들을 가장 당혹스럽게 만드는 오류 중 하나인 NoUniqueBeanDefinitionException 해결 방법에 대해 깊이 있게 다뤄보겠습니다. 특히 EntityManagerFactory빈이 충돌하여 발생하는 문제를 중심으로 원인과 해결책을 하나씩 살펴보겠습니다.
1. 문제 상황: 애플리케이션 실행 실패
멀티 모듈 혹은 복잡한 JPA 설정을 마친 후 서버를 띄우려고 할 때, 다음과 같은 긴 에러 로그가 출력되며 애플리케이션이 종료되는 현상이 발생했습니다.
ERROR [main] o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Unsatisfied dependency expressed through method 'setConfigurers' parameter 0: ...
...
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'jakarta.persistence.EntityManagerFactory' available: more than one 'primary' bean found among candidates: [generalEntityManager, secureEntityManager, springEntityManager]
로그를 자세히 살펴보면 문제의 핵심이 드러납니다. 바로 NoUniqueBeanDefinitionException입니다. 에러 메시지는 친절하게도 다음과 같이 설명하고 있습니다.

“No qualifying bean of type ‘jakarta.persistence.EntityManagerFactory’ available: more than one ‘primary’ bean found among candidates: [generalEntityManager, secureEntityManager, springEntityManager]”
즉, Spring 컨테이너가 jakarta.persistence.EntityManagerFactory 타입의 빈을 찾으려고 했으나, 후보군인 generalEntityManager, secureEntityManager, springEntityManager 중에서 어떤 것을 메인으로 사용해야 할지 결정하지 못했다는 뜻입니다.
2. 원인 분석: 왜 빈(Bean) 충돌이 발생했는가?
이 오류는 Spring의 의존성 주입(DI) 원칙과 밀접한 관련이 있습니다. Spring Boot는 자동 구성(Auto Configuration) 기능을 통해 개발자가 명시적으로 설정하지 않아도 필요한 빈들을 자동으로 생성하고 주입합니다.
이번 케이스에서 문제가 된 부분은 OpenEntityManagerInViewInterceptor입니다. Spring Boot는 웹 요청이 들어왔을 때 JPA 영속성 컨텍스트를 뷰 렌더링 시점까지 열어두기 위해 이 인터셉터를 자동으로 구성하려고 시도합니다. 이때 기본이 되는 EntityManagerFactory가 하나 필요합니다.
하지만 현재 프로젝트에는 세 개의 EntityManagerFactory가 정의되어 있었습니다.
generalEntityManagersecureEntityManagerspringEntityManager
Spring 입장에서는 이 셋 중 누구를 대장(Primary)으로 삼아 자동 구성에 사용해야 할지 모르는 상황인 것입니다. 더욱이 실제 코드를 확인해 본 결과, generalEntityManager와 springEntityManager 두 곳에 모두 @Primary어노테이션이 선언되어 있었습니다. 하나의 왕국에 왕이 두 명인 셈이니 충돌이 발생할 수밖에 없는 구조였습니다.
3. NoUniqueBeanDefinitionException 해결 솔루션
이 문제를 해결하기 위해 적용할 수 있는 방법은 크게 두 가지 접근 방식이 있습니다. 상황에 맞춰 가장 적합한 방법을 선택하시기 바랍니다.
해결책 1: @Primary 어노테이션 재정의 (가장 권장됨)
가장 정석적인 NoUniqueBeanDefinitionException 해결 방법은 Spring에게 “이것이 진짜 메인 빈이다”라고 명확하게 알려주는 것입니다.
여러 개의 EntityManagerFactory가 존재한다면, 그중 가장 주가 되는(Main) DB 설정 하나에만 @Primary를 부여하고 나머지는 제거해야 합니다. Spring Boot의 자동 구성 요소들은 @Primary가 붙은 빈을 우선적으로 선택하여 가져갑니다.
수정된 코드 예시:
Java
@Configuration
public class JpaConfig {
// 주로 사용될 메인 빈에만 @Primary를 유지합니다.
@Bean(name = "springEntityManager")
@Primary
public EntityManagerFactory springEntityManager(EntityManagerFactoryBuilder builder) {
// ... 설정 로직
}
// 보조 빈에는 @Primary를 제거합니다.
@Bean(name = "secureEntityManager")
public EntityManagerFactory secureEntityManager(EntityManagerFactoryBuilder builder) {
// ... 설정 로직
}
// generalEntityManager 설정에서도 @Primary가 있다면 제거해야 합니다.
}
이렇게 하면 OpenEntityManagerInViewInterceptor와 같은 내부 컴포넌트들이 springEntityManager를 자동으로 인식하여 주입받게 되므로 에러가 사라집니다.
해결책 2: OpenEntityManagerInView(OEIV) 비활성화
두 번째 방법은 에러를 유발한 원인 자체를 제거하는 방식입니다. 에러 로그를 다시 보면 OpenEntityManagerInViewInterceptor를 생성하다가 문제가 터진 것을 알 수 있습니다.
만약 여러분의 프로젝트가 Spring Batch 애플리케이션이거나, 순수 API 서버로서 View 템플릿(Thymeleaf, JSP 등)을 사용하지 않는다면 굳이 OEIV 기능이 필요하지 않을 수 있습니다. OEIV는 뷰 렌더링 시 지연 로딩(Lazy Loading) 이슈를 해결하기 위한 패턴이기 때문입니다.
해당 프로젝트 역시 Spring Batch와 일부 REST API만 제공하는 형태였기에 굳이 불필요한 인터셉터를 띄울 필요가 없었습니다.
설정 파일 수정 (application.properties 또는 yml):
Properties
# application.properties
spring.jpa.open-in-view=false
이 설정을 추가하면 Spring Boot는 OpenEntityManagerInViewInterceptor를 빈으로 등록하지 않게 되며, 자연스럽게 EntityManagerFactory를 찾는 과정도 생략되어 충돌 문제가 해결됩니다. 성능 측면에서도 트랜잭션을 뷰 레이어까지 끌고 가지 않으므로 DB 커넥션 반환이 빨라지는 이점이 있습니다.
해결책 3: @Qualifier를 사용한 수동 구성 (고급 설정)
만약 OEIV 기능도 필요하고, 특정 빈을 명시적으로 지정해서 인터셉터에 끼워 넣고 싶다면 @Qualifier를 사용하여 수동으로 빈을 등록할 수 있습니다. 이는 자동 구성을 비활성화하거나 오버라이딩하는 방식입니다.
Java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor(
@Qualifier("springEntityManager") EntityManagerFactory entityManagerFactory) {
OpenEntityManagerInViewInterceptor interceptor = new OpenEntityManagerInViewInterceptor();
interceptor.setEntityManagerFactory(entityManagerFactory);
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 수동으로 생성한 인터셉터를 등록
registry.addInterceptor(openEntityManagerInViewInterceptor(null));
}
}
이 방법은 @Primary에 의존하지 않고, 코드 레벨에서 어떤 빈을 사용할지 강제하는 강력한 방법입니다.
4. 결론
Spring Boot에서 NoUniqueBeanDefinitionException은 매우 빈번하게 발생하는 에러지만, 그 원리는 단순합니다. “선택지가 너무 많아 무엇을 고를지 모르겠다”는 Spring의 외침입니다.
오늘 살펴본 NoUniqueBeanDefinitionException 해결 과정을 요약하자면 다음과 같습니다.
- 에러 로그를 통해 어떤 타입의 빈이 충돌 났는지 확인한다 (
EntityManagerFactory). - 중복된
@Primary선언이 없는지 확인하고, 메인 빈 하나에만 선언한다. - 프로젝트 성격에 따라
spring.jpa.open-in-view옵션을false로 끄는 것을 고려한다.
멀티 데이터 소스 환경은 설정이 까다롭지만, 빈의 우선순위와 사용 목적만 명확히 한다면 안정적으로 운영할 수 있습니다. 이 글이 여러분의 디버깅 시간을 단축하는 데 도움이 되기를 바랍니다.