Framework & Lib/스프링

스프링 빈 생성 과정 분석 [4] - 디버깅 참고 자료

코딩공장공장장 2024. 8. 7. 22:29

[스프링 빈 생성 과정 분석 시리즈]

 

1편부터 3편까지 하여 스프링 빈 생성 과정이 어떻게 이뤄지는지 알아보았다.

이번 포스팅에서는 이 개념들을 찾아보기 위해 실제 디버깅한 소스 코드를 공유하기 위해 작성하였다.

스프링 부트의 run 메서드를 통해 시간순으로 어떻게 흘러가는지 코드만 공유하겠다.

이를 통해 디버깅한다면 1-3편에서 설명한 빈 생성 과정의 실제 과정을 확인할 수 있을 것이다.

 

[SpringApplication 클래스 - run 메서드]

public ConfigurableApplicationContext run(String... args) {

   ... 생략

 

   try {

       ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

       ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);

       Banner printedBanner = this.printBanner(environment);

 

       // 1. applicationContext 생성

       context = this.createApplicationContext();

 

       this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

 

       // 2. beanDefinition 등록 및 빈 인스턴스화

       this.refreshContext(context);

 

      ... 생략

 

}

 

 

Application Context 생성


run 함수의 context = this.createApplicationContext();를 추적하면 DefaultApplicationContextFactory 클래스의 create 메서드의 반환값이 applicationContext로 사용됨을 알 수 있다.

이 create 함수는 다시 한번 getFromSpringFactories 메서드를 호출하여 해당 메서드의 리턴값을 넘기는데

 

public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {

   try {

// getFromSpringFactories의 결과를 리턴

       return (ConfigurableApplicationContext)this.getFromSpringFactories(webApplicationType, ApplicationContextFactory::create, this::createDefaultApplicationContext);

 

 

   } catch (Exception var3) {

       throw new IllegalStateException("Unable create a default ApplicationContext instance, you may need a custom ApplicationContextFactory", var3);

   }

}

 

getFromSpringFactories는 세번째 매개변수에 this::createDefaultApplicationContext를 넘기고 있다.

this::createDefaultApplicationContext는 이 클래스의 createDefaultApplicationContext 메서드이다.

 

private <T> T getFromSpringFactories(WebApplicationType webApplicationType, BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {

   Iterator var4 = SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class, this.getClass().getClassLoader()).iterator();

 

   Object result;

   do {

       if (!var4.hasNext()) {

           // defaultResult가 존재하면 defaultResult.get()을 리턴

           return defaultResult != null ? defaultResult.get() : null;

       }

 

       ApplicationContextFactory candidate = (ApplicationContextFactory)var4.next();

       result = action.apply(candidate, webApplicationType);

   } while(result == null);

 

   return result;

}

 

getFromSpringFactories는 세번째 파라미터인 defaultResult가 존재하면 defaultResult의 get() 함수를 호출하여 해당 함수의 결과를 리턴한다.

(* Supplier<T> 타입은 함수형 인터페이스로 이 타입을 매개변수로 선언하면 함수를 매개변수로 받을 수 있다.)

 

private ConfigurableApplicationContext createDefaultApplicationContext() {

   return (ConfigurableApplicationContext)(!AotDetector.useGeneratedArtifacts() ? new AnnotationConfigApplicationContext() : new GenericApplicationContext());

}

 

createDefaultApplicationContext 함수를 보면 위와 같이 AotDetector의 특정 값을 통해 AnnotationConfigApplicationContext 또는 GenericApplicationContext를 반환하게 되는데 별도의 설정을 하지 않는다면 AnnotationConfigApplicationContext이 반환되게 된다.

 

new AnnotationConfigApplicationContext(); 라는 코드를 통해 직접적으로 컨텍스트를 생성한다.

 

AnnotationConfigApplicationContext는 GenericApplicationContext를 상속하고 있는데 
GenericApplicationContext의 생성자에는 this.beanFactory = new DefaultListableBeanFactory();가 구현되어있다.
이를 통해 beanFactory가 설정된다.

 

Application Context 구성의 시작


refresh()

run 메서드의 refreshContext 메서드를 추적하면 아래와 같이 ConfigurableApplicationContext타입의 refresh() 메서드를 호출하는데  refresh() 메서드를 통해 context에 필요한 구현체를 등록하고 beanDefinition 등록 및 인스턴스화가 진행된다.

 

protected void refresh(ConfigurableApplicationContext applicationContext) {

   applicationContext.refresh();

}

 

우리가 사용하는 AnnotationConfigApplicationContext의 상위 타입 AbstractApplicationContext를 보면 refresh() 메서드에서 처리하는 내용을 알 수 있다.

 

public void refresh() throws BeansException, IllegalStateException {

   this.startupShutdownLock.lock();

 

   try {

       this.startupShutdownThread = Thread.currentThread();

       StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

       this.prepareRefresh();

       ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();

       this.prepareBeanFactory(beanFactory);

 

       try {

           this.postProcessBeanFactory(beanFactory);

           StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");

 

           // beanFactoryPostProcessor 등록(beanDefinition 등록)

           this.invokeBeanFactoryPostProcessors(beanFactory);

 

           this.registerBeanPostProcessors(beanFactory);

           beanPostProcess.end();

           this.initMessageSource();

           this.initApplicationEventMulticaster();

           this.onRefresh();

           this.registerListeners();

 

           // 빈 인스턴스화

           this.finishBeanFactoryInitialization(beanFactory);

 

           this.finishRefresh();

       } catch (Error | RuntimeException var12) {

           if (this.logger.isWarnEnabled()) {

               this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var12);

           }

 

           this.destroyBeans();

           this.cancelRefresh(var12);

           throw var12;

       } finally {

           contextRefresh.end();

       }

   } finally {

       this.startupShutdownThread = null;

       this.startupShutdownLock.unlock();

   }

 

}

 

