6.7 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 / webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01 | error-note | raw |
|
|
|
2026-06-01 | resolved |
error: webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01
Layer:
raw/errors/— 실제 발생한 오류 / 막힘 / 트러블슈팅의 원석. canonical 정제 전 raw 후보이며,wiki/blog/또는wiki/troubleshooting/직접 생성 근거가 아니다.
Parent / 부모
- raw/branch-notes/feature-operational-error-observability-foundation — Phase C2 구현 중 새 계약 테스트(
EnvelopeMetaContractTest)를app-bootstrap에 추가하다가 발생.
증상
Phase C2 (envelope meta 객체 + error.category 도입) 구현 후 ./gradlew test 에서 app-bootstrap:test 만 5건 실패. 두 부류:
- 신규
EnvelopeMetaContractTest(3건) — 전부 컨텍스트 로드 단계 실패:problemdetails_is_pinned_off()→org.springframework.boot.context.properties.bind.BindException→IllegalStateException at Assert.java:101- 나머지 2건도
DefaultCacheAwareContextLoaderDelegate컨텍스트 로드 실패.
- 기존
OperationalContractRuntimeTest(2건) — Phase C2 이전엔 통과하던 회귀:operationalContractBeans_areWiredIntoTheApplicationContext()→AssertionError: [EnvelopeBodyAdvice must be component-scanned ...] Expecting actual not to be empty(즉EnvelopeBodyAdvice빈이 컨텍스트에 없음)envelopeAdvice_wrapsRawControllerBody()→PathNotFoundExceptionon$.success(응답이 envelope 로 안 감싸짐)
shared-contract / adapter-web / sample-portfolio 의 자체 슬라이스 테스트는 전부 통과 — 문제는 app-bootstrap 컨텍스트에 국한.
조사 단계 / Investigation log
정적 추론으로는 "왜 @RestControllerAdvice 가 컴포넌트 스캔에서 빠지나"가 안 풀려, 격리 실험으로 좁혔다 (working tree 미커밋 상태였으므로 비파괴적 진단 사용):
git stash -u로 전체 변경 임시 제거 → 원본(c36b764)에서OperationalContractRuntimeTest실행 → BUILD SUCCESSFUL. → 내 변경이 회귀 원인 확정.git stash pop으로 복원.- yaml 2개(
application.yml/application-test.yml)만git stash push -- <files>로 격리 → 여전히 실패. → config 무관, Java 변경이 원인. - 신규
EnvelopeMetaContractTest.java만/tmp로mv한 뒤OperationalContractRuntimeTest실행 → BUILD SUCCESSFUL. →EnvelopeMetaContractTest의 존재 자체가 같은 패키지의 다른 테스트를 오염시킨다고 확정.
근본 원인 / Root cause
신규 EnvelopeMetaContractTest 가 OperationalContractRuntimeTest 와 동일 패키지 (dev.caskeleton.bootstrap.runtime) 에 있으면서, 내부에 nested @SpringBootConfiguration static class TestBootstrap 를 선언했다.
@WebMvcTest는 명시적 config 가 없으면AnnotatedClassFinder(SpringBootConfiguration.class)로 테스트 클래스 패키지에서@SpringBootConfiguration을 찾아 컨텍스트 소스로 삼는다.OperationalContractRuntimeTest는 원래CaSkeletonApplication(@SpringBootApplication,scanBasePackages="dev.caskeleton") 을 찾아 그 컴포넌트 스캔으로EnvelopeBodyAdvice/GlobalExceptionHandler를 등록했다.- 같은 패키지에 두 번째
@SpringBootConfiguration(EnvelopeMetaContractTest.TestBootstrap) 가 생기자 config 탐지가 교란됐다.TestBootstrap은@EnableAutoConfiguration만 있고scanBasePackages가 없어, 그 컨텍스트에는EnvelopeBodyAdvice가 스캔되지 않는다 → 빈 부재 + 응답 미-wrap. - 별개로
EnvelopeMetaContractTest자신의BindException은, 그 슬라이스가 test 프로파일 없이 productionapplication.yml을 로드해${OIDC_ISSUER_URI}등 미해소 placeholder 가@Validatedsettings 의Assert.state(...)를 깨뜨린 것.
핵심 교훈: @SpringBootConfiguration(또는 nested 형태)을 다른 Spring Boot 테스트와 같은 패키지에 두면, 그 패키지의 config 자동 탐지를 조용히 오염시킬 수 있다. 컴파일/실행은 되지만 다른 테스트의 컨텍스트가 바뀐다.
해결 / Resolution
EnvelopeMetaContractTest 를 폐기하고, 검증 대상 3개 컴포넌트(EnvelopeBodyAdvice/GlobalExceptionHandler/RequestLoggingFilter)가 모두 adapter-web 소속이라는 점에 착안해 adapter-web 의 standalone MockMvc 통합 테스트(EnvelopeMetaIntegrationTest)로 재작성:
mvc = MockMvcBuilders.standaloneSetup(new Probe())
.addFilter(new RequestLoggingFilter())
.setControllerAdvice(new EnvelopeBodyAdvice(), new GlobalExceptionHandler())
.build();
- Spring 컨텍스트가 없으므로 production placeholder 바인딩도,
@SpringBootConfiguration오염도, security 필터 체인도 없다. - 필터가 실제로 돌아 snake_case MDC 를 채우므로
meta.requestId/meta.traceId가 채워진 채로 success/5xx 두 경로를 검증. problemdetails.enabled=false의 env-property 단언은 standalone 에서 불가 → 폐기. ProblemDetail 금지는 이미 ArchUnitno_problem_detail_usage규칙 +application.yml핀이 커버.
검증: ./gradlew check (전 모듈 test + verifyCleanArchitectureDependencies + ArchUnit CleanArchitectureTest) BUILD SUCCESSFUL.
회고 / Lessons (재발 방지)
- Spring Boot 슬라이스 테스트(
@WebMvcTest등)의 nested@SpringBootConfiguration은 같은 패키지의 다른 테스트와 충돌할 수 있다. 슬라이스가 자기만의 config 가 필요하면 (a) 전용 패키지로 분리하거나 (b)@ContextConfiguration으로 명시 지정하거나 (c) 애초에 컨텍스트 없는 standalone MockMvc 를 쓴다. - adapter 컴포넌트만으로 검증 가능한 계약은
app-bootstrap풀 컨텍스트가 아니라 해당 adapter 모듈의 standalone 테스트가 더 견고하고 빠르다 (placeholder/security 부담 없음). - 미커밋 상태에서 회귀 원인 격리는
git stash -u/git stash push -- <files>/ 파일mv의 비파괴 실험이 가장 확실 — 정적 추론보다 한 번의 격리 실행이 빠르다.