3.9 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-slice-configprops-enum-placeholder-no-default-2026-06-20 | error-note | raw |
|
|
|
2026-06-20 | resolved |
error: webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20
Layer:
raw/errors/— 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
Parent / 부모
- raw/branch-notes/feature-test-taxonomy-fixture-contract — test-taxonomy 작업 중
./gradlew test전체 스위트가 RED 인 것을 발견하면서 root-cause.
증상 / Symptom
./gradlew :app-bootstrap:test 전체 실행 시 OperationalContractRuntimeTest 2건 실패(나머지는 통과). 예외 체인:
IllegalStateException: Failed to load ApplicationContext (@WebMvcTest(CaSkeletonApplication.class))
└ UnsatisfiedDependencyException
└ ConfigurationPropertiesBindException
└ BindException
└ ConversionFailedException
└ IllegalArgumentException (LenientObjectToEnumConverterFactory.java:93)
상세 메시지:
Failed to bind properties under 'ca-skeleton.rate-limit.client-ip-mode'
to dev.caskeleton.adapter.web.ratelimit.RateLimitClientIpMode
Failed to convert String -> RateLimitClientIpMode for value [${APP_RATE_LIMIT_CLIENT_IP_MODE}]
근본 원인 / Root cause
application.yml 의 placeholder 가 기본값 없이 선언됨:
ca-skeleton:
rate-limit:
client-ip-mode: ${APP_RATE_LIMIT_CLIENT_IP_MODE} # ← :default 없음
값은 src/.env 의 APP_RATE_LIMIT_CLIENT_IP_MODE=remote-addr-only 에만 존재. bootRun 은 working dir 가 src/ 라 .env 를 읽지만, ./gradlew test 는 .env 를 안 읽는다. 그래서 @WebMvcTest(CaSkeletonApplication.class) 슬라이스가 @ConfigurationPropertiesScan 으로 RateLimitProperties 를 eager 바인딩할 때 placeholder 가 미해석 리터럴 ${...} 로 남고, enum(REMOTE_ADDR_ONLY/FORWARDED_HEADERS_TRUSTED) 변환에 실패 → context load 실패.
함정: RateLimitSettings record 의 compact constructor 에 if (clientIpMode == null) clientIpMode = REMOTE_ADDR_ONLY; null-default 가 있으나, 미해석 placeholder 는 null 이 아니라 non-null 쓰레기 문자열이라 생성자 도달 전 변환 단계에서 터진다 → null-coalescing default 는 이 케이스를 못 막는다.
레지스트리(docs/registries/env-keys.yaml)는 이 키를 required: false, default: remote-addr-only 로 선언 — 즉 application.yml 이 레지스트리 의도와 어긋나 있었다(${VAR} = required 형식인데 레지스트리는 optional).
해결 / Fix
application.yml 에 레지스트리가 선언한 기본값을 인코딩:
client-ip-mode: ${APP_RATE_LIMIT_CLIENT_IP_MODE:remote-addr-only}
이러면 .env 없이도 슬라이스 부팅, 그리고 verifyEnvKeys(“${VAR}=required, ${VAR:default}=optional”)가 레지스트리 required:false 와 정합. 검증: :app-bootstrap:test --tests '*OperationalContractRuntimeTest' PASS + verifyEnvKeys: OK.
교훈 / Lesson
- enum/타입
@ConfigurationProperties를 eager 바인딩하는 슬라이스 테스트(@WebMvcTest(App.class)류)가 있으면, 그 키의 application.yml placeholder 는 반드시:default를 가져야.env없는 test/CI 에서 부팅된다. required:false+default를 레지스트리에 적었다면 application.yml 도${VAR:default}로 맞춰야 한다(verifyEnvKeys 게이트와 정합).- 미해석 placeholder 는 null 이 아니므로 record/생성자 null-default 로는 못 막는다.
- pre-existing 여부 입증법:
git stash push -u로 작업 전부 제거 → clean HEAD 에서 동일 실패 재현 →git stash pop.