티스토리 뷰

Spring

[Spring] SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(ApplicationContext)의 refreshContext 동작 과정 - (6)

망나니개발자 2022. 2. 24. 10:00
반응형

이번에는 SpringBoot의 실행 과정을 소스 코드로 직접 살펴보려고 합니다. 지난번에는 SpringBoot의 run 메소드를 자세히 살펴보았는데, refreshContext에 대해서는 자세히 알아보지 못했습니다. 이번에는 SpringBoot 애플리케이션 실행에 있어서 가장 중요한 refresh에 대해 알아보고자 합니다.

아래의 내용은 SpringBoot 2.6.3를 기준으로 작성되었습니다.

 

 

 

1. 애플리케이션 컨텍스트(ApplicationContext)의 refreshContext 동작 과정


[ refreshContext 동작 과정 ]

refreshContext 메소드는 다음과 같이 구성되어 있는데, 먼저 ShutdownHook을 등록해주는 것을 볼 수 있다.

private boolean registerShutdownHook = true;
    
private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        shutdownHook.registerApplicationContext(context);
    }
    refresh(context);
}

protected void refresh(ConfigurableApplicationContext applicationContext) {
    applicationContext.refresh();
}

 

 

ShutdownHook이란 프로그램의 종료를 감지하여 프로그램 종료 시에 후처리 작업을 진행하는 기술이다. 프로그램이 실행되다가 프로세스가 죽어버리면 연결 종료, 자원 반납 등의 처리를 못하게 되는데, ShutdownHook을 사용함으로써 프로그램이 종료되어도 별도의 쓰레드가 올바른 프로그램 종료(Graceful Shutdown)를 위한 작업을 처리할 수 있다. 

스프링 같은 경우에는 대표적으로 DisposableBean 인터페이스나 @PreDestroy 또는 @Bean에 소멸자 메소드를 지정해 소멸자를 구현할 수 있는데, SpringApplication이 여기서 ShutdownHook을 등록함으로써 애플리케이션이 갑자기 죽어도 별도의 쓰레드를 통해 정상적으로 소멸자 메소드를 처리할 수 있는 것이다. (소멸자 메소드에 대해 모르면 여기를 참고해주세요!)

만약 registerShutdownHook을 false로 설정하면 소멸자 메소드가 처리되지 않는다.

 

그리고 진짜 애플리케이션 컨텍스트를 refresh 해주고 있는데, 우리는 ServletWebServerApplicationContext를 기준으로 해당 메소드를 살펴보도록 하자. ServletWebServerApplicationContext에 구현된 refresh 메소드는 다음과 같다. 

@Override
public final void refresh() throws BeansException, IllegalStateException {
    try {
        super.refresh();
    }
    catch (RuntimeException ex) {
        WebServer webServer = this.webServer;
        if (webServer != null) {
            webServer.stop();
        }
        throw ex;
    }
}

 

 

부모 클래스(AbstractApplicationContext)의 refresh 메소드 내부에는 템플릿 메소드 패턴이 적용되어 있는데, 위의 코드를 보면 왜 refresh 내부에서 템플릿 메소드 패턴을 적용했는지 짐작할 수 있다.

애플리케이션 컨텍스트는 애플리케이션 타입에 따라 서로 다른 웹 서버를 생성해야 함을 앞선 포스팅에서 살펴보았다.

대표적으로 ServletWebServerApplicationContext는 내장 톰캣을 만들어주어야 한다. 문제는 빈을 찾고 객체로 만드는 등의 전반적인 refresh 로직은 동일하다는 것인데, 이를 모든 자식클래스에 구현하면 중복이 발생한다. 그래서 스프링은 전반적인 refresh 로직은 부모 클래스(AbstractApplicationContext)에 만들어두고, 애플리케이션 타입 별로 달라져야하는 부분은 자식이 onRefresh 메소드를 오버라이딩하고 로직을 구현하도록 문제를 해결하고 있다. 매우 훌륭하게 템플릿 메소드를 적용해 문제를 해결한 것이다.

위와 관련되어서는 아래에서 자세히 다시 살펴보도록 하고 refresh 코드를 보도록 하자.

 

 

 

[ refresh 동작 과정 ]

