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

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
spring-boot
component-scan
multimodule
bean-definition-override
Clean-Architecture
test-context
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 내부의 DomainContextConfigsample-portfolio 내부의 SampleDomainContextConfig가 둘 다 스캔 범위 내에 들어가게 되었고, 동일한 이름인 domainContextPropagator라는 빈을 이중 등록하려고 시도하면서 BeanDefinitionOverrideException이 발생했다.

추가적인 시도와 부작용 (Separate @ComponentScan)

이를 피하기 위해 CaSkeletonApplication.java에 별도의 @ComponentScanexcludeFilters를 적용했다:

@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@ConfigurationPropertiesScanscanBasePackages/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 {
    // ...
}

이 방식을 통해:

  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 성공.