본문 바로가기
Java/Spring

Auto Configuration

by oneny 2023. 10. 9.

Auto Configuration

Spring Boot의 Auto Configuration은 Spring Boot 애플리케이션을 개발할 때 기본적인 설정을 자동으로 제공하는 기능이다. 덕분에 개발자가 별도의 구성 파일을 작성하거나 빈을 설정하는 번거로움을 줄여준다. 스프링 부트는 수 많은 자동 구성을 제공하고 spring-boot-autoconfigure에 자동 구성을 모아두는데 아래 사이트를 이동하면 확인할 수 있다.

 

자동 구성 확인

@Slf4j
@SpringBootTest
class DbConfigTest {

    @Autowired
    DataSource dataSource;

    @Autowired
    TransactionManager transactionManager;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    void checkBean() {
       log.info("dataSource = {}", dataSource);
       log.info("transactionManager = {}", transactionManager);
       log.info("jdbcTemplate = {}", jdbcTemplate);

       assertAll(
          () -> assertThat(dataSource).isNotNull(),
          () -> assertThat(transactionManager).isNotNull(),
          () -> assertThat(jdbcTemplate).isNotNull()
       );
    }
}

해당 빈들은 개발자가 직접 설정을 통해 스프링 컨테이너에 등록한 적이 없다. 하지만 테스트는 정상적으로 통과하고 출력결과에 DataSource, TransactionManager, TransactionManager 빈들이 존재하는 것을 확인할 수 있다. 이 빈들은 스프링 부트가 자동 구성(Auto Configuration) 기능을 통해 자동으로 등록해주는 것이다. 이러한 자동 구성 덕분에 개발자는 반복적이고 복잡한 빈 등록과 설정을 최소화하고 애플리케이션 개발을 빠르게 시작할 수 있다.

 

자동 구성 알아보기

프로젝트 의존관계를 살펴보면 spring-boot-starter-jdbc나 spring-boot-starter-web 프로젝트가 spring-boot-autoconfigure을 사용하는 것을 확인할 수 있고, 스프링 부트는 spring-boot-autoconfigure라는 프로젝트 안에서 수 많은 자동 구성을 제공한다. 그 중 JdbcTemplate을 설정하고 빈으로 등록해주는 자동 구성을 확인해보자.

 

JdbcTemplateAutoConfiguration

JdbcTemplateAutoConfiguration은 Spring Boot에서 JdbcTemplate과 관련된 자동 구성 클래스 중 하나이다. 해당 자동 구성 클래스는 클래스 경로에 필요한 클래스가 존재하고 데이터 소스가 단일 빈으로 등록되어 있을 때 활성화되며, 필요한 JDBC 설정은 JdbcProperties를 통해 구성된다.

  • @AutoConfiguration(after = DataSourceAutoConfiguration.class): 해당 어노테이션은 해당 자동 구성 클래스가 DataSourceAutoConfiguration 이후에 실행되도록 지정한다. 자동 구성을 사용하려면 이 어노테이션을 등록해야 한다.
    • 해당 어노테이션 내부에 @Configuration 어노테이션이 있어서 빈을 등록하는 자바 설정 파일로 사용할 수 있다.
  • @ConditionalOnClass({DataSource.class, JdbcTemplate.class}): 해당 어노테이션은 클래스 경로에 DataSource 및 JdbcTemplate 클래스가 존재할 때 자동 구성을 활성화하낟. 즉, 이 클래스들이 존재하지 않는 환경에서는 해당 자동 구성이 비활성화된다.
  • @ConditionalOnSingleCandidate(DataSource.class): 해당 어노테이션은 DataSource 빈이 단일 빈으로 등록되어 있을 대만 자동 구성을 활성화한다. 만약 여러 개의 DataSource 빈이 있는 경우에 자동 구성을 방지할 수 있다.
  • @EnableConfigurationProperties(JdbcProperties.class): 해당 어노테이션은 JdbcProperties 클래스를 구성 프로퍼티로 사용하도록 활성화한다. 이를 통해 application.properties 또는 application.yml 파일에 정의된 JDBC 관련 설정을 읽고 JdbcTemplate에 제공할 수 있다.
  • @Import({...}): 스프링에서 자바 설정을 추가할 때 사용한다.

 

JdbcTemplateConfiguration

@Import의 대상이 되는 JdbcTemplateConfiguration을 확인해보자. 가장 주목되는 점은 JdbcTemplate이 빈으로 등록되는 것을 확인할 수 있다.

  • Configuration(proxyBeanMethods = false): 해당 어노테이션을 해당 클래스가 Spring의 구성 클래스임을 나타낸다.
  • @ConditionalOnMissingBean(JdbcOperaions.class): 해당 어노테이션은 조건부 자동 구성을 활성화한다. JdbcOperaions 빈이 컨텍스트에 등록되어 있지 않을 때만 이 자동 구성이 활성화되고, 이미 JdbcOperations 빈이 정의되어 있는 경우에는 클래스가 동작하지 않는다. 쉽게 말해, 개발자가 직접 작성한 JdbcTemplate이 빈으로 등록되어 있을 않을 때만 설정을 실행한다는 것이다.
  • @Primary: 해당 어노테이션은 동일한 타입의 다른 빈보다 우선적으로 선택되도록 지정한다.
  • jdbcTemplate(): 해당 메서드는 DataSouce와 JdbcProperties를 인자로 받고, JdbcTemplate 객체를 생성할 때 생성자로 dataSource를 주입하고, JdbcProperties에서 JDBC 설정 프로퍼티를 가져와 JdbcTemplate의 fetchSize, maxRows, queryTimeout 등의 설정을 적용한다.

 