refresh는 start-up 메소드로써 싱글톤 빈으로 등록할 클래스들을 찾아서 생성하고 후처리하는 단계이다. 여기서 후처리는 @Value, @PostConstruct, @Autowired 등을 처리하는 것이다.

실제 refresh 로직은 부모 클래스인 AbstractApplicationContext에 구현되어 있으며 다음과 같다.

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

        // 1. refresh 준비 단계
        prepareRefresh();

        // 2. BeanFactory 준비 단계
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        prepareBeanFactory(beanFactory);

        try {
            // 3. BeanFactory의 후처리 진행
            postProcessBeanFactory(beanFactory);

            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
            
            // 4. BeanFactoryPostProcessor 실행
            invokeBeanFactoryPostProcessors(beanFactory);

            // 5. BeanPostProcessor 등록
            registerBeanPostProcessors(beanFactory);
            beanPostProcess.end();

            // 6. MessageSource 및 Event Multicaster 초기화
            initMessageSource();
            initApplicationEventMulticaster();

            // 7. onRefresh(웹 서버 생성)
            onRefresh();

            // 8. ApplicationListener 조회 및 등록
            registerListeners();

            // 9. 빈들의 인스턴스화 및 후처리
            finishBeanFactoryInitialization(beanFactory);

            // 10. refresh 마무리 단계
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                    "cancelling refresh attempt: " + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
            contextRefresh.end();
        }
    }
}

 

 

refresh 메소드 호출을 통해 모든 객체들이 싱글톤으로 인스턴스화되는데, 만약 에러가 발생하면 등록된 빈들을 모두 제거한다. 즉, refresh 단계를 거치면 모든 빈이 인스턴스화 되었거나 어떠한 빈도 존재하지 않거나 둘 중 하나의 상태가 된다. 이러한 refresh 단계는 크게 다음과 같은 순서로 나눌 수 있다.

  1. refresh 준비 단계
  2. BeanFactory 준비 단계
  3. BeanFactory의 후처리 진행
  4. BeanFactoryPostProcessor 실행
  5. BeanPostProcessor 등록
  6. MessageSource 및 Event Multicaster 초기화
  7. onRefresh(웹 서버 생성)
  8. ApplicationListener 조회 및 등록
  9. 빈들의 인스턴스화 및 후처리
  10. refresh 마무리 단계

 

 

 

 

1. refresh 준비 단계

가장 먼저 진행되는 단계는 prepareRefresh이다. prepareRefresh 내부에서는 애플리케이션 컨텍스트의 상태를 active로 변경하는 등의 준비 작업을 하고 있다.

protected void prepareRefresh() {
    // Switch to active.
    this.startupDate = System.currentTimeMillis();
    this.closed.set(false);
    this.active.set(true);

    ...

    // Initialize any placeholder property sources in the context environment.
    initPropertySources();

    ...
}

 

 

여기서 애플리케이션 컨텍스트의 상태 중 active를 true로 바꾸는 것은 중요하다. 왜냐하면 빈 팩토리에서 빈을 꺼내는 작업은 active 상태가 true 일때만 가능하기 때문이다. 대표적으로 getBean 메소드를 보면 active 상태가 아니면 throw하도록 되어있다.

@Override
public <T> T getBean(Class<T> requiredType) throws BeansException {
    assertBeanFactoryActive();
    return getBeanFactory().getBean(requiredType);
}

protected void assertBeanFactoryActive() {
    if (!this.active.get()) {
        if (this.closed.get()) {
            throw new IllegalStateException(getDisplayName() + " has been closed already");
        }
        else {
            throw new IllegalStateException(getDisplayName() + " has not been refreshed yet");
        }
    }
}

 

 

물론 아직 빈이 인스턴스화되지 않았기 때문에 빈은 존재하지 않지만 그래도 빈을 찾는 행위 자체는 가능해진 것이다.

이후에 propertySource와 pre-refresh 애플리케이션 리스너들, ApplicationEvents를 초기화하는데, 너무 디테일하므로 넘어가도록 하자.

 

 

 

 

2. BeanFactory 준비 단계

