5.4 KiB
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 |
|
|
|
2026-06-14 | resolved |
error: spanerrorrecorder-constructor-breaks-webmvctest-slice-2026-06-14
Layer:
raw/errors/— 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
Parent / 부모
- raw/branch-notes/feature-distributed-tracing-contract — D12
SpanErrorRecorderseam 을GlobalExceptionHandler에 주입하면서 발생. - raw/project-notes/ca-skeleton-operational-contract — adapter-web 의 base 운영 핸들러가 모든 모듈/테스트 컨텍스트에서 wiring 되어야 하는 cross-module 계약 이슈.
증상 / 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:test—OperationsControllerWireTest/WorkLogControllerWireTest등 34개@WebMvcTest슬라이스 테스트가Failed to load ApplicationContext로 실패. - 발생 시점: 2026-06-14, distributed-tracing Slice 2(adapter-web)에서
GlobalExceptionHandler(SpanErrorRecorder)생성자 추가 직후. - 재현 가능 여부:
always—app-bootstrap에 NOOP bean 을 등록해도 @WebMvcTest 슬라이스에는 보이지 않음.
재현 절차 / Reproduction
GlobalExceptionHandler에SpanErrorRecorder단일 생성자 파라미터를 추가.- NOOP 기본값을
app-bootstrap의@Configuration에@Bean @ConditionalOnMissingBean SpanErrorRecorder = NOOP로만 등록. cd src && ./gradlew :sample-portfolio:test실행.- 기대 결과: 모든 wire 테스트 통과.
- 실제 결과:
@WebMvcTest(controllers=...) + @Import({Controller, GlobalExceptionHandler.class, ...})슬라이스가SpanErrorRecorderbean 부재로 컨텍스트 로드 실패.
근본 원인 / 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@ConditionalOnMissingBeanbean 과OperationalContractRuntimeTest의 보조@Import(TracingConfig.class)는 제거. - 효과: 모든 컨텍스트가 bean 없이 NOOP 로 self-wire; fork 가 tracer-backed
SpanErrorRecorderbean 을 기여하면ObjectProvider가 자동 pickup → override. - 검증 방법:
cd src && ./gradlew check→ BUILD SUCCESSFUL, 1091/1091 tests, 0 failures (이전 34 실패 전부 해소).verifyCleanArchitectureDependencies+ ArchUnitCleanArchitectureTest통과 (ObjectProvider 추가는 shared-contract import 만 사용 — 모듈 경계 무영향).
- 잔여 위험 / 후속 작업: fork 가
SpanErrorRecorderbean 을 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 도 그대로 가능하다.
Related / 관련
- 관련 branch note: raw/branch-notes/feature-distributed-tracing-contract
- 파생 면접/글감: raw/branch-notes/feature-distributed-tracing-contract §Interview prep (ObjectProvider self-default vs @ConditionalOnMissingBean), §Blog topics