@Conditional

위 설정에서 @ConditionalXXX 어노테이션이 자주 등장한 것을 확인할 수 있다. @Conditional 어노테이션은 같은 소스 코드인데 특정 상황일 때만 특정 빈들을 등록해서 사용하도록 도와주는 기능으로 스프링 부트 자동 구성에서 자주 사용되는 어노테이션이다.

 

@Conditional을 사용하려면 Condition 인터페이스를 구현해야 한다.

  • matches(): true를 반환하면 조건에 만족해서 동작하고, false를 반환하면 동작하지 않는다.
  • ConditionContext: 스프링 컨테이너, 환경 정보 등을 담고 있다.
  • AnnotatedTypeMetadata: 애노테이션 메타 정보를 담고 있다.

 

Memory

public class Memory {

    private long used; // 사용 중인 메모리
    private long max; // 최대 메모리

    public Memory(long used, long max) {
       this.used = used;
       this.max = max;
    }

    public long getUsed() {
       return used;
    }

    public long getMax() {
       return max;
    }

    @Override
    public String toString() {
       return "Memory{" +
          "used=" + used +
          ", max=" + max +
          '}';
    }
}

메모리 사용량과 최대사용량이 필드인 Memory를 생성했다.

 

MemoryFinder

@Slf4j
public class MemoryFinder {

     public Memory get() {
        long max = Runtime.getRuntime().maxMemory();
        long total = Runtime.getRuntime().totalMemory();
        long free = Runtime.getRuntime().freeMemory();
        long used = total - free;
        return new Memory(used, max);
     }

     @PostConstruct
    public void init() {
        log.info("init memoryFinder");
     }
}

위에서 만든 Memory 클래스를 통해서 실행중인 애플리케이션의 최대 메모리 사용량과 사용중인 메모리 사용량을 인자로 넘겨 Memory 객체를 생성하는 클래스를 만들었다.

 

 MemoryCondition

@Slf4j
public class MemoryCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

       String memory = context.getEnvironment().getProperty("memory");
       log.info("memory={}", memory);
       return "on".equals(memory);
    }
}

스프링은 외부 설정을 추상화해서 Environment로 통합했다. 따라서 VM Options나 application.properties 등 다양한 외부 환경 설정을 Environment 하나로 읽어들일 수 있다. Environment를 통해 memory라는 이름의 환경 프로퍼티 값을 읽어와 해당 값이 on과 같으면 true를, 아닌 경우에는 false를 반환하도록 만들었다.

 

MemoryConfig

@Configuration
@Conditional(MemoryCondition.class)
public class MemoryConfig {

    @Bean
    public MemoryController memoryController() {
       return new MemoryController(memoryFinder());
    }

    @Bean
    public MemoryFinder memoryFinder() {
       return new MemoryFinder();
    }
}

@Conditional 애노테이션에 MemoryCondition.class를 지정하면 MemoryCondition의 조건에 따라 달라진다. 즉, true를 반환하는 경우 설정 파일을 읽어 빈으로 등록하고 false인 경우에는 빈을 등록하지 않는다.

 

결과

VM Options를 통해서 memory 프로퍼티를 설정하고 실행하면 위 결과처럼 사용중인 메모리량과 최대 메모리량(8GB)가 나오는 것을 확인할 수 있다.

 

@Conditional의 다양한 옵션

@ConditionalOnProperty는 환경 프로퍼티가 설정되고 그 값이 특정한 조건을 만족할 때 빈을 등록하도록 조건을 지정할 수 있다. name은 검사할 프로퍼티의 이름을 지정하고, havingValue는 프로퍼티의 값과 비교할 값을 지정하여 on과 비교하여 같을 때 조건을 만족하여 빈을 등록한다. 이렇게 @Conditional과 관련해서 개발자가 편리하게 사용할 수 있는 @Conditional 어노테이션들은 다음과 같다.

  • ConditionalOnClass: 클래스가 있는 경우 동작한다.
  • ConditionalOnMissingClass: 클래스가 없는 경우 동작한다.
  • ConditionalOnBean: 빈이 등록되어 있는 경우에 동작한다.
  • ConditionalOnMissingBean: 빈이 등록되어 있지 않은 경우에 동작한다.
  • ConditionalOnProperty: 환경 정보가 있는 경우 동작한다.
  • ConditionalOnResource: 리소스가 있는 경우 동작한다.
  • ConditionalOnWebApplication: 웹 애플리케이션인 경우에 동작한다.
  • ConditionalOnNotWebApplication: 웹 애플리케이션이 아닌 경우에 동작한다.
  • ConditionalOnExpression: SpEL 표현식에 만족하는 경우 동작한다.

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations

