Enjoy My Posts

Spring Boot 구동 시 refreshContext에서 Configuration 클래스들을 scan하는 코드 따라가보기

Posted on By Geunwon Lim

스프링 애플리케이션이 구동되는 것을 따라가보며 공부한 몇가지를 적어보겠습니다. 이 포스트에서는 “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() 이외 기타(?) 적을만한 것

  1. createApplicationContext(): 일단 어찌보면 스프링에 있어서 가장 중요한 ApplicationContext를 생성합니다. ApplicationContext는 추상화되어 있고 구현체는 굉장히 다양합니다. AnnotationConfigApplicationContext, FileSystemXmlApplicationContext등과 같이요. 그리고 FailureAnalizer라는 빈을 등록하는데, 아마도 Exception을 처리해주는 것 같아요. 잘 모르니 넘어가겠습니다.

    코드를 봤을 때 든 생각은 개발자가 핵심적으로 바뀔 부분을 1. ‘빈을 어떻게 로드할지’와 2. ‘어떤 환경(?)일지’라고 생각한 것 같아요. 빈을 어떻게 로드할지는 크게 xml, 어노테이션 등으로 바뀔 것 같습니다. 그리고 어떤 환경일지는 웹인지 그렇지 않은지 등이 있을 것 같은데, 이건 좀 헷갈립니다. 어떤건 WebApplicationContext라고 돼있기도 하고 어떤건 WebServerApplicationContext라고 되어 있는 것도 있거든요. WebApplication과 WebServerApplication이 뭐가 다른지도 모르겠고…^^; Reactive가 붙은것도 있고 아닌것도 있고, Servlet이 붙은 것도 있고 아닌 것도 있고. 어쨌든 다양한 구현체가 있고, 빈을 로드하는 방식이 중요한 변화 사항인 건 맞는것같습니다!

  2. 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;
}

코드를 보면서 궁금증이 해결되기도 했지만, 다른 부분도 더 찬찬히 봐야겠다는 생각이 들었습니다. 리스너와 관련된 부분이 특히 더 궁금했고, 나중에 주입하는 것들도 보고 싶네요. 원래 계획이 공부하는 김에 스프링이 구동되는 과정 전체를 다뤄보고 싶었는데, 그러기에는 아직 모르는것들이 많더라고요… 잘 모르면서 따라가니까 디버거를 여러번 돌려야해서 오래 걸리더라고요…^^; 공부하면서 이전보다 ‘아 이런식이구나’하는 감은 좀 생긴것같은데, 막상 글로 적으려고 하니 적을만한게 별로 없네요… 쓰고보니 내용이 부실한것같아서 아쉽습니다. 코딩을 조금 미뤄뒀었는데, 이제 코딩을 좀 하고싶어서 다른 부분은 다음으로 넘기겠습니다~!