이번 글에서는 Spring Boot의 주요 기능중 하나인 AutoConfiguration이 어떤 원리로 동작하는지 코드 레벨에서 살펴본다.

Spring Boot에는 아주 유용한 기능인 AutoConfiguration 이 있다. 나는 최근에 MSA로 구성된 프로젝트를 하면서 이 기능을 활용하여 설정을 자동화 한 경험이 있었는데, 너무 편리했을 뿐 아니라 스프링의 깊은 부분까지 이해할 수 있었다.
우선 어떤 경로로 관련 모듈들이 로딩되는지 살펴보자
spring-boot-starter-*로 시작하는 디펜던시들을 공통적으로 spring-boot-starter를 api 로 가지고 있다.

참고로 gradle에서 api는 이를 참조하는 프로젝트에도 이 모듈을 노출하는 것을 의미한다.
그리고 저 spring-boot-starter 모듈에는 우리가 스프링 어플리케이션을 작성할 때 썼던 @SpringBootApplication 어노테이션이 있고, 이 어노테이션 안에는 @EnableAutoConfiguration이 들어가 있다.
package org.springframework.boot.autoconfigure;
//...
@EnableAutoConfiguration
//...
public @interface SpringBootApplication {
스프링에는 수많은 @Enable류의 어노테이션이 있는데, 공통적으로 @Import 어노테이션과 함께 설정을 로드한다. 이 @Import어노테이션에 @Configuration 클래스나 ImportSelector, ImportBeanDefinitionRegistrar 구현체를 넣어주면, 설정을 함께 로드하게 된다.
//...
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
여기서는 AutoConfigurationImportSelector라는 ImportSelector구현체를 가져오고 있다.
클래스 코드는 읽기 쉬우니, 생략하고 핵심 부분만 보겠다.
// AutoConfigurationImportSelector :: getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
.getCandidates();
//...
return configurations;
}
// ImportCandidates :: load
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
//...
// String LOCATION = "META-INF/spring/%s.imports";
String location = String.format(LOCATION, annotation.getName());
//...
return new ImportCandidates(importCandidates);
}
보면 spring resource 중에 META-INF/spring/%s.imports형식의 경로를 뽑아내는 것을 볼 수 있고, 이 코드에서는 META-INF/spring/AutoConfiguration.imports 의 경로를 가리키는 것을 볼 수 있다.
실제로 AutoConfiguration이 동작하기 위해서는 META-INF/spring/AutoConfiguration.imports 에 AutoConfiguration 클래스를 등록해줘야 한다. 우리가 만약 직접 자동 설정을 만들고 싶다면, 저 경로에 클래스 명을 적어줘야한다.
그렇다면, 우리가 단순히 boot web을 가져왔을 뿐인데, 어떻게 필요한 빈들이 등록되었을까?
실제로 소스코드를 보면, boot-starter의 모든 AutoConfiguration들이 등록되어 있는 것을 볼 수 있다.
Spring Boot 프로젝트의 spring-boot-autoconfigure모듈은 다양한 모듈(security, web, webflux, jpa ...)의 자동 설정 클래스 들이 담겨 있고, spring-boot-starter 모듈을 임포트하면 같이 로드된다.
나의 경우 MSA 프로젝트를 진행하면서, 설정지옥에 빠진 적이 있다. 생각해보라, Spring Cloud Eureka나 Config같은 설정을 5,6개가 넘는 서버에 일관되게 적용하려면? 엄청난 반복작업이 펼쳐질 것이다. (에러 나기도 쉽다)
나의 경우에는, gradle의 멀티 모듈기능을 활용하여 AutoConfiguration 모듈을 만들고, @Conditional(조건에 따라 설정을 로드하도록 설정할 수 있다.) 어노테이션으로 조건을 설정하였다. (예를 들어 서버가 서블릿 기반인지 리액티브 기반인지에 따라..)
이렇게 하면 하나의 설정만 변경시키면 이 모듈을 참조하는 모든 서버의 설정을 변경할 수 있다.
Spring Boot에 기여할 생각이 있다면, 모듈 구조를 알 필요가 있다.
spring-boot-starter-*로 시작하는 것은 공식적으로 boot에서 제공하는 라이브러리에만 붙일 수 있다.
이러한 모듈(ex spring-boot-starter-security) 들은 본래 모듈(spring-security)와 spring-boot-starter 모듈을 api형태로 노출하고 있다.
spring-boot-starter모듈은 또 다시 spring-boot-autoconfigure 를 노출하고 있다.
만약 자동설정을 추가하거나 변경하고 싶다면, spring-boot-autoconfigure 모듈을 수정하고, /src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports를 수정하면 된다.
또한 자동설정을 로드하는 코드 들도 바로 이 모듈에 있으므로, 이 부분을 변경하고 싶다면 해당 모듈을 참조하자
boot 프레임워크가 별것이 아니다. 내가 만든 프레임워크를 사용하기 편하게 자동설정 형태로 제공하고 싶다면 고려해보면 된다. 실제로 이러한 boot 프레임워크가 꽤 있다.
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-spring-boot-starter</artifactId>
</dependency>
예를 들면, timefold solver(구 Opta planner)는 최적화 기법 기반 ai 툴인데, 여기서는 spring boot와 손쉽게 통합하는 boot 모듈을 제공한다.
근데 중요한 점은 spring boot 공식 프레임워크의 경우
spring-boot-starter-* 접두어를 가지고 있지만
비공식 프레임워크의 경우
*-spring-boot-starter 접미어를 붙여야 한다는 것이다.
어쨌든, 내가 설정하려는 모듈과 spring-boot-starter 모듈을 api로 노출시키고, 자동설정을 작성한 뒤, META-INF 하위에 설정해주면 된다.
이렇게 하면, spring 어플리케이션에서 우리 모듈을 로드하고 @EnableAutoConfiguration을 활성화 하면, (혹은 스프링 부트 어플리케이션이면 자동으로 들어가 있음) 우리의 설정이 반응하여 실행된다.
@Conditional이라는 조건부 기능이 있으니 이 부분은 다음에 다루기로 하겠다.
이 기능을 사용하면 상황을 체크해서 조건부로 설정이 가능하다.(예를 들면, 특정 빈이 등록되어 있지 않을 때나, servlet기반 어플리케이션일 때 등)