[Spring Boot] @SpringBootApplication 파헤치기

Jiwoo Kim·2021년 9월 13일
4
post-thumbnail

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이 실행되는데, runrun이지만 그 전에 @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 Configuration Mechanism

이렇게 많은 클래스들이 항상 다 빈으로 등록된다면 엄청난 리소스 낭비가 될 것이다. 따라서 스프링 부트는 현재 프로젝트에서 필요한 부분만 auto configure 되도록 하며, AutoConfigurationImportSelector@Conditional 어노테이션 등을 활용해 필터링 작업을 수행한다.

그렇다면 실제로 AutoConfigurationImportSelector가 어떻게 일을 하는지 알아보자.

일단 클래스를 포함할지 제외할지 결정하기 위해서는 그 기준이 필요하다. 필터링은 AutoConfigurationImportFilterOnBeanCondition, 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가 상속받는 FilteringSpringBootConditionmatch() 메서드에 브레이크 포인트를 찍고 Stack trace를 살펴 봤다. 그 결과 대략 아래 순서로 필터링이 진행되는 것을 확인했다.

SpringApplication.run()SpringApplication.refreshContext()
ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
ConfigurationClassParser.parse()
AutoConfigurationImportSelector.process()
ConfigurationClassFilter.filter()
FilteringSpringBootCondition.match()

여기서 FilteringSpringBootConditionAutoConfigurationImportFilter을 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;
    // ...
}

그리고 마지막으로 AutoConfigurationImportSelectorselectImports()에서 제외할 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

1개의 댓글

comment-user-thumbnail
2021년 12월 1일

포스트 잘 봤습니다. 정말 감사합니다.

답글 달기