스프링 부트 - 자동 구성 관련 요약

     

     

    스프링 부트의 AutoConfiguration

    • 스프링 부트는 사용하는 라이브러리 중 필요한 빈을 자동으로 스프링 컨테이너에 등록해주는 AutoConfiguration 기능윽 제공함. 
    • 스프링 부트는 spring-boot-autoconfigure 라이브러리를 이용해 AutoConfiguration으로 등록이 필요한 스프링 빈 정보들을 모아둔다.

     


    @AutoConfiguration

    // AutoConfigurationImportSelector.class
    
    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;
    • 스프링 프레임워크는 시작할 때, AutoConfigurationImportSelector를 이용해서 @AutoConfiguration이 붙은 모든 클래스를 설정 정보로 등록한다. 
    • 만약 AutoConfiguration으로 등록하고 싶다면, 아래 코드처럼 설정 클래스에 @AutoConfiguration을 선언해야한다. 
    // JDBC 등록해주는 거.
    package org.springframework.boot.autoconfigure.jdbc;
    
    @AutoConfiguration(after = DataSourceAutoConfiguration.class)
    @ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
    @ConditionalOnSingleCandidate(DataSource.class)
    @EnableConfigurationProperties(JdbcProperties.class)
    @Import({ DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class,
    		NamedParameterJdbcTemplateConfiguration.class })
    public class JdbcTemplateAutoConfiguration {
    
    }
    • @AutoConfiguration 어노테이션 안에는 @Configuration 어노테이션이 있음. 즉, 이 어노테이션이 붙은 클래스를 자바 설정 파일로 사용한다는 의미다. 
    • @AutoConfiguration에 특정 옵션을 줘서 등록해야 할 AutoConfiguration의 선후 관계를 조절할 수 있다. 
      • after()
      • afterName()
      • before()
      • beforeName()

    @Conditional 관련 내용

    • 특정 조건일 때만 스프링 빈을 등록되도록 하려면 @ConditionalOnXxx 어노테이션을 이용하면 된다.
    package org.springframework.context.annotation; 
    
    @FunctionalInterface
    public interface Condition {
       boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    }
    • @Conditional 기능을 사용하려면 먼저 Condition 인터페이스를 구현해야 함. 
    • Condition 인터페이스는 matches() 메서드를 제공함. 
      • true 반환 → 조건 만족으로 판단.
      • false 반환 → 조건 만족하지 않음으로 판단. 
    @Configuration
    @Conditional(MemoryCondition.class)
    public class MemoryConfig {
    ...
    }

    위 설정 클래스는 다음과 같이 동작한다.

    • MemoryCondition.class는 Condition 인터페이스의 구현체이며, matches() 메서드를 실행함. 
    • matches() 메서드의 결과가 true를 반환하면, 설정정보로 등록해서 사용함. 

    다양한 @Conditional 기능

    @Configuration
    // @Conditional(MemoryCondition.class)
    @ConditionalOnProperty(name = "memory", havingValue = "on") // 위와 같은 기능임. 구현하지 않아도 됨.
    public class MemoryConfig {
        ...
    }
    • @Conditional 어노테이션을 이용하기 위해 Condition 인터페이스를 구현해서 사용해야했다. 스프링은 이미 다양한 Condition 인터페이스 구현체를 제공하고, 어노테이션 기반으로 이용할 수 있다. 
    • @ConditionalOnProperty(...)는 @Conditional(MemoryCondition.class) 대신에 사용할 수 있음. 
    • 다양한 Conditional 기능은 다음과 같다.
      • @ConditionalOnClass, @ConditionalOnMissingClass : 클래스가 있는 경우 동작. 
      • @ConditionalOnBean, @ConditionalOnMissingBean : 빈이 등록되어 있는 경우 동작.
      • @ConditionalOnProperty : 환경정보가 있는 경우 동작.
      • @ConditionalOnResource : 특정 리소스가 있는 경우 동작.
      • @ConditionalOnWebApplication, @ConditionalOnNotWebApplication : 웹 어플리케이션인 경우 동작
      • @ConditionalOnExpression : SpEL 표현식 만족하는 경우 동작함.

     


    스프링의 @Import

    스프링의 @Import 어노테이션에는 크게 두 가지가 올 수 있음. 

    • @Configuration 
    • ImportSelector 인터페이스 

    @Configuration은 해당 클래스를 스프링에서 자바 설정 파일로 사용한다는 것을 의미한다. 또한 ImportSelector 역시 넣을 수 있는데, ImportSelector를 넣으면 약간은 다른 방식으로 사용할 자바 설정 파일을 제공해준다. 

    // 정적 Import → 설정 정보 그대로 사용.
    @Configuration
    @Import( {AConfig.class, BConfig.class} )
    public class AppConfig {...}
    
    
    // 동적 Import → ImportSelector 사용.
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {}

    위처럼 @Import 어노테이션을 이용해서 직접 설정 파일을 넣는 정적 Import, ImportSelector를 이용해 동적으로 읽어올 수 있는 동적 Import 방식을 구현할 수 있다. 그렇다면 ImportSelector는 어떻게 동작하는 녀석일까? 

     


    ImportSelector

    package org.springframework.context.annotation; 
    
    public interface ImportSelector {
    	String[] selectImports(AnnotationMetadata importingClassMetadata);
    	//..
    }
    • ImportSelector 인터페이스는 selectImports()를 가지고 있다.
    • 설정 정보로 사용하고 싶은 클래스를 selectImports()의 결과로 반환하면, 스프링은 해당 클래스를 스프링 설정 정보로 이용한다. 즉, 동적으로 @Configuration으로 사용되는 자바 설정 파일을 여러개 받을 수 있다. 
    public class HelloImportSelector implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            return new String[]{"hello.selector.HelloConfig"};
        }
    }

    ImportSelector의 구현체 HelloImportSelector를 생성한다. 이 구현체는 항상 hello.selector.HelloConfig를 반환한다. 

    @Test
    void selectorConfig() {
        // 이렇게 하면 스프링 컨테이너를 만들 때, HelloConfig.class를 참고함.
        AnnotationConfigApplicationContext appContext =
                new AnnotationConfigApplicationContext(SelectorConfig.class);
        HelloBean bean = appContext.getBean(HelloBean.class);
        Assertions.assertThat(bean).isNotNull();
    }
    
    @Configuration
    @Import(HelloImportSelector.class)
    public static class SelectorConfig{}
    • 위에서 스프링 컨테이너를 생성할 때 SelectorConfig.class를 설정 정보로 사용하도록 했다.
    • SelectorConfig는 HelloImportSelector를 @Import했다. 따라서 HelloImportSelector의 selectImports() 메서드를 호출해서 설정 정보로 사용할 녀석들을 반환받는다. 이 때 hello.selector.HellConfig 파일을 반환받고 이것을 설정 정보로 사용한다. 
    @Configuration
    public class HelloConfig {
        @Bean public HelloBean helloBean() { return new HelloBean(); }
    }
    • HellConfig는 @Configuration에서 볼 수 있듯이 자바 설정 파일이다. 이 설정 파일은 HelloBean 클래스를 스프링 빈으로 등록시켜준다. 

     


    스프링 부트는 어떻게 AutoConfiguration을 해주는가? 

    스프링부트는 ImportSelector를 이용해 @AutoConfiguration이 있는 라이브러리들의 설정 정보를 읽어서 스프링빈을 등록해준다. 큰 흐름은 아래와 같다. 

    • @SpringBootApplication → @EnableAutoConfiguration → AutoConfigurationImportSelector.class  
    // AutoConfigurationImportSelector.class
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
       List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
             .getCandidates();
       ...
    }

    AutoConfigurationImportSelector 클래스는 위 메서드를 호출한다. 이 때 AutoConfiguration.class 정보를 넘겨주면서 load() 메서드를 호출한다. 

    // ImportCandidates.java
    public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
       Assert.notNull(annotation, "'annotation' must not be null");
       ClassLoader classLoaderToUse = decideClassloader(classLoader);
       
       // LOCATION = "META-INF/spring/%s.imports"
       // %s = Annotation의 이름. 
       String location = String.format(LOCATION, annotation.getName());
       ...
    }

    load() 메서드는 LOCATION 정보를 이용해서 설정 파일의 이름을 가져온다. 이 때 @AutoConfiguration 어노테이션의 풀네임과 LOCATION 상수를 조합해서 읽어와야 할 정보를 다음과 같이 만들어 낸다. 

    • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    // 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
    ...
    • 해당 위치의 파일을 살펴보면, 자동으로 등록해야 할 모든 자동설정 정보가 포함되어있다. 
    • AutoConfigurationImportSelector의 selectImports()의 결과로 위의 자동설정 정보를 반환해준다.
      • 스프링 프레임워크는 자동설정 클래스를 읽어서, 해당 클래스 내의 설정에 따라 스프링 빈을 하나씩 등록해준다.
    // AutoConfigurationExcludeFilter.java
    
    private boolean isAutoConfiguration(MetadataReader metadataReader) {
       boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata()
             .isAnnotated(AutoConfiguration.class.getName());
       return annotatedWithAutoConfiguration
             || getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
    }
    • AutoConfiguration을 등록하기 전에 isAutoConfiguration() 메서드를 이용해 자동 설정 정보인지 판단되는 과정이 있다.
    • 여기서 특정 클래스에 @AutoConfiguration 어노테이션이 있는 경우에만 자동설정 케이스로 판단하고 넣어준다. 

     


    자동 설정을 하기 위해서는? 

    정리해보면 AutoConfiguration은 스프링 프레임워크의 @Impor, ImportSelector, @AutoConfiguration을 이용해 진행된다. 그렇다면 우리가 실제로 자동구성으로 등록하기 위해서는 어떻게 작성해야할까? 

    • 자동설정을 정보를 제공할 설정 클래스에 @AutoConfiguration 어노테이션을 붙임 → isAutoConfiguration() Validate 관련
    • 해당 라이브러리의 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 생성하고, 자동 설정에 이용할 설정 파일을 명시해줘야 함. 

    위와 같이 작성해주면 되고, 파일을 이곳을 참고하면 된다.  (https://github.com/chickenchickenlove/spring-auto-configuration-1/tree/master/memory-v2)

     


    @AutoConfiguration이 있는 녀석은 컴포넌트 스캔 대상이 되면 안됨. 

    • @AutoConfiguration 자동구성 설정 파일은 일반 스프링 설정과 라이프 사이클이 다르기 때문에 컴포넌트 스캔 대상이 되면 안됨. 
    • 따라서 @SpringBootApplication 어노테이션에서는 컴포넌트 스캔 대상에서 제외하기 위해 @Filter(AutoConfigurationExcludeFilter.class)를 이용해서 제거해준다. 

     


    자동 구성을 언제 사용해야할까? 

    • AutoConfiguration은 라이브러리를 만들어서 제공할 때만 사용해야 함. 
    • 일반적인 개발을 할 때, 필요한 빈들은 Component Scan 혹은 직접 등록해서 사용하기 때문이다. 

     

     

     

     

     

     

     

    연관 글

    'Spring > Spring' 카테고리의 다른 글

    Spring : 외부 설정이란?  (0) 2023.10.21
    스프링 부트 - 자동구성 4  (0) 2023.09.17
    스프링 부트 - 자동구성 2  (0) 2023.09.17
    스프링 부트 - 자동구성 3  (2) 2023.09.17
    스프링 부트 - 자동구성 1  (0) 2023.09.14

    댓글

    Designed by JB FACTORY