스프링 애플리케이션이 구동되는 것을 따라가보며 공부한 몇가지를 적어보겠습니다. 이 포스트에서는 “Configuration 클래스들을 스캔하는 부분까지의 코드 따라가기”가 주요 내용일 것 같네요. 스프링을 쓰긴 하지만 어떤 식으로 빈들을 스캔하고 등록하는지는 잘 몰랐습니다. 아직도 어렵긴 하지만 코드로 보니까 조금은 구체화된 것 같아요.
1. SpringApplication.run(String… args) 전체 코드
나무를 보기 전 숲을 보는게 좋을 것 같아서 run() 코드를 다 복붙해봤습니다. 여기에서는 refreshContext(context) 중 일부를 다룬다고 생각해주셔도 좋을 것 같아요.
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
2. refreshContext
빈 팩토리 설정(DefaultListableBeanFactory)을 하고, scan을 하고 빈들을 등록하고 합니다. scan하는 부분은 특히 궁금하던 것이었기에 코드를 천천히 따라가봤는데요. invokeBeanFactoryPostProcessors() 안에서 스캔을 하더라고요. 여기서는 Configuration 클래스들만 스캔, 등록하는 것 같습니다.
// AbstractApplicationContext.java
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
...
try {
...
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
...
}
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
...
}
// PostProcessorRegistrationDelegate.java
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
...
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
...
}
private static void invokeBeanDefinitionRegistryPostProcessors(
Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {
for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
}
// ConfigurationClassPostProcessor.java
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
processConfigBeanDefinitions(registry);
}
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
do {
parser.parse(candidates);
...
}
while (!candidates.isEmpty());
}
// ConfigurationClassParser.java
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
...
}
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}
// ConfigurationClassParser.java
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
...
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
...
}
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
...
}
...
}
// ComponentScanAnnotationParser.java
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
...
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
3. refreshContext() 이외 기타(?) 적을만한 것
-
createApplicationContext(): 일단 어찌보면 스프링에 있어서 가장 중요한 ApplicationContext를 생성합니다. ApplicationContext는 추상화되어 있고 구현체는 굉장히 다양합니다. AnnotationConfigApplicationContext, FileSystemXmlApplicationContext등과 같이요. 그리고 FailureAnalizer라는 빈을 등록하는데, 아마도 Exception을 처리해주는 것 같아요. 잘 모르니 넘어가겠습니다.
코드를 봤을 때 든 생각은 개발자가 핵심적으로 바뀔 부분을 1. ‘빈을 어떻게 로드할지’와 2. ‘어떤 환경(?)일지’라고 생각한 것 같아요. 빈을 어떻게 로드할지는 크게 xml, 어노테이션 등으로 바뀔 것 같습니다. 그리고 어떤 환경일지는 웹인지 그렇지 않은지 등이 있을 것 같은데, 이건 좀 헷갈립니다. 어떤건 WebApplicationContext라고 돼있기도 하고 어떤건 WebServerApplicationContext라고 되어 있는 것도 있거든요. WebApplication과 WebServerApplication이 뭐가 다른지도 모르겠고…^^; Reactive가 붙은것도 있고 아닌것도 있고, Servlet이 붙은 것도 있고 아닌 것도 있고. 어쨌든 다양한 구현체가 있고, 빈을 로드하는 방식이 중요한 변화 사항인 건 맞는것같습니다!
-
prepareContext()에서는 기본 설정등을 세팅해줍니다. 꼭 필요한 빈들을 등록해준다고 생각하면 될 것 같습니다. 예를 들어 ContextId, Environment(Profile과 관련된), applicationArguments 등을 등록해주죠. 그리고 Source를 로드해줍니다. 여기서 말하는 소스에는 설정파일, Application 객체 등을 말합니다. 예를들어 스프링 부트 기준으로 @SpringBootApplication가 붙어있는 클래스를 빈으로 등록해주는 것이죠.
load 하는 부분의 코드를 조금 봐보면, 소스를 빈으로 등록해주고 있습니다.
private int load(Class<?> source) {
if (isEligible(source)) {
this.annotatedReader.register(source);
return 1;
}
return 0;
}
코드를 보면서 궁금증이 해결되기도 했지만, 다른 부분도 더 찬찬히 봐야겠다는 생각이 들었습니다. 리스너와 관련된 부분이 특히 더 궁금했고, 나중에 주입하는 것들도 보고 싶네요. 원래 계획이 공부하는 김에 스프링이 구동되는 과정 전체를 다뤄보고 싶었는데, 그러기에는 아직 모르는것들이 많더라고요… 잘 모르면서 따라가니까 디버거를 여러번 돌려야해서 오래 걸리더라고요…^^; 공부하면서 이전보다 ‘아 이런식이구나’하는 감은 좀 생긴것같은데, 막상 글로 적으려고 하니 적을만한게 별로 없네요… 쓰고보니 내용이 부실한것같아서 아쉽습니다. 코딩을 조금 미뤄뒀었는데, 이제 코딩을 좀 하고싶어서 다른 부분은 다음으로 넘기겠습니다~!