
Spring Boot의 자동 설정 기능은 정말 강력합니다. 의존성만 추가해주면 Spring Boot가 뒤에서 필요한 설정들(Bean 설정 및 생성)을 자동으로 해줍니다. 이러한 강력한 기능으로 우리의 개발 시간을 줄여주기도 하지만, 간혹 커스터마이징이 필요한 경우 그 원리를 모른다면 꽤나 애를 먹기도 합니다.
이번 포스트에서는 Spring Boot의 AutoConfiguration의 동작 원리에 대해 파헤쳐보고, 그 원리를 이용해 나만의 AutoConfiguration을 커스터마이징 해보도록 하겠습니다.
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
}
AutoConfiguration의 시작은 우리가 Spring Boot를 사용할 때 항상 보는 @SpringBootApplication 애노테이션입니다.
그 안을 들여다보면, @EnableAutoConfiguration 이라는 애노테이션이 있는데요. 이 친구가 바로 AutoConfiguration 활성화 시키는 애노테이션이죠.
Spring은 무엇을 자동 구성해야할지 어떻게 알 수 있을까요?
@EnableAutoConfiguration 에서 더 타고 들어와보면 AutoConfigurationImportSelector 라는 클래스를 볼 수 있습니다. 그 안에는 selectImport 라는 메서드가 있는데요. 이 메서드를 통해 Import할 클래스가 무엇인지 알 수 있게 됩니다.
/* AutoConfigurationImportSelector*/
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
getAutoConfigurationEntry() 메서드로 AutoConfigurationEntry를 반환받는군요. getAutoConfigurationEntry()를 한 번 보겠습니다.
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);
}
여기서는
getCantidateConfigurations()), removeDuplicates()),getExclusion()),configuration.removeAll()),getConfigurationClassFilter().filter()),AutoConfiguration 목록을 담은 AutoConfigurationEntry를 반환합니다.여기서 getCandidateConfigurations() 메서드는 애노테이션을 기반으로 자동 설정 후보군을 구성합니다.
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor 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;
}
ImportCandidates.load(AutoConfiguration.class, ..).forEach(configuration::add) 부분에서 알 수 있듯이, @AutoConfiguration 애노테이션이 붙은 클래스에 대한 정보를 모두 가져옵니다.

