Spring-MVC 읽기 #4. AbstractContextLoaderInitializer

2020. 9. 4. 16:49 Spring Framework/Spring boot

이전 글에서 WebApplicationInitializer 구조에 대해 봤다. 이번 글은 WebApplicationInitializer 구현체 중 하나인 AbstractContextLoaderInitializer 클래스에 대해 이야기해볼 예정이다.

코드를 보자.

AbstractContextLoaderInitializer

public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

    /** Logger available to subclasses. */
    protected final Log logger = LogFactory.getLog(getClass());

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        registerContextLoaderListener(servletContext);
    }

    protected void registerContextLoaderListener(ServletContext servletContext) {
        WebApplicationContext rootAppContext = createRootApplicationContext();
        if (rootAppContext != null) {
            ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
            listener.setContextInitializers(getRootApplicationContextInitializers());
            servletContext.addListener(listener);
        }
        else {
            logger.debug("No ContextLoaderListener registered, as " +
                    "createRootApplicationContext() did not return an application context");
        }
    }
    
    @Nullable
    protected abstract WebApplicationContext createRootApplicationContext();

    @Nullable
    protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
        return null;
    }

}

ServletContext에 ContextLoaderListener를 등록한다

라고 주석에 쓰여 있다. 그리고 이 클래스는 Spring 버전 3.2에 만들어졌고, 클래스 작성을 Arjen Poutsma, Chris Beams, Juergen Hoeller 라는 개발자들이 작성했다. 오픈소스를 보며 유명 개발자가 작성한 코드를 보는 것 또한 하나의 재미다.

  1. AbstractContextLoaderInitializer#registerContextLoaderListener 메소드는 ContextLoaderListener를 생성한다.
  2. ContextLoaderListener에 ApplicationContextInitializer<?>[]를 주입(ContextLoaderListener#setContextInitializers 메소드)한다.
    • AbstractContextLoaderInitializer#getRootApplicationContextInitializers 메소드는 null을 반환한다.
  3. ServletContext에 ContextLoaderListener를 추가(ServletContext#addListener 메소드)한다.

ContextLoaderListener


ContextLoaderListener 클래스는 ContextLoader를 상속(extends)하고, ServletContextListener를 구현(implements)하고 있다.

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    /**
     * Initialize the root web application context.
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

    /**
     * Close the root web application context.
     */
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }

}
  1. ContextLoaderListener#contextInitialized 메소드는 ServletContextListener 인터페이스의 구현 메소드다.
  2. ContextLoaderListener#contextInitialized 메소드의 이름과 주석을 보니 root Web ApplicationContext 초기화 하는 메소드이다.
  3. ContextLoaderListener#contextInitialized 메소드는 ContextLoader#initWebApplicationContext 메소드를 호출한다.
  4. ContextLoaderListener#contextDestroyed 메소드는 ServletContextListener 인터페이스의 구현 메소드다.
  5. ContextLoaderListener#contextDestroyed 메소드의 이름과 주석을 보니 root Web ApplicationContext를 종료하는 메소드이다.

아직 자세한 코드를 보지 못해 추정만 할 뿐이다. 두 개의 메소드 중 contextInitialized 메소드만 한번 살펴보자.

ContextLoader#initWebApplicationContext

ContextLoader#initWebApplicationContext 메소드는 ContextLoaderListener 클래스의 부모 클래스인 ContextLoader가 포함하고 있다. 중요한 행동이라고 생각되는 부분을 제외한 나머지 코드는 중략한다.

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // ... 중략 ...

    try {
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        
        // ... 중략 ...

        return this.context;
    }
    catch (RuntimeException | Error ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
}  
  1. context(WebApplicationContext)가 null이면, WebApplicationContext를 생성(ContextLoader#createWebApplicationContext 메소드) 한다.
    • ContextLoader는 AbstractContextLoaderInitializer에서 WebApplicationContext를 생성자로 주입했기 때문에 null이 아니다.
    • AbstractContextLoaderInitializer#registerContextLoaderListener 메소드를 다시 보자.
  2. 부모 WebApplicationContext(cwac.getParent 메소드)가 null이면, 부모 WebApplicationContext을 load(loadParentContext 메소드) 후 주입(cwac.setParent 메소드) 한다.
  3. WebApplicationContext에 설정(configure)과 초기화(refresh)를 한다. (configureAndRefreshWebApplicationContext메소드)

ContextLoader#configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    // ... 중략 ...

    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    customizeContext(sc, wac);
    wac.refresh();
}
  1. Environment를 WebApplicationContext에 초기화(((ConfigurableWebEnvironment) env).initPropertySources(sc, null)) 한다.
  2. WebApplicationContext#refresh 메소드를 호출한다.

WebApplicationContext가 자주 등장한다. 이전 글에서도 말했다시피 ApplicationContext 설명은 이 글을 참고하자.

그런데 ServletContextListener#contextInitialized 메소드는 어디서 실행될까?

ContextLoaderListener가 결국 WebApplicationContext#refresh를 하는 건 알았다. 그런데 초기화하기 위한 ServletContextListener#contextInitialized 메소드는 어디서 누가 실행하는 걸까? 결론부터 말하자면, WAS(Web Application Server)가 실행한다. 나는 Tomcat을 기준으로 코드를 봤는데, Tomcat 코드까지 코드를 분석하는 건 너무 시간이 오래 걸릴 거 같고 아주 좋은 분석 글이 있어 자세한 설명은 Tomcat & Spring Bootstrapping Sequence — 2편 Spring 글로 대신하고, 나는 이 글을 토대로 대략적인 설명만 해야겠다.

  1. ContextLoaderListener#registerContextLoaderListener 메소드를 다시 보면 servletContext.addListener(listener); 이런 코드가 있다. 이 코드는 ServletContext에 ContextLoaderListener 클래스를 추가(add) 하는 메소드다.
  2. ServletContext는 인터페이스다.
  3. Tomcat에 ServletContext 인터페이스의 구현체인 ApplicationContext라는 클래스가 있다.
    • Spring의 ApplicationContext가 아니다.
    • Tomcat의 ApplicationContext다.
  4. ApplicationContext#addListener 메소드를 보면 ServletContextListener 객체를 StandardContext 클래스의 addApplicationLifecycleListener 메소드로 추가한다. 
    • ContextLoaderListener는 ServletContextListener 구현체다.
    • 그러므로 StandardContext#addApplicationLifecycleListener 메소드로 ContextLoaderListener 추가가 가능하다.
  5. Tomcat & Spring Bootstrapping Sequence — 2편 Spring 글에 따르면, Tomcat의 라이프사이클에 의해 StandardContext#startInternal 메소드가 호출된다고 한다.
    • StandardContext#startInternal 메소드는 Spring-MVC 읽기 #3. Spring-MVC의 시작 글에서 봤던 onStartup메소드를 호출한다.
    • StandardContext#startInternal 메소드는 StandardContext#listenerStart 메소드를 호출한다.
    • StandardContext#listenerStart 메소드는 ServletContextListener#contextInitialized 메소드를 호출한다. 
    • ServletContextListener#contextInitialized 메소드는 root Web ApplicationContext를 초기화(refresh) 한다.

마무리


마지막 Tomcat에서 ServletContextListener#contextInitialized 메소드를 호출하는 부분을 자세히 설명하지 않아 이해하기 조금 어려울 수 있지만, 이해가 안 되면 Tomcat & Spring Bootstrapping Sequence — 2편 Spring을 읽고 다시 읽어보면 좀 더 이해가 잘될 거라 생각한다. 그래도 이해 안 가면 댓글 ㄱ

출처 : https://blog.woniper.net/367?category=699184