본문 바로가기
빌드노트 검색

빌드노트

설정 키가 있는데 @ConditionalOnProperty 빈이 안 생기는 이유

@ConditionalOnProperty는 기본으로 키가 있고 값이 false가 아닐 때 통과한다. 네 입력 상태의 실제 빈 생성 결과를 비교하며 havingValue로 값을 요구하는 조건과 matchIfMissing으로 누락을 허용하는 조건을 구별한다.

feature.digest.enabled=false를 적어 두었는데 @ConditionalOnProperty가 붙은 빈이 생기지 않는다면, 키가 있는지만 확인해서는 원인을 찾기 어렵다. 기본 조건은 키가 존재하고 그 값이 false가 아니어야 통과한다. 반대로 custom 같은 문자열은 기본 조건을 통과할 수 있다. 기능을 true로 켰을 때만 빈이 필요하다면 havingValue="true"를 명시한다.

먼저 대상 빈의 prefix와 name을 합쳐 실제로 어떤 키를 검사하는지 확인한다. 아래는 기존 @Configuration 클래스 안에 둘 수 있는 예제 메서드다. DigestSender는 전송 기능 없이 표식만 보관하는 record DigestSender(String marker) {}라고 두었다. 이 조건이 검사하는 키는 feature.digest.enabled다.

@Bean
@ConditionalOnProperty(prefix = "feature.digest", name = "enabled")
DigestSender digestSender() {
    return new DigestSender("digest");
}

설정 파일에 키가 보인다는 사실과 Spring이 읽은 값도 구별해야 한다. 컨텍스트가 생성된 로컬 진단 코드나 테스트에서 context가 해당 ConfigurableApplicationContext를 가리킨다면, 다음 코드로 키의 존재와 최종 값을 함께 확인할 수 있다.

var env = context.getEnvironment();
System.out.println(env.containsProperty("feature.digest.enabled"));
System.out.println(env.getProperty("feature.digest.enabled"));

별도 컨텍스트에 false를 넣어 실행했을 때 출력은 첫 줄 true, 다음 줄 false였다. 키는 있지만 값 조건을 통과하지 않는 상태다. 키를 넣지 않은 컨텍스트에서는 첫 줄 false, 다음 줄 null이 나왔다. 같은 키가 다른 설정 소스에서 덮였는지 조사할 때도 파일 내용보다 이 컨텍스트의 값을 먼저 기준으로 삼는다.

기본 동작과 옵션의 의미는 ConditionalOnProperty 공식 API (새 탭에서 열림)에 설명돼 있다. 아래 비교는 Spring Boot 3.5.6, Spring Framework 6.2.11, OpenJDK 17.0.2, Windows 11에서 직접 실행한 결과다. 3.5 계열 API 페이지는 확인 당시 3.5.16을 표시하므로, 실행 버전의 규칙은 v3.5.6의 애너테이션 설명 (새 탭에서 열림)과도 대조했다.

true를 요구하려면 havingValue를 적는다

havingValue를 생략하면 “참으로 켜져 있는가”를 검사하는 것이 아니다. 키가 있고 false가 아닌 값을 허용하는 규칙이다. 따라서 feature.digest.enabled=custom도 앞의 기본 조건을 통과했다. 불리언 기능 플래그로 쓸 생각이었다면 의도보다 넓은 조건이다.

같은 digestSender 메서드에서 애너테이션만 다음과 같이 바꾸면 true라는 값을 요구한다. 빈을 만드는 코드는 그대로 둔다.

@ConditionalOnProperty(
    prefix = "feature.digest", name = "enabled", havingValue = "true")

이 조건을 켜는 설정은 feature.digest.enabled=true다. 직접 비교했을 때 true에서는 빈이 있었고, false와 custom에서는 없었다. 키를 아예 제공하지 않아도 빈은 없었다. havingValue는 원하는 값의 문자열을 적는 속성이며, 별도 대조에서 havingValue="false"는 입력이 false일 때, havingValue="custom"은 입력이 custom일 때만 통과했다.

