스프링 부트(핵심 원리와 활용)

Ch04. 자동 구성(Auto Configuration) - 자동 구성 이해

webmaster 2023. 3. 10. 02:15
728x90

스프링 부트의 동작

스프링 부트는 다음 경로에 있는 파일을 읽어서 스프링 부트 자동 구성으로 사용한다.

resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

스프링 부트가 제공하는 spring-boot-autoconfigure 라이브러리의 다음 파일을 확인해 보면 스프링 부트 자동 구성을 확인할 수 있다.

  • spring-boot-autoconfigure - org.springframework.boot.autoconfigure.AutoConfiguration.imports
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
...

스프링 부트 자동 구성 동작 원리

  • @SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class)

SpringBootAppliction

@SpringBootApplication
public class AutoConfigApplication {

    public static void main(String[] args) {
        SpringApplication.run(AutoConfigApplication.class, args);
    }

}
  • run() 에 보면 AutoConfigApplication.class 를 넘겨주는데, 이 클래스를 설정 정보로 사용한다는 뜻이다.
  • AutoConfigApplication 에는 @SpringBootApplication 애노테이션이 있는데, 여기에 중요한 설정 정보들이 들어있다

@SpringBootApplication

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
  @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
 @AliasFor(annotation = EnableAutoConfiguration.class)
 Class<?>[] exclude() default {};

 @AliasFor(annotation = EnableAutoConfiguration.class)
 String[] excludeName() default {};

 @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
 String[] scanBasePackages() default {};
 
 @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
 Class<?>[] scanBasePackageClasses() default {};

 @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
 Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

 @AliasFor(annotation = Configuration.class)
 boolean proxyBeanMethods() default true;

}
  • 여기서 우리가 주목할 애노테이션은 @EnableAutoConfiguration 이다.
    • 이름 그대로 자동 구성을 활성화 하는 기능을 제공한다.

@EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

 String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

 Class<?>[] exclude() default {};

 String[] excludeName() default {};

}

AutoConfigurationImportSelector

  • @Import 는 주로 스프링 설정 정보( @Configuration )를 포함할 때 사용한다.
  • 그런데 AutoConfigurationImportSelector 를 열어보면 @Configuration 이 아니다.
    • 그냥 구현코드가 적혀있다.
    • ImportSelector를 자세히 알면 해당 기능을 알 수 있다.

ImportSelector

@Import 에 설정 정보를 추가하는 방법은 2가지가 있다.

  • 정적인 방법: @Import (클래스) 이것은 정적이다. 코드에 대상이 딱 박혀 있다. 설정으로 사용할 대상을 동적으로 변경할 수 없다.
  • 동적인 방법: @Import ( ImportSelector ) 코드로 프로그래밍해서 설정으로 사용할 대상을 동적으로 선택할 수 있다

정적인 방법

@Configuration
@Import({AConfig.class, BConfig.class})
public class AppConfig {...}
  • 그런데 예제처럼 AConfig , BConfig 가 코드에 딱 정해진 것이 아니라, 특정 조건에 따라서 설정 정보를 선택해야 하는 경우에는 어떻게 해야할까?

동적인 방법

ImportSelector

public interface ImportSelector {

 String[] selectImports(AnnotationMetadata importingClassMetadata);

 @Nullable
 default Predicate<String> getExclusionFilter() {
  return null;
 }

}
  • 스프링은 설정 정보 대상을 동적으로 선택할 수 있는 ImportSelector 인터페이스를 제공한다.

ImportSelector 예제

HelloBean

public class HelloBean {

}
  • 빈 등록 대상이다

HelloConfig

@Configuration
public class HelloConfig {

  @Bean
  public HelloBean helloBean(){
    return new HelloBean();
  }
}
  • 설정 정보이다. HelloBean 을 스프링 빈으로 등록한다.

HelloImportSelector

