자동 구성 과정
자동 구성 후보가 메타데이터·조건 평가·빈 등록으로 이어지는 과정을 확인하고 순서와 사용자 재정의 규칙을 구현합니다.
자동 구성은 컴포넌트 스캔이 우연히 라이브러리의 구성 클래스를 찾아내는 방식이 아닙니다. Boot는 정해진 메타데이터 파일에서 후보를 읽고, 조건을 평가해 구성을 불러옵니다. 그래서 라이브러리가 애플리케이션의 스캔 패키지 밖에 있어도 동작하며, 사용자 빈이 있을 때 기본값이 물러나도록 작성해야 여러 애플리케이션에서 재사용할 수 있는 통합이 됩니다.
이 글에서는 후보가 선택되는 과정, 메타데이터 파일, 구성 사이의 순서, 조건 평가 보고서를 읽는 순서를 차례로 살펴봅니다.
AUTO-CONFIGURATION · FOUR DIAGNOSTIC STAGES
빈 부재의 원인은 후보·조건·양보·생성 실패로 나눈다
먼저 자동 구성 후보와 제외를 확인하고 구성·빈 메서드의 조건을 읽습니다. 긍정 일치는 조건의 결과이며 빈 생성 성공을 뜻하지 않습니다.
1 · 후보 발견
AutoConfiguration.imports 항목·클래스명·제외를 확인합니다. 후보가 없으면 해당 구성의 조건은 평가되지 않으며 보고서에도 없을 수 있습니다.
2 · 클래스와 프로퍼티 조건
요구 클래스와 키·havingValue·matchIfMissing을 확인합니다. 조건 불일치에서는 해당 기본값을 등록하지 않습니다.
3 · 사용자 빈에 양보
빈 메서드의 OnMissingBean 검색 결과를 봅니다. 기본값 부재와 사용자 빈 동일성 보존은 정상 back-off일 수 있습니다.
4 · 등록 이후 생성 실패
조건은 참이어도 팩토리·의존성·검증이 실패할 수 있습니다. 실제 빈 그래프, 시작 예외와 원인 연쇄를 보고서와 함께 봅니다.
before/after는 자동 구성 처리 순서이며 빈 초기화나 런타임 호출 순서를 대신하지 않습니다. 실제 의존성은 빈 파라미터와 명시적 조건으로 표현합니다.
그림의 위쪽은 이 글의 연습 문제에서 만들 시계와 요청 ID 생성기의 관계입니다. Clock 빈은 기본값이거나 사용자가 정의한 것이며 RequestIdGenerator가 그것을 참조하고, 호출자가 넘긴 순번을 next가 밀리초 값과 이어 문자열 ID를 만듭니다. 아래 네 칸은 빈이 없을 때 원인을 찾는 순서입니다. 후보로 발견됐는지, 클래스·속성 조건이 맞았는지, 사용자 빈에 양보했는지, 등록 뒤 생성에 실패했는지를 차례로 나눠 봅니다. 맨 아래 줄은 before와 after가 자동 구성의 처리 순서일 뿐, 빈의 초기화 순서나 런타임 호출 순서를 대신하지 않는다는 점을 짚습니다.
자동 구성 후보
애플리케이션의 구성을 파싱하는 동안, 자동 구성 선택기가 환경과 클래스 로더를 써서 후보를 찾고 제외 설정과 필터를 적용합니다. 일반 사용자 구성을 먼저 처리한 뒤에 자동 구성을 적용하므로, @ConditionalOnMissingBean 조건이 사용자의 재정의를 존중할 수 있습니다.
@SpringBootApplication에는 자동 구성 활성화가 들어 있지만, 테스트 슬라이스나 직접 만든 컨텍스트에서는 그 범위가 다를 수 있습니다. 자동 구성을 단위 테스트할 때는 ApplicationContextRunner처럼 필요한 후보를 명시하는 도구를 씁니다.
package board.autoconfig;
import java.util.Set;
public record AutoConfigInputs(
Set<String> candidates,
Set<String> exclusions,
Set<String> presentClasses
) {
public AutoConfigInputs {
candidates = Set.copyOf(candidates);
exclusions = Set.copyOf(exclusions);
presentClasses = Set.copyOf(presentClasses);
}
public Set<String> eligible() {
return candidates.stream()
.filter(candidate -> !exclusions.contains(candidate))
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
}이 모델은 실제 Boot 선택기를 대신하지 않지만, 후보와 제외가 서로 다른 입력이라는 점을 보여 줍니다. eligible()은 후보에서 제외 목록만 뺄 뿐 presentClasses는 읽지 않습니다. 실제로는 클래스 존재, 속성, 사용자 빈 같은 조건이 선택된 구성 안에서 따로 평가되므로, 이 한 단계의 출력을 모든 조건을 검사한 결과로 해석하지 않습니다.
자동 구성과 스캔 범위
라이브러리의 구성 클래스는 명시적인 후보 메타데이터에 등록하고, 애플리케이션이 직접 불러올 공개 구성 API와 섞지 않습니다. 자동 구성 클래스가 컴포넌트 스캔에 잘못 잡히면, 조건 평가와 순서 지정을 우회해 두 번 등록될 수 있습니다.
package board.autoconfig;
import java.time.Clock;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class BoardClockAutoConfiguration {
@Bean
@ConditionalOnMissingBean(Clock.class)
Clock boardClock() {
return Clock.systemUTC();
}
}사용자가 Clock.fixed(...)로 만든 Clock 빈을 제공하면 이 기본값은 생성되지 않습니다. 빈 이름이 아니라 타입을 기준으로 양보할지, 한정자(qualifier)별로 여러 시계를 허용할지는 라이브러리의 규칙으로 정합니다.
자동 구성 메타데이터
자동 구성 모듈의 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에 구성 클래스의 완전한 이름을 한 줄씩 적습니다. 소스 클래스만 만들고 이 파일을 빠뜨리면, 사용자의 클래스 경로에 JAR이 있어도 후보가 0개입니다.
library class present=true
AutoConfiguration.imports entry=false
candidate count=0
Clock bean created=false
result=AUTO_CONFIGURATION_NOT_IMPORTED릴리스할 JAR 파일을 검증할 때는 메타데이터 항목과 클래스 로딩을 함께 확인합니다. 셰이딩(shading)이나 재배치로 클래스 이름이 바뀐다면 메타데이터도 함께 바뀌어야 합니다.
더 깊이: 후보를 클래스도 읽지 않고 걸러 내는 두 번째 메타데이터
자동 구성 JAR에는 AutoConfiguration.imports 말고도 메타데이터 파일이 하나 더 있을 수 있습니다. Spring Boot 문서에 따르면 Boot는 애노테이션 처리기로 자동 구성에 붙은 조건을 모아 META-INF/spring-autoconfigure-metadata.properties에 기록하고, 이 파일이 있으면 맞지 않는 자동 구성을 미리 걸러 내 시작 시간을 줄입니다. 예를 들어 @ConditionalOnClass가 요구하는 클래스가 클래스 경로에 없다는 것을 이 파일만 보고 판단할 수 있으므로, 그 구성 클래스를 읽고 파싱하는 단계까지 가지 않습니다. 그래서 자동 구성 모듈을 만든다면 Boot 저장소의 spring-boot-autoconfigure-processor를 애노테이션 처리기로 함께 쓰는 편이 좋습니다.
이 거르기를 맡는 것이 AutoConfigurationImportFilter입니다. Spring Boot 4.1.2의 spring-boot-autoconfigure 모듈은 spring.factories에 OnBeanCondition, OnClassCondition, OnWebApplicationCondition 세 조건을 이 필터로 등록해 두고, 인터페이스의 API 문서는 이것이 자동 구성 클래스의 바이트코드를 읽기도 전에 후보를 빠르게 지우려는 장치라고 설명합니다. 그래서 진단 순서에서 후보가 발견됐는지와 클래스 조건이 맞았는지 사이에는, 메타데이터 파일만으로 일찍 탈락하는 단계가 하나 더 있는 셈입니다. 이렇게 일찍 걸러진 후보도 조건 평가 보고서에는 조건이 맞지 않은 항목으로 남으므로, 보고서에서 그 구성 클래스의 이름을 찾는 방법은 같습니다.
자동 구성의 순서
@AutoConfiguration의 before와 after 속성은 구성을 처리하는 순서를 조정하지만, 빈의 초기화 순서나 런타임 호출 순서와는 다릅니다. 실제 의존 관계는 빈 메서드의 파라미터와 조건으로 표현합니다. 다른 자동 구성이 만든 빈이 있을 때만 적용하려면, 순서 지정과 빈 조건을 함께 씁니다.
순서 지정이 서로를 가리키는 순환을 만들지 않고, 왜 그 순서가 필요한지 테스트 이름에 남깁니다. 사용자 구성이 최종 우선권을 갖도록 무조건 등록되는 기본 빈을 피합니다. 빈 정의 덮어쓰기(bean definition overriding)를 켜야만 교체되는 방식보다, 빈이 없을 때만 기본값을 등록하는 방식이 다른 구성과 조합하기에 안전합니다.
조건 평가 보고서
후보 자체가 없으면 보고서에 그 구성이 나타나지 않을 수 있습니다. 후보는 있지만 클래스 조건이 거짓이면 조건이 맞지 않은 항목(negative match)으로 보입니다. 조건은 참인데 빈 팩토리 메서드가 실패했다면, 생성 예외와 원인 연쇄를 봅니다.
--debug나 조건 평가 보고서 로깅은 진단에 쓸모 있지만, 비밀 정보가 담긴 속성 값을 출력하지는 않게 합니다. 라이브러리를 만든다면 조건의 결과 메시지에 무엇을 찾았고 무엇이 없었는지 구체적으로 적습니다.
진단은 후보가 발견됐는지부터 시작합니다. 먼저 AutoConfiguration.imports 메타데이터와 제외 설정으로 후보가 되었는지 보고, 다음으로 클래스·속성 같은 구성 수준의 조건이 통과했는지, 마지막으로 빈 메서드의 @ConditionalOnMissingBean 조건과 생성 예외를 봅니다. 이 순서를 거꾸로 따라가면, 애초에 불러오지도 않은 빈 메서드를 오래 디버깅하게 됩니다.
ConditionEvaluationReport에서 조건이 맞은 항목(positive match)은 빈 생성에 성공했다는 뜻이 아니라 해당 조건이 참이었다는 뜻이고, 조건 없이 적용된 클래스는 별도의 묶음으로 나타날 수 있습니다. 그래서 보고서와 실제 빈 그래프, 시작 실패의 원인을 함께 읽습니다.
candidate absent -> imports/exclusion/package artifact 확인
candidate present + negative configuration match -> class/property/resource 입력 확인
configuration positive + bean negative match -> user bean과 search scope 확인
bean condition positive + startup failure -> factory method cause chain 확인운영 애플리케이션에서 --debug를 늘 켜 두기보다, 실패를 재현한 작은 컨텍스트의 보고서를 산출물로 남깁니다. 같은 구성이 맞은 항목과 맞지 않은 항목 양쪽에 섞여 보이면, 중첩된 구성의 조건과 빈 메서드의 조건을 구분합니다. 릴리스할 때는 이전 버전의 보고서와 비교해, 의존성 업그레이드가 후보를 새로 켜거나 끄지 않았는지 확인합니다.
시작 테스트는 기본 빈 생성, 사용자 재정의로 기본값이 등록되지 않는 경우, 속성으로 비활성화한 경우, 클래스가 없는 경우라는 네 시나리오를 나눠서 둡니다. 전체 애플리케이션 컨텍스트 하나만 띄워서 모든 분기를 확인하지 않습니다.
공개 API와 내부 빈
사용자가 주입해도 되는 타입과 구성 속성은 문서로 공개하고, 내부 도우미 빈의 이름에는 의존하지 않게 합니다. 버전을 올리면서 내부 그래프가 바뀌어도 공개 기능은 유지되어야 합니다. 자동 구성 클래스를 사용자가 직접 @Import하면 조건과 순서, 메타데이터의 처리가 달라질 수 있으므로, 테스트를 위한 도우미가 필요하다면 별도의 공개 테스트 구성을 제공합니다.
정리
- 자동 구성은
AutoConfiguration.imports메타데이터에서 후보를 읽고, 제외와 조건을 거쳐 사용자 구성 다음에 적용됩니다. @ConditionalOnMissingBean으로 사용자 빈이 있을 때 기본값이 물러나게 하며, 무조건 등록하거나 빈 덮어쓰기에 기대지 않습니다.before와after는 구성의 처리 순서일 뿐이고, 실제 의존 관계는 빈 파라미터와 조건으로 표현합니다.- 빈이 없을 때는 후보 발견, 구성 조건, 사용자 빈 양보, 생성 실패 순서로 원인을 좁힙니다.
연습 문제
1. 시계와 요청 ID 생성기의 자동 구성
UTC 시계와 요청 ID 생성기를 제공하는 자동 구성을 설계하세요. 라이브러리 클래스의 존재, 속성 활성화, 사용자 시계로의 재정의, AutoConfiguration.imports 메타데이터, 순서 지정, ApplicationContextRunner의 분기를 표로 만들고, 기본값과 재정의가 동시에 생기지 않게 하세요.
해설 보기
요청 ID 생성기가 시계에 의존한다면, 그 관계를 빈 메서드의 파라미터로 드러냅니다. 구성의 순서 지정만으로 시계가 먼저 만들어질 것이라 기대하지 않습니다.
package board.autoconfig;
import java.time.Clock;
public final class RequestIdGenerator {
private final Clock clock;
public RequestIdGenerator(Clock clock) {
this.clock = clock;
}
public String next(long sequence) {
return clock.instant().toEpochMilli() + "-" + sequence;
}
}sequence 값을 여러 스레드가 함께 쓴다면 원자적 카운터나 영속적인 발급원을 쓰고, 만들어진 ID를 메트릭 태그로 넣지 않습니다.
2. 생성기가 만드는 문자열
Clock.fixed가 epoch 밀리초 1000을 돌려준다고 합니다. 같은 RequestIdGenerator에 next(7), next(7), next(8)을 차례로 호출하면 세 문자열은 무엇인가요? next가 스스로 sequence를 늘리거나 중복을 막나요?
해설 보기
세 문자열은 1000-7, 1000-7, 1000-8입니다. next는 호출자가 준 숫자를 문자열로 붙일 뿐, 숫자를 늘리거나 이미 발급한 값을 기억하지 않습니다.
Clock 빈을 사용자의 것으로 바꿀 수 있는지와, ID가 유일하다는 보장은 서로 다른 조건입니다. 여러 생성기나 여러 프로세스에서도 유일해야 한다면, 발급원과 노드 구분, 저장 정책까지 따로 설계해야 합니다.
3. 후보와 제외만 보는 모델
AutoConfigInputs의 candidates가 ClockConfig와 SinkConfig, exclusions가 SinkConfig, presentClasses가 비어 있습니다. eligible()의 결과는 무엇인가요? presentClasses에 필요한 클래스를 모두 넣으면 결과가 달라지나요?
해설 보기
결과는 ClockConfig 하나만 담은 집합입니다. SinkConfig는 제외 목록에 있어서 빠집니다.
presentClasses를 채워도 결과는 같습니다. eligible()은 이 집합을 전혀 읽지 않기 때문입니다. 실제 Boot에서는 클래스가 없으면 ClockConfig의 조건이 거짓이 될 수 있지만, 그 판단은 후보 선택 다음의 조건 평가 단계에서 일어나며 이 모델에는 들어 있지 않습니다.
다음 글 조건부 자동 구성에서는 클래스, 속성, 빈 조건을 직접 써 보며, 기본값이 양보하거나 꺼지는 규칙을 작은 컨텍스트로 확인합니다.