Files
llm-wiki/raw/errors/webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20.md
T

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
feature-test-taxonomy-fixture-contract
feature-rate-limit-idempotency-contract
ca-skeleton
error
ca-skeleton
spring-boot
configuration-properties
webmvctest
env-placeholder
enum-binding
2026-06-20 resolved

error: webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20

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

Parent / 부모

증상 / 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/.envAPP_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.