public class HelloImportSelector implements ImportSelector {

  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    //해당 부분은 동적으로 변경할 수 있다 -> 코드이기 때문
    return new String[]{"hello.selector.HelloConfig"};
  }
}
  • 설정 정보를 동적으로 선택할 수 있게 해주는 ImportSelector 인터페이스를 구현했다.
  • 여기서는 단순히 hello.selector.HelloConfig 설정 정보를 반환한다.
  • 이렇게 반환된 설정 정보는 선택되어서 사용된다.
  • 여기에 설정 정보로 사용할 클래스를 동적으로 프로그래밍 하면 된다

ImportSelectorTest

public class ImportSelectorTest {

  @Test
  public void staticConfig() {
    AnnotationConfigApplicationContext appContext =
        new AnnotationConfigApplicationContext(StaticConfig.class);
    HelloBean bean = appContext.getBean(HelloBean.class);
    assertThat(bean).isNotNull();
  }

  @Test
  public void selectorConfig() {
    AnnotationConfigApplicationContext appContext =
        new AnnotationConfigApplicationContext(SelectorConfig.class);
    HelloBean bean = appContext.getBean(HelloBean.class);
    assertThat(bean).isNotNull();

  }

  @Configuration
  @Import(HelloConfig.class)
  public static class StaticConfig {

  }

  @Configuration
  @Import(HelloImportSelector.class)
  public static class SelectorConfig {

  }
}
  • staticConfig()
    • 스프링 컨테이너를 만들고, StaticConfig.class 를 초기 설정 정보로 사용했다.
    • 그 결과 HelloBean 이 스프링 컨테이너에 잘 등록된 것을 확인할 수 있다.
  • selectorConfig()
    • selectorConfig() 는 SelectorConfig 를 초기 설정 정보로 사용한다.
    • SelectorConfig 는 @Import(HelloImportSelector.class) 에서 ImportSelector 의 구현체인 HelloImportSelector 를 사용했다.
    • 스프링은 HelloImportSelector 를 실행하고, "hello.selector.HelloConfig" 라는 문자를 반환 받는다.
    • 스프링은 이 문자에 맞는 대상을 설정 정보로 사용한다. 따라서 hello.selector.HelloConfig 이 설정 정보로 사용된다.
    • 그 결과 HelloBean 이 스프링 컨테이너에 잘 등록된 것을 확인할 수 있다.

@EnableAutoConfiguration 동작 방식

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

 String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

 Class<?>[] exclude() default {};

 String[] excludeName() default {};

}
  • AutoConfigurationImportSelector 는 ImportSelector 의 구현체이다. 따라서 설정 정보를 동적으로 선택할 수 있다.
  • 실제로 이 코드는 모든 라이브러리에 있는 다음 경로의 파일을 확인한다.
    • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

AutoConfigurationImportSelector

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
  ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
 
 //...
 
 @Override
 public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
   return NO_IMPORTS;
  }
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
 }
 
 //...
 
 protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
 }
 //...
 
 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
            .getCandidates();
    Assert.notEmpty(configurations,
            "No auto configuration classes found in "
                    + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                    + "are using a custom packaging, make sure that file is correct.");
    return configurations;
 }
 //...
}
ImportCadidates.load()
  • DeferredImportSelector 때문에 selectImports가 바로 실행되지는 않지만, 중요하지는 않다
  • getAutoConfigurationEntry 메서드를 살펴보면 getCandidateConfigurations 메서드를 통해 configuration 정보를 읽어온다.
  • getCandidateConfigurations를 살펴보면 ImportCadidates.load()를 통해 설정 정보를 읽어 온다.
  • ImportCadidates.load()를 샆펴보면, LOCATION 정보를 통해 위치를 읽어 오는 부분이 있는데 해당 부분이 아래로 치환된다
    • org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • 결론적으로 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 하위의 문자열 정보를 설정 정보로 읽어 들여온다.

스프링 부트 자동 구성이 동작하는 방식은 다음 순서로 확인할 수 있다.

@SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class)
  • resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 열어서 설정 정보 선택
  • 해당 파일의 설정 정보가 스프링 컨테이너에 등록되고 사용
 
728x90