포스트

Spring Boot 자동 구성(Auto-configuration)은 어떻게 동작하나

Spring Boot 자동 구성(Auto-configuration)은 어떻게 동작하나

“아무것도 설정 안 했는데 왜 되지?”

순수 Spring으로 웹 애플리케이션을 만들던 시절엔 DispatcherServlet, DataSource, EntityManagerFactory, 메시지 컨버터까지 전부 직접 등록해야 했습니다. 그런데 Spring Boot는 spring-boot-starter-web 의존성 하나면 톰캣이 뜨고 JSON 직렬화가 되고 에러 페이지까지 나옵니다.

이걸 “마법”으로 남겨두면 두 가지 순간에 반드시 무너집니다. (1) 내가 등록한 Bean이 무시될 때, (2) 기대한 자동 구성이 안 걸릴 때. 이 글의 목표는 자동 구성을 “이름은 아는 블랙박스”에서 “로그를 읽고 직접 작성·디버깅할 수 있는 메커니즘”으로 바꾸는 것입니다. 그래서 개념 소개에서 멈추지 않고 어떤 클래스가, 어떤 순서로, 왜 그렇게 동작하는지까지 내려갑니다.

한눈에 보는 전체 파이프라인

수백 개의 자동 구성 후보가 로드 → 필터 → 평가를 거쳐 일부만 Bean으로 등록되고, 조건이 안 맞는 나머지는 도중에 탈락합니다. 글로 읽기 전에, 그 흐름을 움직임으로 먼저 느껴 보세요 — 초록은 끝까지 통과한 후보, 빨강은 ASM 필터에서 떨어지는 후보입니다.

imports 로드 수백 개 후보 ASM 필터 OnClassCondition @Conditional OnBean / OnProperty Bean 등록 컨텍스트 반영

조금 더 정적으로, 어떤 클래스가 각 단계에 관여하는지까지 보면 이렇습니다.

flowchart TD
    A["@SpringBootApplication"] --> B["@EnableAutoConfiguration"]
    B --> C["AutoConfigurationImportSelector<br/>(DeferredImportSelector)"]
    C --> D["ImportCandidates.load()<br/>...AutoConfiguration.imports 읽기"]
    D --> E["AutoConfigurationImportFilter<br/>(OnClassCondition ASM 필터)"]
    E --> F["@AutoConfigureBefore/After/Order<br/>정렬"]
    F --> G{"@Conditional 평가<br/>(OnBean/OnProperty…)"}
    G -->|충족| H[Bean 등록]
    G -->|불충족| I[건너뜀 + 리포트 기록]

핵심은 세 단계입니다. 후보 로드 → 빠른 필터링 → 조건 평가. 각 단계가 왜 분리되어 있는지가 이 글의 본론입니다.

1단계: 후보는 어디서 오나 — imports 파일과 DeferredImportSelector

@SpringBootApplication 안에는 @EnableAutoConfiguration이 들어 있고, 이건 @Import(AutoConfigurationImportSelector.class)를 품고 있습니다. 이 셀렉터가 후보 목록을 읽어옵니다.

목록의 위치는 버전에 따라 다릅니다.

버전위치비고
~2.7META-INF/spring.factories (EnableAutoConfiguration 키)deprecated
2.7~3.xMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports한 줄에 한 클래스
4.x위와 동일 + 모듈 분할거대 단일 spring-boot-autoconfigure가 기능별 jar로 쪼개짐

여기서 첫 번째 “왜”가 나옵니다. AutoConfigurationImportSelector는 평범한 ImportSelector가 아니라 DeferredImportSelector 입니다. 일반 @Configuration과 @Import가 전부 처리된 뒤에 마지막으로 실행된다는 뜻입니다.

이게 자동 구성의 가장 중요한 설계 결정입니다. 자동 구성이 항상 맨 나중에 돌기 때문에, 내가 직접 정의한 Bean은 이미 컨테이너에 등록된 상태입니다. 그래서 @ConditionalOnMissingBean이 “사용자가 안 만들었을 때만 기본값 제공”을 신뢰성 있게 보장할 수 있습니다. 이 순서가 없으면 자동 구성과 내 설정이 등록 순서에 따라 무작위로 충돌합니다.

소스 한 겹 더: selectImports의 실제 흐름

AutoConfigurationImportSelector가 후보를 만들어 내는 경로를 메서드 단위로 따라가면 이렇습니다.

