Files
llm-wiki/raw/errors/spring-componentcan-multimodule-overlap-collision-2026-06-23.md

97 lines
5.1 KiB
Markdown

---
title: "Spring Boot multi-module component scan overlap causes BeanDefinitionOverrideException"
source_type: error-note
status: raw
tags: [spring-boot, component-scan, multimodule, bean-definition-override, Clean-Architecture, test-context]
created: 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.
```
## 원인
1. **상위 패키지 스캔의 한계**: 프로덕션 모듈의 실행 진입점인 `CaSkeletonApplication``@SpringBootApplication(scanBasePackages = "dev.caskeleton")`을 선언하여 `dev.caskeleton` 하위의 모든 컴포넌트를 스캔하고 있었다.
2. **테스트 스코프 모듈의 노출**: `sample-portfolio` 모듈은 테스트 시에만 로드되는 테스트 스코프 의존성이었으나, 테스트 런타임 클래스패스에 올라오면서 `dev.caskeleton.sample.portfolio` 패키지도 최상위 패키지인 `dev.caskeleton`에 포함되게 되었다.
3. **빈 정의 충돌**: 이로 인해 `app-bootstrap` 내부의 `DomainContextConfig``sample-portfolio` 내부의 `SampleDomainContextConfig`가 둘 다 스캔 범위 내에 들어가게 되었고, 동일한 이름인 `domainContextPropagator`라는 빈을 이중 등록하려고 시도하면서 `BeanDefinitionOverrideException`이 발생했다.
### 추가적인 시도와 부작용 (Separate @ComponentScan)
이를 피하기 위해 `CaSkeletonApplication.java`에 별도의 `@ComponentScan``excludeFilters`를 적용했다:
```java
@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` 속성에 프로덕션에서 스캔해야 할 패키지 목록을 구체적인 문자열 배열로 직접 명시하는 것이다.
```java
@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 {
// ...
}
```
이 방식을 통해:
1. `dev.caskeleton.sample.portfolio` 패키지를 스캔 대상에서 원천적으로 제외하여 빈 충돌을 차단한다.
2. Spring Boot가 제공하는 기본 `@ComponentScan` 필터들이 올바르게 보존 및 동작하여, 다른 테스트 내 nested `@Configuration`들이 오버스캔되지 않는다.
3. 아키텍처적으로 모듈 경계가 명확하게 보호된다.
## 정리 (Lessons)
1. **Clean Architecture 또는 멀티모듈 구조에서 최상위 공통 패키지(`dev.caskeleton`) 기준의 광범위 스캔은 타 모듈(예: 테스트 전용 샘플 모듈) 클래스패스 유입 시 원치 않는 빈 정의 충돌을 야기하기 쉽다.**
2. **`@SpringBootApplication`에 별도의 `@ComponentScan` 어노테이션을 덮어씌우면 Spring Boot 내부의 중요한 컴포넌트 스캔 제외 필터들이 무력화되므로 지양해야 한다.**
3. **명시적으로 허용할 프로덕션 패키지 목록을 나열하여 스캔 대상을 좁히는 기법이 가장 안전하고 명확하다.**
## 재현 환경
- 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` 성공.