5.1 KiB
5.1 KiB
title, source_type, status, tags, created
| title | source_type | status | tags | created | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Spring Boot multi-module component scan overlap causes BeanDefinitionOverrideException | error-note | raw |
|
2026-06-23 |
Multi-module component scan overlap causes BeanDefinitionOverrideException
Parent
raw/branch-notes/feature-build-release-supply-chain-contract
현상
Spring Boot 애플리케이션 시작 또는 테스트 기동 시 다음 예외가 발생하며 컨텍스트 초기화가 실패한다:
org.springframework.beans.factory.support.BeanDefinitionOverrideException:
Invalid bean definition with name 'domainContextPropagator'
defined in class path resource [dev/caskeleton/sample/portfolio/bootstrap/context/SampleDomainContextConfig.class]:
Cannot register bean definition [...] for bean 'domainContextPropagator'
since there is already [...] bound.
원인
- 상위 패키지 스캔의 한계: 프로덕션 모듈의 실행 진입점인
CaSkeletonApplication은@SpringBootApplication(scanBasePackages = "dev.caskeleton")을 선언하여dev.caskeleton하위의 모든 컴포넌트를 스캔하고 있었다. - 테스트 스코프 모듈의 노출:
sample-portfolio모듈은 테스트 시에만 로드되는 테스트 스코프 의존성이었으나, 테스트 런타임 클래스패스에 올라오면서dev.caskeleton.sample.portfolio패키지도 최상위 패키지인dev.caskeleton에 포함되게 되었다. - 빈 정의 충돌: 이로 인해
app-bootstrap내부의DomainContextConfig와sample-portfolio내부의SampleDomainContextConfig가 둘 다 스캔 범위 내에 들어가게 되었고, 동일한 이름인domainContextPropagator라는 빈을 이중 등록하려고 시도하면서BeanDefinitionOverrideException이 발생했다.
추가적인 시도와 부작용 (Separate @ComponentScan)
이를 피하기 위해 CaSkeletonApplication.java에 별도의 @ComponentScan과 excludeFilters를 적용했다:
@SpringBootApplication
@ComponentScan(
basePackages = "dev.caskeleton",
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "dev\\.caskeleton\\.sample\\.portfolio\\..*")
})
하지만 이 방식을 도입하자, Spring Boot의 기본 컴포넌트 스캔 자동 설정이 완전히 오버라이드(override)되어 무력화되었다. 그 결과 Spring Boot가 테스트 클래스 패키지에 포함된 내부 static @Configuration들을 필터링하기 위해 사용하던 기본 필터들(TypeExcludeFilter, AutoConfigurationExcludeFilter)이 동작하지 않아, 다른 테스트 클래스들의 nested @Configuration 빈 정의가 마구잡이로 스캔되어 또 다른 BeanDefinitionOverrideException 연쇄 충돌을 일으켰다.
해결
가장 깔끔하고 부작용이 없는 해결책은 별도의 @ComponentScan 선언을 배제하고, @SpringBootApplication 및 @ConfigurationPropertiesScan의 scanBasePackages/basePackages 속성에 프로덕션에서 스캔해야 할 패키지 목록을 구체적인 문자열 배열로 직접 명시하는 것이다.
@SpringBootApplication(
scanBasePackages = {
"dev.caskeleton.bootstrap",
"dev.caskeleton.adapter",
"dev.caskeleton.application",
"dev.caskeleton.domain",
"dev.caskeleton.shared"
})
@ConfigurationPropertiesScan(
basePackages = {
"dev.caskeleton.bootstrap",
"dev.caskeleton.adapter",
"dev.caskeleton.application",
"dev.caskeleton.domain",
"dev.caskeleton.shared"
})
public class CaSkeletonApplication {
// ...
}
이 방식을 통해:
dev.caskeleton.sample.portfolio패키지를 스캔 대상에서 원천적으로 제외하여 빈 충돌을 차단한다.- Spring Boot가 제공하는 기본
@ComponentScan필터들이 올바르게 보존 및 동작하여, 다른 테스트 내 nested@Configuration들이 오버스캔되지 않는다. - 아키텍처적으로 모듈 경계가 명확하게 보호된다.
정리 (Lessons)
- Clean Architecture 또는 멀티모듈 구조에서 최상위 공통 패키지(
dev.caskeleton) 기준의 광범위 스캔은 타 모듈(예: 테스트 전용 샘플 모듈) 클래스패스 유입 시 원치 않는 빈 정의 충돌을 야기하기 쉽다. @SpringBootApplication에 별도의@ComponentScan어노테이션을 덮어씌우면 Spring Boot 내부의 중요한 컴포넌트 스캔 제외 필터들이 무력화되므로 지양해야 한다.- 명시적으로 허용할 프로덕션 패키지 목록을 나열하여 스캔 대상을 좁히는 기법이 가장 안전하고 명확하다.
재현 환경
- Spring Boot 3.4.x, Java 21, Gradle 9.0
app-bootstrap구동 및:app-bootstrap:test실행 시 발생- 해결 후: 전체 테스트 통과 (BUILD SUCCESSFUL)
Evidence
actually-implemented:src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java수정 적용.locally-verified:cd src && ./gradlew test성공.