이 버전의 비교 구현은 문자열의 대소문자를 구별하지 않는다. 다만 여기서 실행한 입력은 소문자 세 값이며, 공백이나 다른 불리언 표기까지 시험한 것은 아니다. 구현은 v3.5.6 OnPropertyCondition (새 탭에서 열림)에서 확인할 수 있다.

matchIfMissing은 값이 없을 때만 적용된다

설정을 생략한 환경에서도 기본으로 기능을 켜려면 같은 메서드에 matchIfMissing=true를 더할 수 있다. 다음 조건은 키가 없거나 값이 true일 때 빈을 허용한다.

@ConditionalOnProperty(
    prefix = "feature.digest", name = "enabled",
    havingValue = "true", matchIfMissing = true)

matchIfMissing은 존재하는 값을 덮어쓰는 옵션이 아니다. feature.digest.enabled=false가 있으면 여전히 거부하며, custom도 true와 다르므로 거부한다. 누락된 설정에 실제로 true 값을 추가하는 것도 아니다. 키가 없는 경우 이 조건을 통과시킬지만 정한다.

각 입력과 조건을 별도 컨텍스트로 실행해 digestSender 빈 존재를 확인했다. 다음은 그 관찰 중 세 조건을 추린 것이다. 있음은 빈이 등록됐다는 뜻이고, 없음은 등록되지 않았다는 뜻이다.

입력      기본 조건   havingValue="true"   havingValue="true" + matchIfMissing=true
누락      없음        없음                 있음
true      있음        있음                 있음
false     없음        없음                 없음
custom    있음        없음                 없음

havingValue 없이 matchIfMissing=true만 설정한 경우도 확인했다. 누락에서는 빈이 생겼지만, false에서는 없었고 custom에서는 생겼다. 누락 허용과 값 비교는 따로 선택해야 한다. 반대로 false까지 포함해 키의 존재만 검사하려는 요구라면 기본 애너테이션 규칙과 다르다. havingValue=""나 matchIfMissing=true를 존재 검사 옵션으로 해석해서는 안 되며, 별도 조건을 설계해야 한다.

누락과 값 불일치는 조건 보고서에서도 다르다

기본 조건의 보고서에는 키를 넣지 않았을 때 did not find property 'enabled', false를 넣었을 때 found different value in property 'enabled'가 남았다. 앞의 메시지는 키 누락을, 뒤의 메시지는 값 비교 실패를 뜻한다. 두 경우 모두 빈은 없지만 고쳐야 할 입력은 다르다.

컨텍스트를 확인할 수 있다면 ConditionEvaluationReport API (새 탭에서 열림)의 get(context.getBeanFactory()).getConditionAndOutcomesBySource()로 조건별 결과를 조회할 수 있다. 이 예제처럼 메서드에 조건을 붙인 경우에는 소스 이름이 #digestSender로 끝나는 항목에서 OnPropertyCondition의 결과를 본다. 클래스에 붙인 조건이라면 해당 클래스 항목을 확인한다. 조건이 통과했는데도 원하는 빈이 없다면, 그 결과만으로 전체 등록 경로까지 정상이라고 판단할 수는 없다.

이번 비교는 네 입력 상태와 여섯 조건 조합을 새 컨텍스트 24개로 나눴다. 조건마다 서로 다른 구성 클래스 이름을 사용했고 한 컨텍스트에는 하나만 등록했다. 모든 컨텍스트에서 빈 존재와 조건 보고서가 일치했다. 다른 설정이 섞이지 않도록 컨텍스트의 속성 소스에는 실험 입력만 넣었으며, 설정 파일·프로필·운영 환경 변수를 불러온 애플리케이션을 재현한 것은 아니다.

여기서 정할 것은 단일 속성의 활성화 정책이다. 값을 명시한 환경에서만 켤지, 생략해도 켤지를 따로 고르면 된다. 컬렉션이나 feature.digest.enabled[0] 같은 인덱스 속성에는 이 결과를 적용하지 않는다. 공식 API도 컬렉션 속성 판단에는 이 조건을 신뢰해서 쓰지 말라고 설명한다.

← 전체 글 보기