앞선 포스팅에서 실제 스프링의 빈들은 beanFactory에서 관리되며, 애플리케이션 컨텍스트는 빈 관련 요청이 오면 이를 beanFactory로 위임한다고 설명하였다. prepareBeanFactory 메소드에서는 beanFactory가 동작하기 위한 준비 작업들이 진행된다.

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    // Tell the internal bean factory to use the context's class loader etc.
    beanFactory.setBeanClassLoader(getClassLoader());
    if (!shouldIgnoreSpel) {
        beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
    }
    beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

    // Configure the bean factory with context callbacks.
    beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
    beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
    beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
    beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
    beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
    beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
    beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
    beanFactory.ignoreDependencyInterface(ApplicationStartupAware.class);

    // BeanFactory interface not registered as resolvable type in a plain factory.
    // MessageSource registered (and found for autowiring) as a bean.
    beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
    beanFactory.registerResolvableDependency(ResourceLoader.class, this);
    beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
    beanFactory.registerResolvableDependency(ApplicationContext.class, this);

    // Register early post-processor for detecting inner beans as ApplicationListeners.
    beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

    // ... Detect a LoadTimeWeaver and prepare for weaving, if found.

    // ... Register default environment beans.
}

 

 

가장 먼저 빈 클래스들을 불러오기 위한 클래스 로더를 셋해주고, 프로퍼티를 처리하는 spEL Resolver와 PropertyEditory를 빈으로 추가한다. 그리고 의존성 주입을 무시할 인터페이스들(Aware 인터페이스들)을 등록하고, BeanFactory와 ApplicationContext 같은 특별한 타입들을 빈으로 등록하기 위한 작업을 진행한 후에 환경변수들을 빈으로 등록하며 마무리한다.

 

Aware 인터페이스는 스프링 프레임워크의 내부 동작에 접근하기 위한 인터페이스이다. Aware 인터페이스에는 접근할 대상을 주입해주는 수정자 메소드(setter)가 존재한다. 예를 들어 ApplicationContextAware에는 setApplicationContext메소드가 있어서 ApplicationContext를 주입할 수 있다. 이러한 Aware 인터페이스는 Spring2.0에 추가되었는데, 과거에는 ApplicationContext나 BeanFactory 등의 구현체를 주입받을 수 없었다. 그래서 해당 구현체들에 접근하려면 Aware 인터페이스를 만들어 주입해주어야 했다. 하지만 지금은 바로 주입이 가능하므로 주로 사용되지는 않지만, 객체가 생성된 이후에 주입해주어야 할 때 사용할 수 있다.

(그런데 위의 코드에서는 Aware 인터페이스를 의존성 주입 대상에서 Ignore 해주지만 확인해보면 Aware 인터페이스들이 빈으로 등록되어 있습니다. 이와 관련해서는 추가적으로 확인이 필요할 것 같은데, 혹시 아시는 분 계시면 댓글 남겨주세요!)

 

 

 

 

3. BeanFactory의 후처리 진행

빈 팩토리의 준비가 끝났으면 빈 팩토리를 후처리한다. 예를 들어 서블릿 기반의 웹 애플리케이션이라면 서블릿 관련 클래스들 역시 빈으로 등록되어야 하므로 빈 팩토리에 추가 작업이 필요하다. 이러한 이유로 스프링은 이 과정에 다시 한번 템플릿 메소드 패턴을 적용해서 각각의 애플리케이션 타입에 맞는 후처리를 진행하도록 하고 있다.

빈 팩토리의 후처리가 마무리되면 이제 빈 팩토리가 동작할 준비가 끝난 것이다.

 

 

 

4. BeanFactoryPostProcessor 실행

BeanFactoryPostProcessor를 실행하는 것은 BeanFactory의 후처리와 다르다. BeanFactory의 후처리는 BeanFactory에 대한 추가 작업을 하는 것이고, BeanFactoryPostProcessor는 빈을 탐색하는 것처럼 빈 팩토리가 준비된 후에 해야하는 후처리기들을 실행하는 것이다.

대표적으로 싱글톤 객체로 인스턴스화할 빈을 탐색하는 작업을 진행된다. 스프링은 인스턴스화를 진행할 빈의 목록(BeanDefinition)을 로딩하는 작업과 실제 인스턴스화를 하는 작업을 나눠서 처리하는데, 인스턴스로 만들 빈의 목록을 찾는 단계가 여기에 속한다.

