빌드노트
직접 등록한 Bean이 있는데도 자동 설정 Bean이 생긴다
사용자 @Bean이 JsonCodec 객체를 반환해도 Object로 선언하면 자동 설정의 타입 조건이 그 Bean을 놓칠 수 있다. ApplicationContextRunner에서 반환 타입과 자동 설정 순서를 바꿔 비교하고, 조건 보고서로 원인을 확인하는 방법을 설명한다.
직접 만든 객체를 @Bean으로 등록했는데 자동 설정의 기본 Bean까지 생긴다면, 사용자 메서드의 반환 타입부터 확인한다. 자동 설정이 찾는 타입은 JsonCodec인데 사용자 메서드가 Object만 노출하면, 조건 평가 시 그 Bean을 JsonCodec으로 알아보지 못할 수 있다. 메서드 안에서 new JsonCodec(...)을 반환한다는 사실만으로는 충분하지 않다.
먼저 문제가 생긴 자동 설정의 @ConditionalOnMissingBean과 사용자 설정의 @Bean을 나란히 본다. 다음은 이 차이를 확인하기 위해 만든 예제다. JsonCodec은 출처를 구분하는 문자열만 가진 타입이며 실제 라이브러리의 설정을 재현한 것은 아니다.
// 자동 설정 안의 메서드
@Bean
@ConditionalOnMissingBean(JsonCodec.class)
JsonCodec autoCodec() {
return new JsonCodec("auto");
}
// 사용자 설정 안의 메서드
@Bean
Object userCodec() {
return new JsonCodec("user");
}
여기서 조건은 userCodec이라는 이름을 찾지 않는다. JsonCodec 타입의 Bean이 없는지를 검사한다. 또한 이 애너테이션은 없다는 조건이 성립하면 기본 Bean을 등록하고, 이미 있으면 기본 Bean 등록에서 물러난다. 조건 보고서의 match=true를 “내 Bean을 발견했다”로 읽으면 뜻이 반대가 된다. 타입 조건과 이름 조건의 구분은 ConditionalOnMissingBean API (새 탭에서 열림)에 설명돼 있다.
사용자 설정에서 바꿀 부분은 반환 선언이다. 이 예제에서는 이름과 생성할 객체를 그대로 두고 Object를 JsonCodec으로 바꾼다.
@Bean
JsonCodec userCodec() {
return new JsonCodec("user");
}
자동 설정에 타입 인자가 없는 @ConditionalOnMissingBean만 붙어 있다면, 검사 대상은 그 애너테이션이 붙은 @Bean 메서드의 반환 타입에서 정해진다. 사용자 메서드가 노출하는 타입과 자동 설정이 검사하는 타입을 각각 확인해야 한다.
같은 객체를 반환해도 등록 결과는 달랐다
Windows의 JDK 17.0.2, Spring Boot 3.5.7, Spring Framework 6.2.12에서 작은 ApplicationContextRunner를 실행했다. 자동 설정은 withConfiguration(AutoConfigurations.of(...)), 사용자 설정은 withUserConfiguration(...)으로 넣어 일반 사용자 설정이 자동 설정보다 먼저 처리되는 구성을 사용했다. 매번 새 컨텍스트를 열고 닫았으며 외부 서비스는 연결하지 않았다.
구체 타입과 Object 반환 메서드는 서로 다른 사용자 설정 클래스에 두었다. 다음은 각 실행에서 JsonCodec Bean의 이름과 출처를 모아 정렬한 실제 출력이다.
CASE concrete-user
after-refresh JsonCodec.beans={userCodec=user}
CASE object-user
after-refresh JsonCodec.beans={autoCodec=auto, userCodec=user}
구체 타입 선언에서는 자동 설정이 물러났고, Object 선언에서는 둘 다 생겼다. 사용자 설정 자체를 빼면 autoCodec 하나만 생겼다. 두 반환 선언 모두 실행 후 userCodec 객체의 클래스는 BackoffLab$JsonCodec이었다. 나중에 얻은 객체의 클래스가 같아도, 앞서 조건을 판단할 때 이용할 수 있었던 정보는 달랐다.
싱글턴 생성 전의 별도 조회에서도 차이가 보였다. Object 선언에서는 userCodec의 예측 타입이 java.lang.Object였고 아직 생성된 싱글턴은 없었다. 이때 JsonCodec 타입 이름 조회에는 autoCodec만 잡혔다. 구체 타입 선언에서는 예측 타입이 BackoffLab$JsonCodec이었으며 조회 결과에 userCodec만 잡혔다. 이 조회는 Bean 정의 처리가 끝난 뒤의 관찰이고, 조건을 평가하는 순간에 끼워 넣은 계측은 아니다.
조건 판정 자체는 ConditionEvaluationReport로 따로 읽었다. Object 선언에서는 fullMatch=true와 did not find any beans가 나왔고, 구체 타입 선언에서는 fullMatch=false와 userCodec을 찾았다는 이유가 남았다. 실행 후 타입 조회 결과를 조건 평가 당시의 결과로 대신하지 않은 이유다.
공식 자동 설정 작성 지침 (새 탭에서 열림)도 Bean 조건을 사용할 때 반환 타입에 가능한 한 구체적인 타입 정보를 제공하라고 설명한다. 실행한 3.5.7의 조건 구현 (새 탭에서 열림)에서는 Bean을 미리 초기화하도록 요청하지 않는 타입 조회를 사용한다. 이 실험 결과는 메서드 본문의 생성식을 읽어 타입을 추론하는 방식으로 설명할 수 없다.
Object 대신 상위 인터페이스를 반환해도 이 문제가 생길 수 있다. 추가 비교에서 JsonCodec implements Codec으로 두고 사용자 반환 선언만 Codec으로 바꾸자, JsonCodec.class를 찾는 자동 설정은 계속 활성화됐다. 인터페이스 반환이 항상 잘못이라는 뜻은 아니다. 실제 자동 설정이 인터페이스를 찾는지, 특정 구현 클래스를 찾는지에 맞춰 판단해야 한다.
내 앱에서는 조건 보고서와 선언을 함께 읽는다
실제 프로젝트에서는 IDE의 애플리케이션 실행 인자에 --debug를 추가하고 다시 시작해 조건 보고서를 확인할 수 있다. 이는 Spring Boot가 안내하는 진단 방법 (새 탭에서 열림)이다. 위 실험은 보고서 API를 직접 조회했으며, 실제 앱의 콘솔 출력을 수집한 것은 아니다.
보고서에서 문제가 된 자동 설정 클래스와 Bean 메서드를 찾고, 검사 타입과 판정 이유를 읽는다. did not find any beans라면 그 시점에 조건에 맞는 후보를 찾지 못한 것이다. 사용자 메서드가 스캔·임포트되는 설정 안에 있는지, 반환 선언이 검사 타입을 드러내는지 확인한다. 이 예제에서는 Object 반환을 고친 뒤, 같은 자동 설정 메서드가 불일치 결과로 바뀌고 사용자 Bean 이름이 이유에 나타나는지 비교하면 된다.
@Primary로 주입 대상을 정해도 자동 설정의 타입 정보가 보강되지는 않는다. 기본 Bean이 생성되는 문제와, 이미 등록된 여러 Bean 중 하나를 주입하는 문제는 구분해야 한다. 같은 이름으로 덮어쓰도록 설정하는 것도 여기서 확인한 원인에 대한 수정은 아니다. 이 실험은 autoCodec과 userCodec이라는 다른 이름을 사용했다.
타입이 맞는데도 물러나지 않으면 등록 순서를 본다
@ConditionalOnMissingBean은 컨텍스트의 최종 모습을 미리 검사하지 않는다. 3.5.7 애너테이션의 설명 (새 탭에서 열림)대로, 그때까지 처리된 Bean 정의를 기준으로 판단한다. 그래서 조건을 일반 설정 곳곳에 흩어 놓으면 처리 순서의 영향을 받는다.
이 한계도 별도로 실행했다. 사용자 설정 대신 두 자동 설정을 사용했고, 한쪽은 구체 타입으로 providedCodec을 등록했다. 다른 쪽은 같은 JsonCodec.class 조건으로 기본 autoCodec을 등록했다.
- 기본 설정을
before = ProviderAutoConfiguration.class로 먼저 평가하자autoCodec과providedCodec이 모두 생겼다. 조건 보고서에는 당시 후보를 찾지 못했다는 결과가 남았다. - 기본 설정을
after = ProviderAutoConfiguration.class로 뒤에 평가하자providedCodec만 남았다. 보고서에는 이 Bean을 발견해 기본 등록을 생략했다는 이유가 남았다.
다른 자동 설정이 대체 Bean을 제공한다면 기본 설정 쪽의 @AutoConfiguration(after = ...) 등으로 정의 처리 순서를 맞춘다. 이것은 객체 생성 순서를 늦추는 것과 다르다. 공식 문서도 자동 설정 순서가 Bean 정의 순서에 영향을 주며, 이후 객체 생성은 의존 관계 등에 따라 결정된다고 구분한다. 단순히 생성자가 나중에 실행됐다는 로그로 이 조건의 순서를 판단하지 않는다.
직접 확인한 범위는 위 버전의 일반 JVM 컨텍스트 여섯 경우다. 부모·자식 컨텍스트, 프록시, FactoryBean, 이미 조기에 생성된 싱글턴, native image는 검증하지 않았다. 링크한 3.5 문서는 확인 당시 3.5.16을 표시했으므로 실행 버전의 조건 설명과 구현도 함께 대조했다. 반환 타입을 고쳐도 결과가 같다면 타입 문제로 단정하지 말고, 보고서가 찾으려 한 타입과 Bean 정의가 처리된 순서를 다음 근거로 삼는다.