위 노란색 주석으로 설명하는 내용이 이전 포스팅들에서 설명한 내용이다.

하나하나 알아보자.

 

BeanFactoryPostProcessor를 통한 BeanDefinition 등록


this.invokeBeanFactoryPostProcessors(beanFactory);

beanFactoryPostProcessor를 등록한다.
이 메서드를 추적하면 beanFactoryPostProcessor 구현체를 뽑아와 실행시키는 로직을 볼 수 있다.

private static void invokeBeanFactoryPostProcessors(Collection<? extends BeanFactoryPostProcessor> postProcessors, ConfigurableListableBeanFactory beanFactory) {

   Iterator var2 = postProcessors.iterator();

 

   while(var2.hasNext()) {

       BeanFactoryPostProcessor postProcessor = (BeanFactoryPostProcessor)var2.next();

       StartupStep var10000 = beanFactory.getApplicationStartup().start("spring.context.bean-factory.post-process");

       Objects.requireNonNull(postProcessor);

       StartupStep postProcessBeanFactory = var10000.tag("postProcessor", postProcessor::toString);

 

       // 빈 팩토리 후처리 실행(beanDefinition 등록)

       postProcessor.postProcessBeanFactory(beanFactory);

 

       postProcessBeanFactory.end();

   }

 

}

 

대게 postProcessBeanFactory를 통해 beanDefinition을 정의한다.

대표적인 예로 ConfigurationClassPostProcessor가 있는데 이 클래스가 @Component, @Configuration을 통해 정의한 클래스의 BeanDefinition을 등록한다.

잠깐, ConfigurationClassPostProcessor는 이 시점에 이미 존재하나?

디폴트 컨텍스트인 AnnotationConfigApplicationContext의 생성자 실행
-> AnnotatedBeanDefinitionReader 생성자 실행
-> AnnotationConfigUtils의 registerAnnotationConfigProcessors 메서드 실행

-> ConfigurationClassPostProcessor의 BeanDefinition 등록

if (!registry.containsBeanDefinition("org.springframework.context.annotation.internalConfigurationAnnotationProcessor")) {
   def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
   def.setSource(source);
   beanDefs.add(registerPostProcessor(registry, def, "org.springframework.context.annotation.internalConfigurationAnnotationProcessor"));
}

스프링에서는 위와 같이 생성자와 함께 실행되는 메서드들로 필요한 BeanDefinition을 정의한다.
소스코드를 분석하다보면 위와 같이 직접 등록하는 코드들이 많이 있다.

빈 생성을 위한 BeanDefinition 등록을 담당하는 BeanFactoryPostProcessor 등록 또한  new RootBeanDefinition(ConfigurationClassPostProcessor.class); 코드를 통해 의존관계를 설정한다.

 

빈 인스턴스화 


this.finishBeanFactoryInitialization(beanFactory);

이 메서드의 마지막 라인에 beanFactory.preInstantiateSingletons(); 이라는 메서드가 있다.
해당 메서드는 DefaultListableBeanFactory에 존재하는데 이 메서드를 보면 getBean(beanName) 메서드를 확인 할 수 있을 것이다.
우리는 이전에 getBean(beanName)을 통해 빈이 없으면 beanDefintion을 토대로 빈을 생성하여 가져오는 로직을 확인하였다. 따라서 이를 통해 빈이 최종적으로 싱글톤 레지스트리에 등록된다.

 

정리 및 후기


우리가 스프링을 사용할 땐, 의존관계를 코드를 통해 직접적으로 설정하는 경우는 거의 없을 것이다.

스프링 빈으로 등록되어있다면 스프링에서 자동 주입 시켜주므로 new 연산자를 통해 객체를 생성하고 주입하는 코드는 거의 사용할 일이 없다. 

이를 스프링에서 어떻게 가능하게 하는지 알아보기 위해 시리즈로 4편에 걸쳐 글을 포스팅했다.

 

글을 연재하며 사실 가장 궁금증이 생기는 부분은 컨텍스트를 구성하기 위한 의존관계는 어떻게 설정 되는지였다.
이번 글에서 밑줄 친 부분들은 컨텍스트의 구성요소들의 의존관계를 설정하는 부분들이다.

new 연산자나 하드코딩으로 이루어진 클래스 경로를 직접적으로 선언하여 의존관계를 설정하였다.
코드를 통해 직접적으로 의존관계를 결정
한다.

 

사실 나는 스프링을 배우며 코드가 아닌 어노테이션이나 xml과 같은 설정으로 의존관계를 결정하는게 스프링의 큰 장점이라고 생각하여 스프링의 컨텍스트 구성은 어떻게 되는지 궁금하였다.

디버깅을 통해 컨텍스트의 가장 초기에 생성되는 의존객체들을 보니 new 연산자와 하드 코딩과 같은 코드를 통한 직접적인 의존 관계 설정이었다.

의존관계는 직접 설정하였지만 내부로직들은 DIP가 잘 적용되어있고 구현체들을 맵 타입 객체(주로 postFix가 registry인 참조속성들)에 저장하여 사용하는 방식이 결합도를 낮추는데 많은 장점이 된 것 같다.

 

추후 이러한 환경을 직접 제공해야한다면 스프링의 이 방식들을 고려해봐야겠다.

반응형