servlet application context가 올라가는 과정

eora21·2023년 5월 1일
0

지난 시간에는 root application context가 어떤 과정을 거쳐 적재되는지 알아보았습니다.
이번에는 servlet application context가 올라가는 과정을 알아보도록 하겠습니다.

dispatcherServlet

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcherServlet-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

dispatcherServlet이라는 이름을 지닌 servlet을 org.springframework.web.servlet.DispatcherServlet 클래스를 참조하여 생성합니다.
또한 contextConfigLocation이라는 이름을 지닌 파라미터 변수를 하나 생성합니다.
해당 변수는 /WEB-INF/dispatcherServlet-servlet.xml을 String으로 들고 있습니다.
우선순위는 1로 설정합니다.

dispatcherServlet-servlet.xml

<context:component-scan base-package="com.nhnacademy.springmvc" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<mvc:annotation-driven />
<mvc:resources mapping="/resources/**" location="/resources/" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <constructor-arg name="prefix" value="/WEB-INF/views/" />
    <constructor-arg name="suffix" value=".jsp" />
</bean>

Controller 어노테이션에 대해 컴포넌트 스캔을 진행하도록 설정하였습니다.
또한 <mvc:annotation-driven />를 통해 Spring MVC 설정을 활성화하고 있습니다.
마지막으로, 컨트롤러가 뷰 이름을 반환할 경우를 대비하여 InternalResourceViewResolver를 통해 해당 뷰를 보여줄 수 있도록 하였습니다.

해당 설정은 바로 적용되지 않고, 추후 WAS에 의해 dispatcherServlet이 생성될 때 사용됩니다.

FrameworkServlet

dispatcherServletFrameworkServlet을 상속받아 생성됩니다.

Detects a "contextClass" parameter at the servlet init-param level, falling back to the default context class, XmlWebApplicationContext, if not found. Note that, with the default FrameworkServlet, a custom context class needs to implement the ConfigurableWebApplicationContext SPI.

FrameworkServletcontextClass라는 매개변수가 있다면 해당 컨텍스트 클래스를 내부적으로 설정합니다. 그러나 없다면, XmlWebApplicationContext로 설정합니다.
현재 매개변수 설정이 되어있지 않기 때문에 기본값인 XmlWebApplicationContext가 contextClass로 설정됩니다.

Passes a "contextConfigLocation" servlet init-param to the context instance, parsing it into potentially multiple file paths which can be separated by any number of commas and spaces, like "test-servlet.xml, myServlet.xml". If not explicitly specified, the context implementation is supposed to build a default location from the namespace of the servlet.

public void setContextConfigLocation(@Nullable String contextConfigLocation) {
    this.contextConfigLocation = contextConfigLocation;
}

contextConfigLocation에 xml에서 설정한 dispatcherServlet-servlet.xml을 등록합니다.

protected final void initServletBean() throws ServletException {
    
    ...
    
    try {
        this.webApplicationContext = this.initWebApplicationContext();
        this.initFrameworkServlet();
    } 
    
    ...
    
}

WebApplicationContext() 메서드를 동작시킵니다.

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
    WebApplicationContext wac = null;
    if (this.webApplicationContext != null) {
        ...  // webApplicationContext이 null이기에 통과
    }
    if (wac == null) {
        wac = this.findWebApplicationContext();  // null이 반환됩니다.
    }
    if (wac == null) {
        wac = this.createWebApplicationContext(rootContext);
    }
    if (!this.refreshEventReceived) {
        synchronized(this.onRefreshMonitor) {
            this.onRefresh(wac);
        }
    }
    if (this.publishContext) {
        String attrName = this.getServletContextAttributeName();
        this.getServletContext().setAttribute(attrName, wac);
    }
    return wac;
}

WebApplicationContextUtils.getWebApplicationContext(this.getServletContext())

해당 메서드에서는 FrameworkServlet의 상위 abstract class인 GenericServletgetServletContext() 메서드를 통해 ServletContext를 얻어와 전달합니다.

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
	return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

servletContext를 통해 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE를 가져와 반환합니다. 해당 값은 org.springframework.web.context.WebApplicationContext.ROOT로서, 이전에 살펴 본 root applicaion context인 것을 알 수 있습니다.

createWebApplicationContext(rootContext)

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = this.getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
    } else {
        ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
        wac.setEnvironment(this.getEnvironment());
        wac.setParent(parent);
        String configLocation = this.getContextConfigLocation();
        if (configLocation != null) {
            wac.setConfigLocation(configLocation);
        }
        this.configureAndRefreshWebApplicationContext(wac);
        return wac;
    }
}

getContextClass()에서는 이전에 설정되었던 XmlWebApplicationContext를 반환합니다.
XmlWebApplicationContextConfigurableWebApplicationContext를 구현했기에 if문이 통과되고, 인스턴스를 생성한 후 wac에 대입합니다.
StandardServletEnvironment()를 통해 기본 environment를 설정하고, root application context를 parent로 지정하며 parent의 environment와 merge합니다(같은 StandardEnvironment로 지정되어 있기에, 해당 시점에서 크게 변경되는 건 없을 것 같습니다).

그 후 지정한 /WEB-INF/dispatcherServlet-servlet.xml을 읽어들여 ConfigLocation으로 설정합니다.

configureAndRefreshWebApplicationContext

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        if (this.contextId != null) {
            wac.setId(this.contextId);
        } else {
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(this.getServletContext().getContextPath()) + '/' + this.getServletName());
        }
    }
    wac.setServletContext(this.getServletContext());
    wac.setServletConfig(this.getServletConfig());
    wac.setNamespace(this.getNamespace());
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment)env).initPropertySources(this.getServletContext(), this.getServletConfig());
    }
    this.postProcessWebApplicationContext(wac);
    this.applyInitializers(wac);
    wac.refresh();
}

FrameworkServlet의 id가 정해지지 않았으므로, else문의 코드가 돌게 되고 컨텍스트의 id가 org.springframework.web.context.WebApplicationContext:/dispatcherServlet이 됩니다.
servletContext와 servletConfig를 등록 후 컨텍스트가 새로고침될 때만 이벤트를 처리할 listener를 추가합니다.

Replace any stub property source instances acting as placeholders with real servlet context/config property sources using the given parameters.

stub property source를 servlet의 property source로 올리는 듯 합니다만, 정확한 사항은 모르겠습니다. 추후 추가하도록 하겠습니다. 아마 StandardServletEnvironment()를 통해 만든 envirenment를 서블렛에 적용시키는 게 아닐까 생각합니다.
그 후 refresh를 통해 빈을 생성하고 DI를 수행합니다.

if (this.publishContext) {
    String attrName = this.getServletContextAttributeName();
    this.getServletContext().setAttribute(attrName, wac);
}

마지막으로, dispatcherServlet의 컨텍스트명을 읽어와 servletConfig에 등록 후 서블릿을 실행시킵니다.

이로써 servlet application context이 생성되며 root application context를 부모 서블렛으로 설정하는 부분과 servlet context에 적재되는 과정에 대해 확인해 보았습니다.

profile
나누며 타오르는 프로그래머, 타프입니다.

0개의 댓글