Files
llm-wiki/raw/errors/spanerrorrecorder-constructor-breaks-webmvctest-slice-2026-06-14.md
T

5.4 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / spanerrorrecorder-constructor-breaks-webmvctest-slice-2026-06-14 error-note raw
feature-distributed-tracing-contract
ca-skeleton
error
ca-skeleton
spring
dependency-injection
webmvctest
conditionalonmissingbean
objectprovider
tracing
2026-06-14 resolved

error: spanerrorrecorder-constructor-breaks-webmvctest-slice-2026-06-14

Layer: raw/errors/ — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.

Parent / 부모

증상 / Symptom

  • 에러 메시지 (원문 그대로):
    org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean
    with name 'dev.caskeleton.adapter.web.error.GlobalExceptionHandler': Unsatisfied dependency
    expressed through constructor parameter 0: No qualifying bean of type
    'dev.caskeleton.shared.tracing.SpanErrorRecorder' available
    
  • 발생 컨텍스트: cd src && ./gradlew check:sample-portfolio:testOperationsControllerWireTest / WorkLogControllerWireTest34개 @WebMvcTest 슬라이스 테스트가 Failed to load ApplicationContext 로 실패.
  • 발생 시점: 2026-06-14, distributed-tracing Slice 2(adapter-web)에서 GlobalExceptionHandler(SpanErrorRecorder) 생성자 추가 직후.
  • 재현 가능 여부: alwaysapp-bootstrap 에 NOOP bean 을 등록해도 @WebMvcTest 슬라이스에는 보이지 않음.

재현 절차 / Reproduction

  1. GlobalExceptionHandlerSpanErrorRecorder 단일 생성자 파라미터를 추가.
  2. NOOP 기본값을 app-bootstrap@Configuration@Bean @ConditionalOnMissingBean SpanErrorRecorder = NOOP 로만 등록.
  3. cd src && ./gradlew :sample-portfolio:test 실행.
  4. 기대 결과: 모든 wire 테스트 통과.
  5. 실제 결과: @WebMvcTest(controllers=...) + @Import({Controller, GlobalExceptionHandler.class, ...}) 슬라이스가 SpanErrorRecorder bean 부재로 컨텍스트 로드 실패.

근본 원인 / Root cause

  • 직접 원인: @WebMvcTest 슬라이스는 web 레이어 component 와 명시적 @Import 만 로드하고, 임의 @Configuration(여기선 bootstrap 의 TracingConfig)을 component-scan 하지 않는다. 따라서 @ConditionalOnMissingBean 으로 등록한 bootstrap NOOP bean 이 슬라이스 컨텍스트에 등장하지 않는다.
  • 근본 원인: base 핸들러(GlobalExceptionHandler)는 모든 컨텍스트(풀 부트 / @WebMvcTest 슬라이스 / 유닛)에서 wiring 되어야 하는 운영 계약인데, 의존성의 기본값을 다른 모듈의 bean 등록에 의존하게 두면 슬라이스 컨텍스트가 깨진다. 기본값은 의존성을 도입한 클래스 자신이 self-default 하는 것이 견고하다.
  • 트리거 조건: 새 의존성을 "생성자 필수 파라미터 + 외부 모듈 bean" 조합으로 도입.

해결 / Resolution

  • 적용한 조치: GlobalExceptionHandler@Autowired ObjectProvider<SpanErrorRecorder> 생성자를 추가하고 getIfAvailable(() -> SpanErrorRecorder.NOOP) 로 직접 생성자에 위임. 직접 SpanErrorRecorder 생성자는 테스트(capturing recorder)용으로 유지. bootstrap 의 redundant @ConditionalOnMissingBean bean 과 OperationalContractRuntimeTest 의 보조 @Import(TracingConfig.class) 는 제거.
  • 효과: 모든 컨텍스트가 bean 없이 NOOP 로 self-wire; fork 가 tracer-backed SpanErrorRecorder bean 을 기여하면 ObjectProvider 가 자동 pickup → override.
  • 검증 방법:
    • cd src && ./gradlew checkBUILD SUCCESSFUL, 1091/1091 tests, 0 failures (이전 34 실패 전부 해소).
    • verifyCleanArchitectureDependencies + ArchUnit CleanArchitectureTest 통과 (ObjectProvider 추가는 shared-contract import 만 사용 — 모듈 경계 무영향).
  • 잔여 위험 / 후속 작업: fork 가 SpanErrorRecorder bean 을 2개 이상 등록하면 getIfAvailable 가 ambiguous 로 throw — 표준 Spring 동작이며 fork 책임.

회고 / Lessons

  • 빨리 감지하는 신호: 새 생성자 의존을 추가한 base 컴포넌트가 슬라이스 테스트(@WebMvcTest/@DataJpaTest)에서만 깨지면, "슬라이스가 @Configuration 을 스캔하지 않는다"를 먼저 의심.
  • 예방 체크리스트: 여러 컨텍스트에서 쓰이는 base 컴포넌트에 선택적 협력자를 추가할 땐, 외부 모듈 bean 에 기대지 말고 ObjectProvider<T> + 기본 구현으로 self-default 하라. 테스트 ergonomics 를 위해 직접 생성자도 함께 둔다(주입 진입점에만 @Autowired).
  • 일반화된 교훈: "기본값을 어디서 제공하는가"는 아키텍처 결정이다 — 소비자(같은 모듈) self-default 가 composition-root bean 보다 컨텍스트 견고성이 높고, seam override 도 그대로 가능하다.