1
2
3
4
5
6
7
8
selectImports()
 └─ getAutoConfigurationEntry()
     ├─ getCandidateConfigurations()     // imports 파일에서 전체 후보 로드
     ├─ removeDuplicates()
     ├─ getExclusions() → 제외 적용       // exclude / spring.autoconfigure.exclude
     ├─ getConfigurationClassFilter()
     │    .filter()                       // ★ AutoConfigurationImportFilter (ASM 1차 필터)
     └─ fireAutoConfigurationImportEvents()

즉 우리가 본 “필터 → 평가”는 추상적인 비유가 아니라 getAutoConfigurationEntry() 한 메서드 안의 실제 호출 순서입니다. 이 메서드에 브레이크포인트를 걸고 후보 리스트가 수백 개 → 수십 개로 줄어드는 걸 직접 보면, 그 다음부터는 자동 구성이 더 이상 마법으로 보이지 않습니다.

2단계: 클래스 로딩 없이 거르기 — ASM 기반 빠른 필터

후보가 수백 개입니다. 각 후보의 @ConditionalOnClass를 진짜로 평가하려면 그 클래스를 로딩해야 하고, 없는 클래스를 참조하면 ClassNotFoundException이 터집니다. 후보 전부를 로딩하면 시작 시간이 무너집니다.

Spring Boot는 이를 두 가지 트릭으로 해결합니다.

  1. AutoConfigurationImportFilter (OnClassCondition) 가 본격적인 조건 평가 이전에 후보를 1차로 거릅니다.
  2. 이 필터는 클래스를 JVM에 로딩하지 않고 ASM 바이트코드 리딩으로 애너테이션 메타데이터만 읽습니다. 그리고 컴파일 타임에 생성된 META-INF/spring-autoconfigure-metadata.properties 를 참고해, “이 자동 구성은 X 클래스가 있어야 함” 같은 조건을 클래스 로딩 없이 판단합니다.

결과적으로 클래스패스에 없는 자동 구성은 로딩되기도 전에 탈락합니다. “왜 이렇게까지?”의 답은 시작 성능입니다. 이 필터가 없으면 starter 수십 개를 쓰는 앱의 부팅이 눈에 띄게 느려집니다.

3단계: 본 조건 평가 — @Conditional 패밀리

필터를 통과한 후보만 실제 @Conditional 평가로 들어갑니다.

1
2
3
4
5
6
7
8
9
10
11
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean        // 내가 DataSource를 직접 등록 안 했다면
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

자주 쓰는 조건들:

애너테이션의미평가 주체
@ConditionalOnClass / OnMissingClass클래스패스 존재 여부OnClassCondition (ASM)
@ConditionalOnBean / OnMissingBean같은 타입 Bean 존재 여부OnBeanCondition
@ConditionalOnProperty설정값 일치OnPropertyCondition
@ConditionalOnWebApplication웹/서블릿/리액티브 타입OnWebApplicationCondition
@ConditionalOnMissingBean(위와 동일)OnBeanCondition

@AutoConfiguration(2.7+)은 단순한 마커가 아닙니다. 내부적으로 @Configuration(proxyBeanMethods = false) 를 의미하고(자동 구성 클래스끼리는 서로의 @Bean 메서드를 직접 호출하지 않으므로 CGLIB 프록시가 불필요 → 시작 비용 절약), @AutoConfigureBefore/After/Order 메타데이터를 붙일 수 있게 해줍니다.

가장 흔한 프로덕션 버그: @ConditionalOnBean 순서 함정

@ConditionalOnBean/@ConditionalOnMissingBean은 “평가 시점까지 등록된 Bean만” 봅니다. 사용자 설정 대비로는 안전(자동 구성이 마지막에 도니까)하지만, 자동 구성끼리는 위험합니다.

1
2
3
4
5
6
@AutoConfiguration
public class MyClientAutoConfiguration {
    @Bean
    @ConditionalOnBean(ObjectMapper.class)   // ObjectMapper가 "먼저" 등록돼야 함
    MyClient myClient(ObjectMapper mapper) { ... }
}

JacksonAutoConfiguration보다 이 클래스가 먼저 평가되면 ObjectMapper가 아직 없어서 myClient가 조용히 사라집니다. 로그에 예외도 없습니다. 순서를 명시해야 합니다.

1
2
@AutoConfiguration(after = JacksonAutoConfiguration.class)
public class MyClientAutoConfiguration { ... }

이게 자동 구성 디버깅의 절반입니다. “분명 starter 넣었는데 Bean이 없다”의 상당수가 이 순서 문제입니다.

무엇이 적용됐는지 — 디버깅 3종 세트

