스프링 부트의 @SpringBootConfiguration을 학습하며 작성된 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.
1. @SpringBootConfiguration
@SpringBootApplication 어노테이션을 살펴보면, 여기에는 @SpringBootConfiguration이 포함되어 있습니다. 이 어노테이션은 스프링 부트에서 사용되는 특별한 어노테이션으로, @Configuration을 확장한 형태입니다. 일반적으로 이는 애플리케이션을 실행하는 메인 클래스, 즉 핵심 설정 클래스에 사용되며, @SpringBootConfiguration이 붙은 클래스는 애플리케이션의 주요 동작을 정의하고 관리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@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 {
......
}
이는 @Configuration과 헷갈릴 수 있는데 그 차이점에 대해 간략히 살펴보겠습니다. 위에서 @Configuration을 확장한다고 설명했는데요, 이는 @Configuration의 기능을 포함합니다. 즉 해당 어노테이션이 붙은 클래스를 빈으로 등록해 줍니다. 이를 확인하기 위해 @SpringBootApplication이 붙은 클래스와 별개로 아래와 같은 클래스를 만든 후 애플리케이션을 실행시켜 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootConfiguration
public class TestConfiguration {
private static final Logger log = LoggerFactory.getLogger(TestConfiguration.class);
public TestConfiguration() {
log.info("---------------------------------------------------------------------------");
log.info("TEST");
log.info("---------------------------------------------------------------------------");
}
}
그러면 아래와 같이 @SpringBootConfiguration 빈을 붙인 클래스가 빈으로 등록된 것을 볼 수 있습니다. 해당 어노테이션이 붙은 클래스는 빈으로 @Configuration과 마찬가지로 빈으로 등록되며, 이는 @SpringBootApplication과 함께 사용될 수 있습니다.
물론 이를 사용하는 것은 괜찮지만 스프링 부트가 설정한 다양한 설정과 얽힐 수 있기 때문에 일반적으로 한 프로젝트에서는 하나의 @SpringBootConfiguration만 사용하는 것을 권장드립니다.
2. 어디에 사용될까?
대표적으로 테스트 코드에서 테스트 문맥을 구성할 때 해당 어노테이션이 사용됩니다. 통합 테스트를 실행하면 이는 우리가 작성한 애플리케이션의 context를 읽어와 테스트 환경을 구성하는데, 이 과정에서 우리가 만든 애플리케이션의 context를 읽어올 때 사용하는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {
......
// 우리가 작성한 애플리케이션의 context를 읽어 테스트를 위한 문맥 구성
@Override
public TestContext buildTestContext() {
TestContext context = super.buildTestContext();
verifyConfiguration(context.getTestClass());
WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass());
if (webEnvironment == WebEnvironment.MOCK
&& deduceWebApplicationType() == WebApplicationType.SERVLET) {
context.setAttribute(ACTIVATE_SERVLET_LISTENER, true);
} else if (webEnvironment != null && webEnvironment.isEmbedded()) {
context.setAttribute(ACTIVATE_SERVLET_LISTENER, false);
}
return context;
}
......
}
하지만 @SpringBootConfiguation은 우리가 직접적으로 사용할 일은 사실 거의 없습니다. 대부분 스프링부트가 해당 어노테이션을 읽어 자동으로 설정을 해주기 때문입니다.
테스트에서 해당 어노테이션을 어디서 읽는지 한 번 살펴보겠습니다. 이는 SpringBootTestContextBootstrapper 클래스의 findConfigurationClass에서 context를 읽기 위해 사용됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {
......
private Class<?> findConfigurationClass(Class<?> testClass) {
String propertyName = "%s.SpringBootConfiguration.%s"
.formatted(SpringBootTestContextBootstrapper.class.getName(), testClass.getName());
String foundClassName = this.aotTestAttributes.getString(propertyName);
if (foundClassName != null) {
return ClassUtils.resolveClassName(foundClassName, testClass.getClassLoader());
}
// @SpringBootConfiguration가 붙은 클래스를 조회
Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class).findFromClass(
testClass);
Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
+ "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
this.aotTestAttributes.setAttribute(propertyName, found.getName());
return found;
}
......
}
해당 어노테이션이 없다면 테스트를 위한 문맥을 구성할 수 없습니다. 이를 테스트하기 위해 아래와 같이 @SpringBootApplication을 주석처리하고 테스트를 실행해 보겠습니다.
1
2
3
4
5
6
7
8
//@SpringBootApplication
public class StudySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(StudySpringBootApplication.class, args);
}
}
그러면 다음과 같은 예외가 발생하는 것을 볼 수 있는데, 테스트를 위한 문맥을 구성할 수 없는 것입니다.
이는 Assert.state에서 발생한 예외로, context 구성을 위해 @SpringBootConfiguration이 붙은 클래스를 조회했지만, 이를 발견하지 못해 발생한 예외입니다.
코드 레벨에서 보면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {
......
private Class<?> findConfigurationClass(Class<?> testClass) {
String propertyName = "%s.SpringBootConfiguration.%s"
.formatted(SpringBootTestContextBootstrapper.class.getName(), testClass.getName());
String foundClassName = this.aotTestAttributes.getString(propertyName);
if (foundClassName != null) {
return ClassUtils.resolveClassName(foundClassName, testClass.getClassLoader());
}
// @SpringBootConfiguration가 붙은 클래스를 조회
Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class).findFromClass(
testClass);
// 찾지 못하면 예외 발생
Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
+ "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
this.aotTestAttributes.setAttribute(propertyName, found.getName());
return found;
}
......
}
3. 의문점
여기서 두 가지 의문점이 들었는데, @SpringBootApplication을 주석처리 했을 때의 테스트가 될까?, @SpringBootApplication을 주석처리 한 채 @SpringBootConfiguation만 사용하면 테스트가 정상적으로 돌아갈까?였습니다. 우선 결론부터 말하면 둘 다 실행되지 않습니다.
첫 번째 케이스를 알기 위해서는 어디서 basePackage를 읽는지 알아야 합니다. 스프링 부트에서 테스트가 실행되면 우선 @SpringBootConfiguation이 붙은 클래스를 찾아 context를 읽어옵니다. 이후 해당 클래스를 찾으면 ComponentScanAnnotationParser를 통해 basePackage를 찾습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ComponentScanAnnotationParser {
......
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan,
......
Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
......
}
}
아래와 같이요. 따라서 @SpringBootApplication이 주석처리되면 테스트 자체를 돌릴 수 없습니다. @SpringBootConfiguration 어노테이션을 읽지를 못하니까요.
두 번째 경우도 비슷한데, @SpringBootApplication 어노테이션은 아래와 같이 복합적인 어노테이션으로 구성되어 있습니다. @SpringBootConfiguration 하나만 있다고 동작하는 것이 아니라 @EnableAutoConfiguration, @ComponentScan 등을 통해 빈으로 등록된 클래스를 읽어와 context를 형성하는데, @SpringBootApplication 없이 @SpringBootConfiguration만으로는 이런 문맥을 만들 수 없습니다. 따라서 두 가지 모두 테스트가 실행되지 않습니다.
1
2
3
4
5
6
7
8
@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) })
4. 정리
이 어노테이션은 스프링 부트에서 사용되는 특별한 어노테이션으로, @Configuration을 확장한 형태입니다. 일반적으로 이는 애플리케이션을 실행하는 메인 클래스, 즉 핵심 설정 클래스에 사용되며, @SpringBootConfiguration이 붙은 클래스는 애플리케이션의 주요 동작을 정의하고 관리합니다. 이는 우리가 직접 사용할 일은 거의 없으며, 테스트에서 애플리케이션의 context를 읽어올 때 사용됩니다.