configuration에 담긴 정보들을 보니, @AutoConfiguration 이 설정된 클래스의 패키지를 포함한 Full Name을 가져오는 것을 알 수 있습니다.
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}
ImportCandidates.load() 메서드에서는 특정 경로(location)에서 후보군을 읽어오는데요. 디버깅으로 확인해보니 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 경로입니다.(spring-boot-autoconfigure 라이브러리 내부)
## META-INF/spring/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
...
해당 경로에는 자동 구성의 대상이 될 클래스들이 등록되어있습니다.
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(asList(attributes, "excludeName"));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
자동 설정을 의도적으로 제외하는 경우도 있는데요. 그럴때 애노테이션에 exclude, excludeName 속성에 값을 지정합니다. 그리고 이 값은 getExclusion() 메서드에서 읽어들여서 자동 설정되지 않도록 하는 것이죠.
다시 getAutoConfigurationEntry() 메서드로 돌아가보겠습니다.
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);
}
getConfigurationClassFilter().filter(configuration) 에서는 AutoConfigurationImportFilter를 적용해 자동 구성에 포함시킬 클래스와 그렇지 않은 클래스를 필터링합니다.
getConfigurationClassFilter() 내부에서
getAutoConfigurationImportFilter()는 OnBeanCondition OnClassCondition OnWebApplicationCondition 라는 3개의 AutoConfigurationImportFilter 를 반환합니다. ConfigurationClassFilter 는 ConfigurationClassFilter 로 래핑됩니다.private ConfigurationClassFilter getConfigurationClassFilter() {
if (this.configurationClassFilter == null) {
List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
for (AutoConfigurationImportFilter filter : filters) {
invokeAwareMethods(filter);
}
this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
}
return this.configurationClassFilter;
}
ConfigurationClassFilter는 filter() 를 메서드로 갖고 있고, AutoConfigurationMetadata를 클래스 변수로 갖습니다.AutoConfigurationMetadata 의 값은 META-INF/spring-autoconfigure-metadata.properties 의 값을 받아옵니다./* META-INF/spring-autoconfigure-metadata.properties */
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration=
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration$DataSourceInitializerConfiguration=
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration$DataSourceInitializerConfiguration.ConditionalOnBean=javax.sql.DataSource
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration$DataSourceInitializerConfiguration.ConditionalOnClass=org.springframework.jdbc.datasource.init.DatabasePopulator
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration.ConditionalOnBean=org.springframework.batch.core.launch.JobLauncher
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,org.springframework.batch.core.launch.JobLauncher
이 파일 안에는 ConditionalOnBean, ConditionalOnClass, AutoConfigureOrder, ConditionalOnMissingBean 과 같은 AutoConfiguration 클래스에 대한 메타데이터가 들어있습니다. filter() 메소드는 이러한 정보를 기반으로 필터링을 진행하는 것이죠.
위의 각 필터가 필터링하는 대상은 다음과 같습니다.
@ConditionalOnBean @ConditionalOnMissingBean @ConditionalOnSingleCandidate@ConditionalOnClass @ConditionalOnMissingClass@ConditionalOnWebApplication @ConditionalOnNotWebApplication실제 AutoConfiguration 클래스의 내부를 들여다보면,
@AutoConfiguration(after = RepositoryRestMvcAutoConfiguration.class)
@EnableSpringDataWebSupport
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ PageableHandlerMethodArgumentResolver.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(PageableHandlerMethodArgumentResolver.class)
@EnableConfigurationProperties(SpringDataWebProperties.class)
public class SpringDataWebAutoConfiguration {
...
@Conditional.. 애노테이션이 붙어있고, 여기에 설정되어 있는 값을 기반으로 필터는 해당 클래스를 자동 설정에 포함시킬지, 제외할 지 결정하게 되는 것입니다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
우리가 스프링을 사용할 때 data source를 구성하기 위해 application(or yaml) 파일에 위와 같은 설정을 하곤 합니다. 이런 설정은 AutoConfiguration 클래스의 @ConfigurationProperties 와 관련이 있습니다.
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {
DataSource 자동 구성과 관련된 DataSourceAutoConfiguration 클래스에는 @EnableConfigurationProperties 라는 애노테이션이 붙어있는데요. 이를 통해 설정 파일(properties)의 값을 자동 구성시에 사용할 수 있게됩니다.
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
...
private String name;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
...
DataSourceProperties 클래스를 보면 더 명확히 볼 수 있습니다. driverClassName, url, username, password 와 같은 속성들이 우리가 properties 파일에 작성한 속성들과 같죠?
이제 직접 AutoConfiguration을 구현해보겠습니다. 다음과 같은 과정으로 진행됩니다.
먼저, 멀티 모듈 프로젝트를 생성하여 4개의 모듈을 만듭니다.
ac-library (코어 로직이 담긴 모듈, AutoConfiguration으로 등록할 Class 추가)ac-spring-boot-autoconfigure (ac-library를 사용하기 위한 자동 설정을 수행하는 모듈)ac-spring-boot-starter (ac-library와 ac-spring-boot-autoconfigure를 패키징한 모듈)ac-client (ac-library를 사용하여 기능을 수행하는 모듈)네이밍 컨벤션
자동 설정 구성 시,xx-spring-boot-autoconfigure,xx-spring-boot-starter를 모듈 이름으로 만들어야 하며 이 둘을 합쳐서 하나로 만든다면xx-spring-boot-starter라는 모듈 이름을 가져야합니다.
settings.gradle.kts
rootProject.name = "ac"
pluginManagement {
repositories {
gradlePluginPortal()
}
}
include("ac-library")
include("ac-client")
include("ac-spring-boot-autoconfigure")
include("ac-spring-boot-starter")
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
id("org.springframework.boot") version "2.7.15"
id("io.spring.dependency-management") version "1.0.15.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
}
allprojects {
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
apply(plugin = "io.spring.dependency-management")
apply(plugin = "org.springframework.boot")
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
apply(plugin = "kotlin")
group = "realsy"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
}
project(":ac-library") {
val jar: Jar by tasks
val bootJar: BootJar by tasks
jar.enabled = true
bootJar.enabled = false
}
project(":ac-client") {
val jar: Jar by tasks
val bootJar: BootJar by tasks
jar.enabled = true
bootJar.enabled = true
dependencies {
api(project(":ac-spring-boot-starter"))
}
}
project(":ac-spring-boot-autoconfigure") {
val jar: Jar by tasks
val bootJar: BootJar by tasks
jar.enabled = true
bootJar.enabled = false
dependencies {
api(project(":ac-library"))
}
}
project(":ac-spring-boot-starter") {
val jar: Jar by tasks
val bootJar: BootJar by tasks
jar.enabled = true
bootJar.enabled = false
dependencies {
api(project(":ac-spring-boot-autoconfigure"))
}
}
ac-library코어 로직을 수행하는 ac-library 를 작성합니다. 여기서는 간단한 도어락 기능을 가진 클래스를 구성해보겠습니다.
DoorLock.kt
class DoorLock(private val password: String) {
fun unlock(password: String) {
runCatching {
check(this.password == password)
}.onFailure { println("Wrong Password!")
}.onSuccess { println("Welcome! (you entered $password)") }
}
}
비밀번호가 일치하는 지 여부에 따라 다른 메시지를 출력하는 메서드를 하나 만듭니다.
ac-spring-boot-autoconfigure여기서는 두 가지 클래스를 작성합니다.
DoorLockProperties : 도어락의 비밀번호 초기 값을 위한 클래스DoorLockAutoConfiguration : 도어락 클래스를 자동 설정하기 위한 클래스DoorLockProperties.kt
@ConfigurationProperties(prefix = "doorlock")
@ConstructorBinding
data class DoorLockProperties(val password: String)
DoorLockAutoConfiguration.kt
@AutoConfiguration
@ConditionalOnClass(DoorLock::class)
@EnableConfigurationProperties(DoorLockProperties::class)
class DoorLockAutoConfiguration(private val doorLockProperties: DoorLockProperties) {
@Bean
@ConditionalOnMissingBean
fun doorLock() = DoorLock(doorLockProperties.password)
}
@AutoConfiguration : 자동 설정할 클래스로 지정합니다.@ConditionalOnClass : DoorLock 클래스가 있을 때만 자동 설정 합니다.@EnableConfigurationProperties : 설정파일(properties/yaml)에서 초기 값을 불러와 설정하기 위한 애노테이션@ConditionalOnMissingBean : 빈이 등록되지 않은 경우에만 자동 설정으로 빈을 등록합니다./resource/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
sy.jin.acspringbootautoconfigure.DoorLockAutoConfiguration
위에서 본 것 처럼 스프링은 자동설정 시 spring.factories에서 자동 설정 정보를 가져오기 때문에 지정해줍니다.
ac-client이제 ac-library ac-spring-boot-autoconfigure 를 하나로 묶은 ac-spring-boot-starter를 사용해 자동등록하고, 기능을 실행시켜 보겠습니다.
application.properties
doorlock.password=12345
도어락 초기 비밀번호를 12345로 설정합니다.
@SpringBootApplication
class AcClientApplication(private val unlocker: Unlocker) : CommandLineRunner {
override fun run(vararg args: String?) {
unlocker.tryUnlock("123456")
}
}
fun main(args: Array<String>) {
runApplication<AcClientApplication>(*args)
}
@Service
class Unlocker(private val doorLock: DoorLock) {
fun tryUnlock(password: String) {
doorLock.unlock(password)
}
}
결과(123456 입력 시)
Wrong Password!
결과(12345 입력 시)
Welcome! (you entered 12345)
AutoConfigurationImportSelector 에서 선정한다.AutoConfigurationImportSelector 는 내부적으로 spring.factories 를 사용하여 자동설정 대상을 찾는다.@AutoConfiguration) 에서 @ConfigurationProperties 로 설정한다.- 스프링 부트의 Autoconfiguration 원리 및 만들어 보기
- Spring Boot 자동 구성 AutoConfiguration 동작하는 원리 파헤치기