추측하지 말고 리포트를 보세요. 모두 같은 ConditionEvaluationReport를 다른 창구로 보여줍니다.

(1) --debug 플래그 — 부팅 로그에 평가 리포트 전체 출력.

1
java -jar app.jar --debug
1
2
3
4
5
6
7
8
9
10
11
Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required class 'javax.sql.DataSource' (OnClassCondition)

Negative matches:
-----------------
   RabbitAutoConfiguration:
      Did not match:
      - @ConditionalOnClass did not find required class
        'com.rabbitmq.client.Channel' (OnClassCondition)

Did not match의 이유가 그대로 적혀 있습니다. “내가 기대한 자동 구성이 왜 안 걸렸지?”의 정답이 여기 있습니다.

(2) Actuator 엔드포인트 — 런타임에 HTTP로 같은 리포트 조회.

1
curl localhost:8080/actuator/conditions | jq

(3) 프로그래밍 방식 테스트 — ApplicationContextRunner로 자동 구성을 단위 테스트.

1
2
3
4
5
6
7
@Test
void DataSource가_없으면_자동구성이_빠진다() {
    new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
        .withClassLoader(new FilteredClassLoader(DataSource.class))   // 클래스패스에서 가림
        .run(ctx -> assertThat(ctx).doesNotHaveBean(DataSource.class));
}

내 라이브러리의 자동 구성을 만든다면, 이 러너가 “특정 조건에서 Bean이 뜬다/안 뜬다”를 검증하는 표준 도구입니다.

자동 구성 끄기 — 두 가지 방법, 다른 동작

1
2
// 1) 코드로 제외 — 클래스 참조라 컴파일 타임 안전
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
1
2
3
4
5
# 2) 설정으로 제외 — 클래스패스에 없는 자동 구성도 문자열로 끌 수 있음
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

exclude 속성은 클래스패스에 실제로 존재하는 자동 구성에만 쓸 수 있어, 오타 시 부팅이 실패합니다(안전장치). 의존성으로만 들어와 직접 import 못 하는 경우엔 spring.autoconfigure.exclude 문자열을 씁니다.

내 라이브러리에 자동 구성 넣기 (스타터 만들기 미리보기)

1
2
3
4
5
6
7
8
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyServiceAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    MyService myService(MyProperties props) { return new MyService(props); }
}

그리고 등록 파일 한 줄:

1
2
3
# src/main/resources/META-INF/spring/
#   org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyServiceAutoConfiguration

⚠️ 자동 구성 클래스는 절대 @ComponentScan 대상 패키지에 두지 마세요. 컴포넌트 스캔에 잡히면 조건 평가를 거치지 않고 무조건 등록되어, “선택적 기본값”이라는 설계 의도가 깨집니다.

면접/리뷰 단골 질문

  • Q. @ConditionalOnMissingBean이 항상 내 Bean을 우선하는 이유는? → 자동 구성이 DeferredImportSelector라 사용자 설정 이후에 평가되기 때문. 순서가 보장의 근거다.
  • Q. 클래스패스에 없는 클래스를 참조하는 자동 구성이 왜 ClassNotFoundException을 안 내나? → OnClassCondition이 ASM으로 메타데이터만 읽어 클래스 로딩 전에 거른다.
  • Q. starter를 넣었는데 Bean이 안 뜬다, 첫 디버깅은? → --debug 또는 /actuator/conditions로 negative match 사유 확인. 십중팔구 @ConditionalOnClass 누락 또는 자동 구성 간 순서(@AutoConfigureAfter) 문제.

정리

  • 자동 구성 = 후보 로드(imports) → ASM 빠른 필터 → @Conditional 평가의 3단 파이프라인.
  • AutoConfigurationImportSelector가 DeferredImportSelector 라 항상 마지막에 돈다 → @ConditionalOnMissingBean의 “내 Bean 우선” 보장의 근거.
  • OnClassCondition은 ASM + 컴파일타임 메타데이터로 클래스 로딩 없이 걸러 시작 성능을 지킨다.
  • 자동 구성 끼리의 @ConditionalOnBean은 순서에 취약 → @AutoConfiguration(after = ...)로 명시.
  • 추측 금지: --debug / /actuator/conditions / ApplicationContextRunner 로 항상 근거를 보고 판단하자.

관련 글: 이 메커니즘의 출발점인 @SpringBootApplication은 별도 글에서, 조건부 등록의 토대인 IoC/DI와 Bean 생명주기는 이 글에서 다룹니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.