[Spring MVC #3] Context Refresh 과정

mangoo·2023년 10월 11일
0
post-custom-banner

DemoApplication - main()

Spring Boot 프로젝트에서 애플리케이션을 실행시키려면 아래 함수를 실행시켜야 한다.

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

SpringApplication - run()

DemoApplication의 main() 메서드는 SpringApplication 클래스의 run() 메서드를 호출한다. 해당 메서드에서 주요한 부분은 3가지가 있는데 아래와 같다.

  • [1] ApplicationContext 생성
  • [2] Context 후처리 작업 및 리프레시를 위한 전처리 작업
  • [3] Context 리프레시
ConfigurableApplicationContext context = null;
private ConfigurableEnvironment environment;
private WebApplicationType webApplicationType;

public ConfigurableApplicationContext run(String... args) {
	
		// [1] ApplicationContext 생성
		context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);	
		
        // [2] Context 후처리 작업 및 리프레시를 위한 전처리 작업
		prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			
		// [3] Context 리프레시
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		  
		return context;
}

이번 포스팅에서는 [3] Context 리프레시에 대해 다뤄보려고 한다.


[3] Context 리프레시

refreshContext 메소드는 다음과 같이 구성되어 있는데, ShutdownHook을 등록해준 뒤 context 리프레시가 일어난다.

// SpringApplication.java
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)를 위한 작업을 처리할 수 있다.

// ServletWebServerApplicationContext.java
@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;
    }
}

// ReactiveWebServerApplicationContext.java
@Override
public final void refresh() throws BeansException, IllegalStateException {
	try {
		super.refresh();
	}
	catch (RuntimeException ex) {
		WebServerManager serverManager = this.serverManager;
		if (serverManager != null) {
			serverManager.getWebServer().stop();
		}
		throw ex;
	}
}

ServletWebServerApplicationContext, ReactiveWebServerApplicationContext 모두 빈을 찾고 객체로 만드는 등의 리프레시 과정이 동일하기 때문에 부모 클래스인 AbstractApplicationContext의 refresh 메서드 내부에 템플릿 메서드 패턴을 적용되었음을 볼 수 있다. 이를 통해 자식 클래스들이 일일히 구현할 필요 없이 일반적인 리프레시 로직은 부모 클래스에 만들어두고, 애플리케이션 타입별로 달라져야 하는 부분은 자식이 onRefresh 메서드를 오버라이딩해 로직을 구현하도록 한다.

애플리케이션 타입 별 onRefresh() 메서드

// ServletWebServerApplicationContext, ReactiveWebServerApplicationContext 동일
@Override
protected void onRefresh() {
	super.onRefresh();
	try {
		createWebServer();
	}
	catch (Throwable ex) {
		throw new ApplicationContextException("Unable to start web server", ex);
	}
} 
// ServletWebSErverApplicationContext.java
private void createWebServer() {
	WebServer webServer = this.webServer;
	ServletContext servletContext = getServletContext();
	if (webServer == null && servletContext == null) {
		StartupStep createWebServer = 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));
	}
	// ...
}
// ReactiveWebServerApplicationContext
private void createWebServer() {
	WebServerManager serverManager = this.serverManager;
	if (serverManager == null) {
		StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
		String webServerFactoryBeanName = getWebServerFactoryBeanName();
		ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
			createWebServer.tag("factory", webServerFactory.getClass().toString());
		boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
		this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
		getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.serverManager.getWebServer()));
		getBeanFactory().registerSingleton("webServerStartStop",new WebServerStartStopLifecycle(this.serverManager));
		createWebServer.end();
	}
	initPropertySources();
}

다시 원래의 코드로 돌아와서 AbstractApplicationContext의 refresh 메서드가 어떻게 구현되었는지 살펴보자.

@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 내부에서는 애플리케이션 컨텍스트 상태 중 하나인 active를 true로 변경하고 현재 시간을 기록하는 등의 준비 작업을 한다.

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

	// ...
}

active.set(true)가 중요한 이유?

빈 팩토리에서 빈을 꺼내는 작업은 active 상태가 true 일때만 가능하다. 대표적으로 getBean 메소드를 보면 active 상태가 아니면 throw하도록 되어있다.

// AbstractApplicationContext.java
@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");
        }
    }
}

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

3. BeanFactory의 후처리 진행

빈 팩토리의 준비가 끝났으면 빈 팩토리에 대한 후처리 작업이 이루어진다. 예를 들어 서블릿 기반의 웹 애플리케이션이라면 서블릿 관련 클래스들 역시 빈으로 등록되어야 하므로 빈 팩토리에 추가 작업이 필요하다. 그러나 postProcessBeanFactory 메서드를 보면 아래와 같이 비어 있음을 알 수 있다. 그 이유는 템플릿 메서드 패턴이 적용되어 각각의 애플리케이션 타입에 맞는 후처리를 진행하도록 하기 위함이다.

protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
}

AbstractApplicationContext의 구현체 중 하나인 AnnotationConfigServletWebServerApplicationContext는 아래와 같이 postProcessBeanFactory 메서드가 오버라이드하고 있다.

// AnnotationConfigServletWebServerApplicationContext.java
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	super.postProcessBeanFactory(beanFactory);
	if (this.basePackages != null && this.basePackages.length > 0) {
		this.scanner.scan(this.basePackages);
	}
	if (!this.annotatedClasses.isEmpty()) {
		this.reader.register(ClassUtils.toClassArray(this.annotatedClasses));
	}
}

4. BeanFactoryPostProcessor 실행

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

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

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
	PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

	// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
	// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
	if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
		beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
		beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
	}
}

Reference

post-custom-banner

0개의 댓글