Spring Initializr
Initializr generates spring boot project with just what you need to start quickly!
Spring Initializr
이 놈은 스프링 부트 프로젝트 생성에 필요한 몇 가지 설정만 입력하면, 자동으로 이것저것 세팅해서 프로젝트를 짠 만들어 주는 놈이다. 홈페이지나 Intellij 유료 버전에서 사용할 수 있는데, 즉시 어플리케이션을 빌드&실행할 수 있는 환경을 깔끔하게 세팅해준다. 정말 마법같은 일이 아닐 수 없다. 스프링 프레임워크를 사용했다면 몇 백 줄이 넘는 xml
세팅 파일을 관리해야 했을 텐데 말이다. 이 마법같은 일을, 스프링 부트는 대체 어떻게 해주는 걸까?
Application.java
여러가지 깊게 공부할 내용이 있겠지만, 이 포스팅에서는 우선 Application.java
를 먼저 보겠다.
스프링 부트 프로젝트를 만들면, 유일한 프로덕션 소스 코드로서 아래와 같은 내용이 생성된다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
어플리케이션을 실행시키면 이 클래스의 main
이 실행되는데, run
도 run
이지만 그 전에 @SpringBootApplication
어노테이션이 많은 일을 해줄 것이라 짐작할 수 있다.
@SpringBootApplication
@SpringBootApplication
은 자동 설정을 해주기 위한 어노테이션으로, org.springframework.boot.autoconfigure
패키지에 들어 있다. 코드를 열어 보면 이렇게 적혀 있다.
@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 {
// ...
}
여기서 중요한 것은 스프링 고유 어노테이션 @SpringBootConfiguration
, @EnableAutoConfiguration
, @ComponentScan
세 가지다. 공식 문서를 참고해서 각각에 대해 자세히 살펴보자.
@EnableAutoConfiguration
enable Spring Boot’s auto-configuration mechanism
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration
스프링 부트의 Application Context 설정을 자동으로 수행한다는 어노테이션으로, META-INF/spring.factories
에 정의되어 있는 configuration 대상 클래스들을 빈으로 등록한다.
우선, spring.factories
를 열어 보면, 185줄에 달하는 엄청나게 많은 클래스들이 스프링 부트 기본 auto configuration 대상으로 나열이 되어 있다.
// ...
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
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,\
// ...
이 리스트에 있는 클래스들은 @Configuration
어노테이션이 없어도 자동으로 빈으로 등록된다. 추가로 개발자가 직접 이 spring.factories
에 클래스를 적어 넣으면 그 클래스 역시 auto configuration 대상에 포함된다.
이렇게 많은 클래스들이 항상 다 빈으로 등록된다면 엄청난 리소스 낭비가 될 것이다. 따라서 스프링 부트는 현재 프로젝트에서 필요한 부분만 auto configure 되도록 하며, AutoConfigurationImportSelector
가 @Conditional
어노테이션 등을 활용해 필터링 작업을 수행한다.
그렇다면 실제로 AutoConfigurationImportSelector
가 어떻게 일을 하는지 알아보자.
일단 클래스를 포함할지 제외할지 결정하기 위해서는 그 기준이 필요하다. 필터링은 AutoConfigurationImportFilter
가 OnBeanCondition
, OnClassCondition
, OnWebApplicationCondition
세 가지의 Condition을 바탕으로 진행한다. 이 클래스들도 모두 spring.factories
에 정의되어 있다.
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
이게 어떻게 활용되는지 보기 위해 일단 OnClassCondition
가 상속받는 FilteringSpringBootCondition
의 match()
메서드에 브레이크 포인트를 찍고 Stack trace를 살펴 봤다. 그 결과 대략 아래 순서로 필터링이 진행되는 것을 확인했다.
SpringApplication.run()
→SpringApplication.refreshContext()
→ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
→ConfigurationClassParser.parse()
→AutoConfigurationImportSelector.process()
→ConfigurationClassFilter.filter()
→FilteringSpringBootCondition.match()
여기서 FilteringSpringBootCondition
은 AutoConfigurationImportFilter
을 implements 한다.
그래서 얘가 이렇게 매치 여부 확인을 요청하는 역할을 하고,
abstract class FilteringSpringBootCondition extends SpringBootCondition
implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {
// ...
@Override
public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
boolean[] match = new boolean[outcomes.length];
for (int i = 0; i < outcomes.length; i++) {
match[i] = (outcomes[i] == null || outcomes[i].isMatch());
if (!match[i] && outcomes[i] != null) {
logOutcome(autoConfigurationClasses[i], outcomes[i]);
if (report != null) {
report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]);
}
}
}
return match;
}
// ...
}
OnClassCondition
, OnBeanCondition
등의 ConfigurationCondition
은 실제로 AutoConfigurationMetadata
에 매칭되는 애가 있는지 확인을 한다.
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {
// ...
@Override
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
for (int i = 0; i < outcomes.length; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
Set<String> onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");
outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class);
if (outcomes[i] == null) {
Set<String> onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass,
"ConditionalOnSingleCandidate");
outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);
}
}
}
return outcomes;
}
// ...
}
이런 식으로 모든 필터링이 진행되면 필요한&제외할 configuration 정보는 AutoConfigurationEntry
에 저장된다.
protected static class AutoConfigurationEntry {
private final List<String> configurations;
private final Set<String> exclusions;
// ...
}
그리고 마지막으로 AutoConfigurationImportSelector
의 selectImports()
에서 제외할 configuration을 제거하고, 나머지 entries를 반환하면서 필터링이 마무리된다.
@Override
public Iterable<Entry> selectImports() {
if (this.autoConfigurationEntries.isEmpty()) {
return Collections.emptyList();
}
Set<String> allExclusions = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
processedConfigurations.removeAll(allExclusions);
return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
.map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
.collect(Collectors.toList());
}
@SpringBootConfiguration
enable registration of extra beans in the context or the import of additional configuration classes. An alternative to Spring’s standard
@Configuration
that aids configuration detection in your integration tests.
@ComponentScan
enable
@Component
scan on the package where the application is located
Spring Boot Reference Document: Using the @SpringBootApplication Annotation
포스트 잘 봤습니다. 정말 감사합니다.