빌드노트
@ConfigurationProperties 키를 틀렸는데 정상 시작하는 이유
@ConfigurationProperties는 기본적으로 모르는 키를 무시한다. Boot 3.5.7의 최소 컨텍스트에서 정상 키·오타·표기 변형·누락·prefix 오타를 비교하고, ignoreUnknownFields=false의 적용 범위와 필수값 검증의 차이를 확인한다.
Spring Boot에서 재시도 횟수를 바꿨는데 애플리케이션은 정상 시작하고 예전 기본값을 쓴다면, 설정값보다 설정 이름이 실제 필드에 연결됐는지 먼저 확인한다. max-attempts를 max-attemtps로 적어도 @ConfigurationProperties의 기본 정책은 모르는 이름을 무시한다. 값이 바인딩되지 않으면 필드에 초기화한 기본값이 남을 수 있다.
이 경우에는 해당 설정 빈의 ignoreUnknownFields를 확인한다. ConfigurationProperties API (새 탭에서 열림)에 명시된 기본값은 true다. 모르는 키를 기동 오류로 다루려면 그 빈에 ignoreUnknownFields = false를 지정한다. 필수값의 누락을 막는 검증은 따로 필요하다.
설정을 읽는 빈에서 정책을 바꾼다
먼저 문제의 prefix를 담당하는 클래스를 찾는다. 다음은 demo.retry 아래에서 maxAttempts를 읽는 JavaBean 방식의 설정이다. 기본값은 3이며, 아래 어노테이션은 알 수 없는 키가 들어오면 실패하도록 바꾼 형태다.
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "demo.retry", ignoreUnknownFields = false)
public class RetryProperties {
private int maxAttempts = 3;
public int getMaxAttempts() { return maxAttempts; }
public void setMaxAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; }
}
이미 같은 클래스를 쓰고 있다면 필드와 getter·setter는 유지하고 어노테이션의 옵션을 바꾼다. 이 클래스가 @ConfigurationPropertiesScan의 탐색 대상이거나, 컨텍스트에 들어오는 설정 클래스의 @EnableConfigurationProperties(RetryProperties.class)로 등록되어 있어야 한다. 클래스만 선언하거나 직접 new로 만든 객체를 조회해서는 Boot 바인딩 정책을 확인할 수 없다. 등록 방식은 Boot 설정 빈 안내 (새 탭에서 열림)에서 확인할 수 있다.
자기 프로젝트에서는 실제로 읽히는 application.properties나 테스트 설정에서 demo.retry.max-attempts=9를 먼저 넣어, 주입받은 설정 빈의 getMaxAttempts()가 9인지 확인한다. 다른 prefix를 사용한다면 키와 어노테이션 양쪽을 자기 값으로 바꾼다. 정상 키조차 3으로 남는다면 파일 로딩·활성 프로필·설정 우선순위·빈 등록부터 확인해야 한다. unknown field 정책을 바꾸는 것만으로 읽히지 않는 파일을 찾을 수는 없다.
정상값이 들어오는 것을 확인한 뒤, 같은 입력 위치의 키만 demo.retry.max-attemtps=9로 바꿔 비교한다. 두 키를 동시에 남겨 두지 않는다. 기본 정책에서는 기본값으로 시작할 수 있고, 엄격한 정책에서는 그 오타가 바인딩 실패의 원인으로 드러나야 한다.
같은 오타로 컨텍스트 시작을 비교했다
이 글을 위해 Spring Boot 3.5.7, Spring Framework 6.2.12, OpenJDK 17.0.2를 고정하고 Windows 11에서 별도 최소 컨텍스트를 만들었다. 각 조건마다 새 AnnotationConfigApplicationContext에 Boot의 @EnableConfigurationProperties를 적용하고, 설정 빈 하나를 등록했다. OS 환경변수·시스템 프로퍼티·서비스 설정 파일 대신 합성 property source 하나만 넣었다. HTTP 서버는 열지 않았으며, 여기서 시작 성공은 컨텍스트의 refresh()가 완료되고 해당 빈을 조회할 수 있었다는 뜻이다.
필드와 초기값은 같게 두고 ignoreUnknownFields의 기본 정책과 false를 비교했다. 다음은 실제 출력에서 결과 행만 추린 것이다. STARTED·FAILED와 조건 이름은 실험 코드가 붙인 표시다.
RESULT case=default-correct STARTED maxAttempts=9 beanCount=1
RESULT case=default-typo STARTED maxAttempts=3 beanCount=1
RESULT case=strict-correct STARTED maxAttempts=9 beanCount=1
RESULT case=strict-typo FAILED
정상 키는 양쪽 모두 9로 바인딩됐다. 오타 키만 준 기본 정책에서는 종료 코드가 0이었고, 빈도 하나 존재했지만 getter는 3을 반환했다. 빈이 없어서가 아니라, 오타 키가 필드에 연결되지 않은 채 초기값이 남은 것이다.
엄격한 정책의 오타 조건은 종료 코드 1이었다. 원인 예외는 ConfigurationPropertiesBindException → BindException → UnboundConfigurationPropertiesException 순서였으며, 마지막 메시지에 문제의 키가 나왔다.
The elements [demo.retry.max-attemtps] were left unbound.
자기 환경에서도 바깥쪽의 “설정 바인딩 실패”만 보지 말고 원인 예외와 남은 키를 읽는다. 키가 다른 이름이라면 그 이름을 설정 클래스와 대조한다. 정상 키 demo.retry.max-attempts=9에 추가 키 demo.retry.memo=x를 함께 준 조건도 엄격한 정책에서 실패했고, 남은 키는 demo.retry.memo였다. 이 옵션을 켜면 의도적으로 추가한 설정도 해당 빈이 모르면 거부될 수 있다. 자신이 관리하는 prefix의 허용 키를 정하고 적용해야 한다.
허용되는 표기와 오타를 구별한다
maxAttempts와 max-attempts가 다르게 생겼다는 이유만으로 오타는 아니다. Boot는 relaxed binding으로 일부 표기 차이를 허용한다. 공식 표기 규칙 (새 탭에서 열림)은 properties·YAML의 camelCase, kebab-case, 밑줄 표기를 설명한다.
실험에서도 엄격한 정책을 유지한 채 demo.retry.maxAttempts=9와 demo.retry.max_attempts=9를 각각 넣었을 때 둘 다 종료 코드 0, maxAttempts=9였다. ignoreUnknownFields=false가 relaxed binding을 끄지는 않았다. 반면 max-attemtps는 글자 순서가 바뀌어 실제 필드와 연결되지 않았다.
이 비교는 열거 가능한 합성 property source에 넣은 키를 대상으로 했다. OS 환경변수는 이름 변환 규칙이 별도로 있으므로, 여기서 확인한 밑줄 표기를 그대로 환경변수 이름으로 옮기지는 않는다. 실제 환경변수를 바꿔 실행하는 비교는 하지 않았다.
키 검사가 필수값 검증을 대신하지 않는다
엄격한 정책에서 demo.retry 입력을 전부 생략해도 컨텍스트는 시작했고 값은 3이었다. 모르는 키 자체가 없기 때문이다. 따라서 ignoreUnknownFields=false만으로 “배포할 때 이 값을 반드시 지정했다”는 조건을 보장할 수 없다.
값 자체에 제약이 필요하면 등록된 설정 빈에 Spring의 @Validated와 Jakarta Validation 제약을 적용하고, Bean Validation 구현체가 클래스패스에 있는지 확인한다. 이는 Boot 설정 검증 안내 (새 탭에서 열림)의 별도 기능이다. 예를 들어 값이 없으면 안 되는 숫자는 초기값 없는 Integer와 @NotNull로 표현할 수 있다. int maxAttempts = 3에 최솟값 제약만 붙이면 생략해도 3이 제약을 만족할 수 있다. 이 글의 실험에는 검증 구현체를 추가하지 않았으므로, 해당 제약의 실행 결과를 측정한 것은 아니다.
검사 범위도 prefix에 묶인다. demo.retry를 엄격하게 둔 상태에서 prefix를 틀린 demo.rety.max-attempts=9를 넣었을 때도 시작했고 값은 3이었다. demo.retry 바인딩 대상 밖으로 벗어난 키를 이 빈이 잡아 주지 못했다. 이 옵션을 애플리케이션의 모든 설정 이름을 검사하는 스위치로 보아서는 안 된다.
마지막으로 이름이 맞고 값만 잘못된 경우도 구분한다. 기본 정책에서 demo.retry.max-attempts=abc를 넣은 조건은 ConversionFailedException과 NumberFormatException으로 실패했다. 알 수 없는 키를 무시하는 정책이 잘못된 타입까지 무시한다는 뜻은 아니다. API에서 타입 변환 실패 등에 관련된 ignoreInvalidFields의 기본값은 별도로 false다.
기본값으로 시작하는 증상을 진단할 때는 정상 키가 실제 빈에 바인딩되는지 먼저 확인하고, 같은 prefix 안의 오타·추가 키를 거부할지 정한다. 필수값의 누락과 값의 범위는 그다음 검증 규칙으로 정한다. 이번 관찰은 Boot 3.5.7의 단일 설정 빈과 합성 입력에 한정되며, 실제 서비스의 프로필·외부 설정 공급자·사용자 정의 바인딩 처리까지 검증한 결과는 아니다. 링크한 3.5 계열 문서는 확인 당시 3.5.16을 표시했으며, 기본 옵션은 실행 버전인 3.5.7의 어노테이션 소스 (새 탭에서 열림)와도 대조했다.