빈 목록은 @Configuration 클래스를 파싱해서 가져오는데, BeanFactoryPostProcessor의 구현체 중 하나인 ConfigurationClassPostProcessor가 진행한다. ConfigurationAnnotationProcessor의 processConfigBeanDefinitions를 통해 파싱이 진행된다.

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    String[] candidateNames = registry.getBeanDefinitionNames();

    // 1. 파싱을 진행할 설정 빈 이름을 찾음(메인 클래스만 남게됨)
    for (String beanName : candidateNames) {
        BeanDefinition beanDef = registry.getBeanDefinition(beanName);
        if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
            }
        }
        else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
            configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
        }
    }

    // 2. 파싱할 클래스가 없으면 종료함
    if (configCandidates.isEmpty()) {
        return;
    }

    // 3. @Order를 참조해 파싱할 클래스들을 정렬함
    configCandidates.sort((bd1, bd2) -> {
        int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
        int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
        return Integer.compare(i1, i2);
    });

    // 4. 빈 이름 생성 전략을 찾아서 처리함
    ...

    // 5. 파서를 생성하고 모든 @Configuration 클래스를 파싱함
    ConfigurationClassParser parser = new ConfigurationClassParser(
        this.metadataReaderFactory, this.problemReporter, this.environment,
        this.resourceLoader, this.componentScanBeanNameGenerator, registry);

    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
    Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
    
    do {
        StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
        parser.parse(candidates);
        parser.validate();

        Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
        configClasses.removeAll(alreadyParsed);

        // Read the model and create bean definitions based on its content
        if (this.reader == null) {
            this.reader = new ConfigurationClassBeanDefinitionReader(
                registry, this.sourceExtractor, this.resourceLoader, this.environment,
                this.importBeanNameGenerator, parser.getImportRegistry());
        }
        this.reader.loadBeanDefinitions(configClasses);
        alreadyParsed.addAll(configClasses);
        processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();

        candidates.clear();
        ...
    }
    while (!candidates.isEmpty());

    // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
    ...
}

 

 

위의 코드는 클래스를 파싱해서 우리가 선언한 빈들을 찾는 로직인데, 코드가 워낙 방대하므로 큰 그림에서 흐름만 따라가도록 하자.

빈 팩토리의 beanDefinitionNames는 저장된 빈 이름 목록을 조회하는데, 아직 우리가 추가한 빈들이 파싱되지 않았으므로 현재 애플리케이션 메인 클래스와 스프링에서 정의한 클래스들만이 존재한다. 그리고 위의 필터링 과정을 거쳐 configCandidates에는 메인 애플리케이션 클래스만이 남게 된다. 메인 애플리케이션이 남는 이유는 앞선 포스팅에서 살펴보았다. 메인 클래스의 @SpringBootApplication이 갖고 있는 @SpringBootApplication가 @SpringBootConfiguration(@Configuration)를 가지고 있기 때문이다.

그리고 클래스를 파싱하는 ConfigurationClassParser의 parse()를 통해 메인 클래스 하위의 빈들을 찾아 모두 빈 정보 객체(BeanDefinition) 로 만들어 등록한다. parse가 진행되고 나면 이제 beanDefinitionNames로 우리가 만든 빈 정보도 불러와진다. 물론 아직 객체로 인스턴스화하지는 않았으므로 beanFactory에 빈이 존재하지는 않는다.

빈 목록을 불러오는 작업은 BeanFactoryPostProcessor 구현체 중 하나인 ConfigurationAnnotationProcessor가 담당할 뿐이고, 그 외에 등록된 다른 BeanFactoryPostProcessor 모두 객체로 만들고 실행시킨다. 대표적인 BeanFactoryPostProcessor 구현체로는 @Value를 처리하는 PropertyPlaceholderConfigurer와 PropertyEditor를 등록하기 위한 CustomEditorConfigurer가 있다.

즉, 이 단계는 BeanFactory가 준비되고 빈을 인스턴스화하기 전의 중간 단계로써 빈의 목록을 불러오고, 불러온 빈의 메타 정보를 조작하기 위한 BeanFactoryPostProcessor를 객체로 만들어 실행시키는 것이다.

 

 

 

5. BeanPostProcessor 등록

