1. 개요

앞 글에서는 @Import에 TomcatWebServerConfig와 DispatcherServletConfig를 직접 적어 인프라 빈을 분리했다. 구성 클래스가 몇 개뿐이라면 충분하지만, 라이브러리가 제공하는 기능이 많아지면 애노테이션 코드를 고칠 때마다 목록을 수정해야 한다.

스프링 부트의 자동 구성은 구성 클래스 목록을 코드 밖의 .imports 파일에 두고, 실행 시 이를 읽어 등록한다. 이 글에서는 ImportSelector, DeferredImportSelector, ImportCandidates를 이용해 이 흐름을 작은 예제로 따라가 본다.


2. ImportSelector로 가져올 구성을 선택하기

@Import는 구성 클래스를 직접 받을 수도 있고, ImportSelector 구현체를 받을 수도 있다. 선택자는 selectImports()에서 가져올 구성 클래스의 완전한 이름을 반환한다.

public class MyAutoConfigImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        return new String[] {
                "toby.config.autoconfig.TomcatWebServerConfig",
                "toby.config.autoconfig.DispatcherServletConfig"
        };
    }
}

DeferredImportSelector는 ImportSelector의 한 종류다. 일반 구성 클래스 처리가 끝난 뒤에 가져올 구성을 선택하므로, 자동 구성처럼 여러 설정의 처리 순서를 다룰 때 사용한다. 이 단계의 반환값은 단순한 문자열 배열이지만, 스프링은 각 이름에 해당하는 구성 클래스를 찾아 빈 정의로 처리한다.

선택자를 @Import하는 애노테이션은 다음과 같다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyAutoConfigImportSelector.class)
public @interface EnableMyAutoConfiguration {
}

이제 @EnableMyAutoConfiguration을 사용하면 선택자가 돌려준 설정 클래스가 포함된다. 하지만 클래스 이름이 선택자 소스 코드에 남아 있으므로, 아직은 목록을 분리한 것이 아니다.


3. .imports 파일로 목록 옮기기

Spring Boot의 ImportCandidates.load()는 클래스패스의 META-INF/spring/<완전한-애노테이션-이름>.imports 파일에서 후보 클래스 이름을 읽는다. 따라서 학습용 자동 구성 애노테이션이 toby.config.MyAutoConfiguration이라면 파일 경로와 내용은 아래와 같다.

# src/main/resources/META-INF/spring/toby.config.MyAutoConfiguration.imports
toby.config.autoconfig.TomcatWebServerConfig
toby.config.autoconfig.DispatcherServletConfig

선택자는 이 파일을 읽어 String[]으로 바꾸기만 하면 된다. 생성자로 ClassLoader를 받으면 현재 실행 환경의 클래스패스에서 파일을 찾을 수 있다.

@RequiredArgsConstructor
public class MyAutoConfigImportSelector implements DeferredImportSelector {

    private final ClassLoader classLoader;

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        return ImportCandidates.load(MyAutoConfiguration.class, classLoader)
                .getCandidates()
                .toArray(String[]::new);
    }
}

파일의 기준이 되는 클래스와 파일 이름은 반드시 같아야 한다. 즉 ImportCandidates.load(MyAutoConfiguration.class, classLoader)를 호출했다면 toby.config.MyAutoConfiguration.imports를 찾아야 한다. 이 둘이 다르면 파일을 찾지 못해 자동 구성 후보가 비어 있게 된다.

위치 역할
@EnableMyAutoConfiguration 선택자를 @Import한다
MyAutoConfigImportSelector .imports의 클래스 이름을 읽어 반환한다
META-INF/spring/*.imports 자동 구성 후보 목록을 보관한다
TomcatWebServerConfig 등 실제 빈을 선언하는 구성 클래스다

4. 자동 구성 클래스라는 의도를 표시하기

자동 구성으로 발견되는 설정과 애플리케이션이 직접 작성한 설정을 구분하기 위해, @Configuration을 감싼 애노테이션을 만든다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration(proxyBeanMethods = false)
public @interface MyAutoConfiguration {
}

이 애노테이션을 인프라 구성 클래스에 붙인다.

@MyAutoConfiguration
public class DispatcherServletConfig {

    @Bean
    DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
}

TomcatWebServerConfig에도 같은 애노테이션을 붙이면, 파일에 나열된 두 클래스가 자동 구성으로 읽혀 웹 서버와 MVC 인프라 빈을 등록한다.

지금까지 만든 애노테이션, 선택자, 구성 정보 파일의 관계를 그림으로 정리하면 다음과 같다. 그림의 MyAutoConfiguration.imports는 실제 클래스패스 경로인 META-INF/spring/toby.config.MyAutoConfiguration.imports를 줄여 표시한 것이다.

학습용 자동 구성 애노테이션과 ImportSelector, imports 파일, 구성 클래스의 연결


5. 자동 구성 흐름 따라가기

이제 시작 애노테이션부터 실제 빈 등록까지의 흐름을 한 번에 볼 수 있다.

@MySpringBootApplication
  → @EnableMyAutoConfiguration
    → @Import(MyAutoConfigImportSelector.class)
      → META-INF/spring/toby.config.MyAutoConfiguration.imports 읽기
        → TomcatWebServerConfig, DispatcherServletConfig 선택
          → @Bean 메서드의 결과를 스프링 빈으로 등록

이 구조에서 애플리케이션은 어떤 인프라 구성이 필요한지 직접 나열하지 않는다. 라이브러리 또는 모듈이 .imports 파일로 후보를 제공하고, 선택자가 이를 읽어 컨테이너에 전달한다. 실제 Spring Boot는 여기에 조건부 자동 구성과 제외 설정 등을 더하지만, 구성 후보를 발견해 등록하는 중심 흐름은 같다.


6. 정리

ImportSelector는 @Import 대상을 코드로 결정하게 하고, DeferredImportSelector는 그 결정을 일반 구성 처리 뒤로 미룬다. 이 덕분에 자동 구성은 시작 애노테이션에 모든 구성 클래스를 나열하지 않아도 된다.

.imports 파일은 자동 구성 후보의 목록을 코드와 분리한다. 목록을 읽는 기준 클래스, 파일 경로, 파일 안의 클래스 이름이 정확히 연결되어야 한다.


참고 자료