위 사이트를 가면 더 자세하게 살펴볼 수 있다.

 

자동구성 라이브러리 만들기

스프링 부트가 제공하는 자동 구성 기능을 이해하려면 핵심 어노테이션인 @Conditional 과 @AutoConfiguration을 이해해야 한다. @Conditional에 대해서 알아봤고, @AutoConfiguration에 대해 알아보자.

 

라이브러리용 memory-v1.0 프로젝트 생성

프로젝트에 라이브러리를 추가만 하면 모든 구성이 자동으로 처리되기 위해서는 @AutoConfiguration 어노테이션을 적용하면 된다. 그러면 자동 구성 클래스가 되어 Spring Boot 애플리케이션을 시작할 때, 클래스 패스(classpath) 상의 파일을 통해 자동 구성 클래스를 검색하고 로드하여 라이브러리만 추가하더라도 자동으로 빈으로 등록할 수 있다.

그리고 해당 프로젝트를 빌드하면 build/libs의 memory-v1.0.jar가 있는 것을 확인할 수 있다. 이를 다른 프로젝트의 라이브러리에 추가할 것이다.

 

org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일은 Spring Boot 자동 구성 기능과 관련된 메타데이터 파일 중 하나로 자동 구성 클래스를 로드하고 활성화하기 위해 사용된다. 즉, 스프링 부트는 시작 시점에 org.springframework.boot.autoconfigure.AutoConfiguration.imports의 정보를 읽어서 자동 구성으로 사용한다. 따라서 내부에 있는 MemoryAutoConfig가 자동으로 실행된다.

 

새로운 프로젝트에 libs/memory-v1.0.jar을 넣어놓고, VM Options에 -Dmemory=on 프로퍼티를 추가하고 실행하면 자동으로 빈으로 등록된 Controller에 의해 /memory 경로를 요청하면 위처럼 결과를 확인할 수 있다.

 

org.springframework.boot.autoconfigure.AutoConfiguration.imports

처음에 spring-boot-autoconfigure 프로젝트를 통해서 자동 구성 클래스들이 구성되어 있다고 설명했다. 해당 프로젝트도 마찬가지로 org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일이 있고 해당 파일을 살펴보면 우리가 살펴봤던 JdbcTemplateAutoConfiguration이 등록되어 있는 것을 확인할 수 있다. 따라서 스프링 애플리케이션이 시작되는 시점에 해당 파일의 정보를 읽어 자동 구성을 활성화한다.

 

스프링 부트 자동 구성 동작 순서

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

 

AutoConfigApplication

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

 

@SpringBootApplication

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

 

@EnableAutoConfiguration

@Import는 주로 스프링 설정 정보를 포함할 때 사용하는데 AutoConfigurationImportSelector를 열어보면 @Configuration이 아니다. 이 기능을 이해하려면 ImportSelector에 대해 알아야 한다.

 

ImportSelector

@Import에 설정 정보를 추가하는 방법에는 다음과 같이 2가지가 있다.

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

정적인 방법처럼 코드에 딱 정해진 것이 아니라 특정 조건에 따라서 설정 정보를 선택해야 하는 경우에 동적인 방법을 선택해야 하고 Spring은 설정 정보 대상을 동적으로 선택할 수 있는 ImportSelector 인터페이스를 제공한다.

 

ImportSelector 인터페이스의 selectImports 메서드는 String 배열을 반환한다. 여기서 알 수 있듯이 org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일들을 열어서 설정 정보를 선택하고, 해당 파일의 설정 정보가 스프링 컨테이너에 빈으로 등록되어 사용되는 것이다.

 

AutoConfigurationImportSelector

ImportSelector 인터페이스를 구현한 AutoConfigurationImportSelector 클래스의 selectImports 메서드에는 getAutoConfigurationEntry 메서드가 있고 해당 메서드를 디버거를 통해 살펴보면 configurations에는 org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일들의 자동 구성 클래스들이 있는 것을 확인할 수 있다.

 

그리고 getCandidateConfigurations 메서드를 살펴보면 ImportCandidates의 load 메서드를 실행하는 것을 확인할 수 있고, 인자로 AutoConfiguration.class를 넘겨주고 있다. 그리고 load 메서드를 살펴보면 LOCATION 상수를 있는 것을 확인할 수 있는데 우리가 지금까지 살펴봤던 META-INF/spring/%s.imports 경로로 해당하는 경로의 모든 파일을 읽어서 자동 구성 클래스를 동적으로 등록한다.

 

출처

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

 

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

빈 후처리기(BeanPostProcessor)  (0) 2023.10.17
프록시 팩토리를 통한 AOP  (1) 2023.10.16
ThreadLocal을 사용하여 로깅하기 + MDCFilter  (0) 2023.10.08
스프링 예외 추상화  (0) 2023.10.07
스프링 트랜잭션  (1) 2023.10.05