그 다음에는 빈들이 생성되고 나서 빈의 내용이나 빈 자체를 변경하기 위한 빈 후처리기인 BeanPostProcessor를 등록해주고 있다. 대표적으로 @Value, @PostConstruct, @Autowired 등이 BeanPostProcessor에 의해 처리되며 이를 위한 BeanPostProcessor 구현체들이 등록된다. 대표적으로 CommonAnnotationBeanPostProcessor는 @PostConstruct와 @PreDestroy를 처리하기 위해 등록된다. 그러므로 의존성 주입은 빈 후처리기에 의해 처리되는 것이며, 실제 빈 대신에 프록시 빈으로 교체되는 작업 역시 빈 후처리기에 의해 처리된다. BeanPostProcessor는 빈이 생성된 후 초기화 메소드가 호출되기 직전과 호출된 직후에 처리가능한 2개의 메소드를 제공하고 있다.

 

 

 

6. MessageSource 및 Event Multicaster 초기화

BeanPostProcessor를 등록한 후에는 다국어 처리를 위한 MessageSource와 ApplicationListener에 event를 publish하기 위한 Event Multicaster를 초기화하고 있다.

 

 

 

7. onRefresh(웹 서버 생성 및 실행)

SpringBoot는 기존의 Spring 프레임워크와 달리 내장 웹서버를 탑재하고 있으며, 애플리케이션이 시작될 때 웹서버 객체를 만들어 실행해야 한다. 문제는 애플리케이션 타입에 따라 웹 서버 생성 로직이 달라져야 한다는 것이다. 웹 애플리케이션이 아닌 경우에는 웹 서버가 없을 것이고, 서블릿 웹인 경우에는 톰캣으로, 리액티브 웹인 경우에는 리액터 네티로 만들어야 한다.

즉, 애플리케이션 컨텍스트에 따라 다른 웹서버 로직이 호출되어야 하는 것인데, 스프링은 템플릿 메소드 패턴을 적용해서 문제를 해결하고 있다. 모든 애플리케이션 컨텍스트에 동일한 로직들(빈을 찾고 객체로 만들어 후처리하는 것)은 큰 틀에서 refresh라는 메소드로 부모 클래스에 작성해두고, 달라져야 하는 부분들(서로 다른 웹 서버의 생성)은 onRefresh 메소드를 오버라이딩 하도록 하는 것이다.

지금 우리가 살펴보고 있는 refresh 코드는 AbstractApplicationContext에 작성되어 있으며, refresh 내부에서 호출하는 onRefresh의 세부 구현은 각 애플리케이션 컨텍스트에 위임하고 있으니 템플릿 메소드 패턴이 적용되어 있음을 이해할 수 있다.

 

 

 

그러므로 웹 서버를 생성하는 로직을 보기 위해서는 애플리케이션 컨텍스트 구현체로 가야 하는데, 이번에는 서블릿을 기반으로 하는 ServletWebServerApplicationContext를 살펴보도록 하자.

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

 

 

서블릿 기반에서는 톰캣 웹 서버를 만들어주어야 한다. 그러므로 부모의 onRefresh를 먼저 호출한 후에 웹서버를 생성하고 있음을 확인할 수 있다. createWebServer는 다음과 같이 구현되어 있다.

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
        ServletWebServerFactory factory = getWebServerFactory();
        createWebServer.tag("factory", factory.getClass().toString());
        this.webServer = factory.getWebServer(getSelfInitializer());
        createWebServer.end();
        getBeanFactory().registerSingleton("webServerGracefulShutdown",
            new WebServerGracefulShutdownLifecycle(this.webServer));
        getBeanFactory().registerSingleton("webServerStartStop",
            new WebServerStartStopLifecycle(this, this.webServer));
    }
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}

 

 

웹서버 생성은 역시 일관되게 팩토리 객체에게 위임하는데, getWebServer 메소드를 호출하면 내부에서 다음과 같이 톰캣 객체를 생성하여 반환해주고 있다.

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    Tomcat tomcat = new Tomcat();
    
    ...
    
    return getTomcatWebServer(tomcat);
}

 

 

 

8. ApplicationListener 조회 및 등록

그 다음에는 ApplicationListener의 구현체들을 찾아서 EventMultiCaster에 등록해주고 있다.

 

 

 

 

9. 빈들의 인스턴스화 및 후처리

이제 등록된 빈 정의(BeanDefinition)를 바탕으로 객체를 생성할 차례이다. BeanDefinition에는 빈 이름, 스코프 등과 같은 정보가 있어서 이를 바탕으로 객체를 생성하게 된다. 우선 전반적인 코드를 살펴보도록 하자.

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    ...

    // Allow for caching all bean definition metadata, not expecting further changes.
    beanFactory.freezeConfiguration();

    // Instantiate all remaining (non-lazy-init) singletons.
    beanFactory.preInstantiateSingletons();
}

 

 

객체를 생성하는 부분은 가장 마지막 줄의 preInstantiateSingletons인데, 그 전에 더 이상 남은 빈 팩토리 작업과 누락된 빈 정보가 없으므로 빈 팩토리의 설정과 BeanDefinitionNames를 freeze 해주고 있다. 그리고 객체를 생성하는데, BeanFactory의 구현체인 DefaultListableBeanFactory를 통해 해당 로직을 살펴보도록 하자.

@Override
public void preInstantiateSingletons() throws BeansException {
    if (logger.isTraceEnabled()) {
        logger.trace("Pre-instantiating singletons in " + this);
    }

    // Iterate over a copy to allow for init methods which in turn register new bean definitions.
    // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
    List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

    // Trigger initialization of all non-lazy singleton beans...
    for (String beanName : beanNames) {
        RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
        if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
            if (isFactoryBean(beanName)) {
                ...
            }
            else {
                getBean(beanName);
            }
        }
    }

    // Trigger post-initialization callback for all applicable beans...
    for (String beanName : beanNames) {
        ... 
    }
}

 

 

실제 코드를 보면 굉장히 복잡한데, 불필요한 부분은 자르고 큰 그림에서만 살펴보도록 하자.

우리가 집중해야 하는 부분은 getBean(beanName)이다. DefaultListableBeanFactory의 getBean 내부에서는 요청을 AbstractBeanFactory의 doGetBean으로 위임하고 있다.

protected <T> T doGetBean(
    String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
        throws BeansException {

    String beanName = transformedBeanName(name);
    Object beanInstance;

    // 빈이 이미 등록되었거나 캐싱된 빈이 존재하는지 검사
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        ... 
        // 빈이 이미 등록되었거나 캐싱된 빈이 존재하는 경우 생성X
        beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }

    else {
			
        ...
           
        try {

            ... 
            // BeanDefinition에서 scope 정보를 참조해 빈을 생성함
            if (mbd.isSingleton()) {
                sharedInstance = getSingleton(beanName, () -> {
                    try {
                        return createBean(beanName, mbd, args);
                    }
                    catch (BeansException ex) {
                        // Explicitly remove instance from singleton cache: It might have been put there
                        // eagerly by the creation process, to allow for circular reference resolution.
                        // Also remove any beans that received a temporary reference to the bean.
                        destroySingleton(beanName);
                        throw ex;
                    }
                });
                beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }

            else if (mbd.isPrototype()) {
                ... // 프로토타입인 경우에 다른 객체 생성 로직 존재
            }
            else {
                ...
            }
        }
        catch (BeansException ex) {
            beanCreation.tag("exception", ex.getClass().toString());
            beanCreation.tag("message", String.valueOf(ex.getMessage()));
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
        finally {
            beanCreation.end();
        }
    }

    return adaptBeanInstance(name, beanInstance, requiredType);
}

 

 

 doGetBean 내부에서는 해당 빈 이름으로 만들어진 빈이 존재하는지를 검사하여 있으면 꺼내서 반환하고 없으면 생성해서 반환해주고 있다. 현재 우리가 보고 있는 클래스는 AbstractBeanFactory인데, 위의 코드에서 싱글톤 객체를 만드는 createBean 부분은 추상 메소드이며 세부 구현은 AbstractAutowireCapableBeanFactory를 참조해야 한다. 다시 한번 더 템플릿 메소드 패턴이 적용된 것이다.

최종적으로 BeanUtils의 instantiateClass에서 객체를 만들어주는데, 역시 예상한대로 내부적으로 자바의 리플렉션 패키지의 생성자 클래스인(Constructor)를 통해 인스턴스화함을 파악할 수 있다.

public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
    Assert.notNull(ctor, "Constructor must not be null");
    try {
        ReflectionUtils.makeAccessible(ctor);
        if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
            return KotlinDelegate.instantiateClass(ctor, args);
        }
        else {
            Class<?>[] parameterTypes = ctor.getParameterTypes();
            Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
            Object[] argsWithDefaultValues = new Object[args.length];
            for (int i = 0 ; i < args.length; i++) {
                if (args[i] == null) {
                    Class<?> parameterType = parameterTypes[i];
                    argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null);
                }
                else {
                    argsWithDefaultValues[i] = args[i];
                }
            }
            return ctor.newInstance(argsWithDefaultValues);
        }
    }
    catch (InstantiationException ex) {
        throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
    }
    catch (IllegalAccessException ex) {
        throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex);
    }
    catch (IllegalArgumentException ex) {
        throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex);
    }
    catch (InvocationTargetException ex) {
        throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException());
    }
}

 

 

생성자로 객체를 만들었으면  @Value, @PostConstruct, @Autowired에 의한 값은 모두 Null일 것이다. 그러므로 만들어진 객체를 대상으로 BeanPostProcessor를 적용해  @Value, @PostConstruct, @Autowired 등을 처리해주고 객체의 생성을 마무리한다. 일반적으로 PersistenceAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, AutowiredAnnotationBeanPostProcessor, ApplicationListenerDetector가 등록되어 있다.

더 자세히 보면 복잡해지므로 이정도로만 알아보고 넘어가도록 하자.

 

 

 

 

10. refresh 마무리 단계

모든 빈들을 인스턴스화하였다면 이제 refresh를 마무리하는 단계이다. 여기서는 애플리케이션 컨텍스트를 준비하기 위해 사용되었던 resourceCache를 제거하고, Lifecycle Processor를 초기화하여 refresh를 전파하고, 최종 이벤트를 전파하며 마무리된다.

Lifecycle Processor에는 웹서버와 관련된 부분이 있어서 refresh가 전파되면, 웹서버가 실행된다.

 

 

 

 

 

 

 refreshContext가 실패한 경우 마무리 단계

위의 refreshContext이 진행되다가 실패할 수 있다. 예를 들어 톰캣 포트가 이미 사용중일 때 에러가 발생할 수 있다.

그러면 스프링은 catch 구문에서 모든 빈들을 제거하고 active를 false로 변경하고 애플리케이션을 종료한다.

 

 

 

 

 

 

빈들을 찾아서 객체로 만들고 의존성 주입을 해주는 refresh는 애플리케이션 컨텍스트에 있어서 가장 핵심이 되는 단계이다. 그만큼 많이 복잡하지만 큰 그림에서 살펴보았다. 그리고 이러한 과정을 거쳐서 애플리케이션 컨텍스트가 만들어지면 이제 디스패처 서블릿이 요청을 받아 처리하게 된다. 다음에는 디스패처 서블릿이 어떻게 동작되는지 살펴보도록 하자.

 

 

 

 

위의 내용들은 개인적으로 공부를 하면서 작성을 한 내용이라 충분히 틀리거나 잘못된 내용들이 있을 수 있습니다. 혹시 수정 또는 추가할 내용들을 발견하셨다면 댓글 남겨주세요! 반영해서 수정하도록 하겠습니다:)

 

 

 

 

 

 

 

관련 포스팅

  1. SpringBoot 소스 코드 분석하기, SpringBoot의 장점과 특징 - (1)
  2. SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(Application Context)와 빈팩토리(BeanFactory) - (2)
  3. SpringBoot 소스 코드 분석하기, @SpringBootApplication 어노테이션의 속성과 구성요소 - (3)
  4. SpringBoot 소스 코드 분석하기, SpringBootApplication의 생성과 초기화 과정  - (4)
  5. SpringBoot 소스 코드 분석하기, SpringBootApplication의 run 메소드와 실행과정  - (5)
  6. SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(ApplicationContext)의 refreshContext 동작 과정 - (6)
  7. SpringBoot 소스 코드 분석하기, DispatcherServlet(디스패처 서블릿) 동작 과정 - (7)

 

 

반응형
댓글
댓글쓰기 폼
반응형
공지사항
Total
3,301,914
Today
5
Yesterday
5,301
링크
TAG
more
«   2022/12   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함