본문 바로가기

서버운영 (TA, ADMIN)/미들웨어

[Servlet] 서블릿 컨테이너와 스프링 컨테이너

https://minwan1.github.io/2017/10/08/2017-10-08-Spring-Container,Servlet-Container/

https://minwan1.github.io/2018/11/21/2018-11-21-jsp-springboot-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95/

 

서블릿 컨테이너는 개발자가 웹서버와 통신하기 위하여 소켓을 생성하고, 특정 포트에 리스닝하고, 스트림을 생성하는 등의 복잡한 일들을 할 필요가 없게 해준다. 컨테이너는 servlet의 생성부터 소멸까지의 일련의 과정(Life Cycle)을 관리한다. 서블릿 컨테이너는 요청이 들어올때마다 새로운 자바 스레드를 만든다. 우리가 알고 잇는 대표적인 Servlet Container가 Tomcat이다. 톰캣같은 was가 java파일을 컴파일해서 Class로 만들고 메모리에 올려 servlet 객체를 만든다.


서블릿 동작과정

1. 사용자가 URL을 클릭하면 HTTP Request를 Servlet Container에 보낸다.

2. Servlet Container는 HttpServletRequest, HttpServletResponse 두 객체를 생성한다.

3. 사용자가 요청한 URL을 분석하여 어느 서블릿에 대한 요청인지 찾는다.

4. 컨테이너는 서블릿 service() 메소드를 호출하며, POST/GET 여부에 따라 doGet() 또는 doPost()가 호출된다.

5. doGet() 이나 doPost() 메소드는 동적인 페이지를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.

6. 응답이 완료되면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킨다.

서블릿 생명주기 (Servlet Lifecycle)

init() - 서버가 켜질때 한번만 실행

service - 모든 유저들의 요청들을 받는다.

destroy() - 서버가 꺼질때 한번만 실행


스프링 컨테이너 (Spring Container)

먼저 Spring Container를 이해하기 위해서는 IOC와 DI를 이해해야 한다. Spring Container는 Bean들의 생명주기를 관리한다. Spring Container는 어플리케이션을 구성하는 Bean들을 관리하기 위해 IoC를 사용한다. Spring Container 종류에는 BeanFactory와 이를 상속한 ApplicationContext가 존재한다. 이 두개의 컨테이너로 의존성 주입된 빈들을 제어하고 관리할 수 있다. 아래는 스프링 웹애플리케이션 동작원리이다.

 

1. 웹 애플리케이션이 실행되면 Tomcat(WAS)에 의해 web.xml이 로딩된다. (load-on-startup 으로 톰캣시작시 servlet 생성가능하도록 설정 가능)

2. web.xml에 등록되어 있는 ContextLoaderListener(Java Class)가 생성된다. ContextLoaderListener 클래스는 ServletContextListener 인터페이스를 구현하고 있으며, ApplicationContext를 생성하는 역할을 수행한다.

3. 생성된 ContextLoaderListener는 applicationContext.xml을 로딩한다.

4. applicationContext.xml에 등록되어 있는 설정에 따라 Spring Container가 구동된다. 이때 개발자가 작성한 비즈니스 로직에 대한 부분과 DAO, VO 객체들이 생성된다.

5. 클라이언트로부터 웹애플리케이션 요청이 온다.

6. DispatcherServlet(Servlet)이 생성된다. DispatcherServlet은 FrontController의 역할을 수행한다. 클라이언트로부터 요청 온 메시지를 분석하여 알맞은 PageController에게 전달하고 응답을 받아 요청에 따른 응답을 어떻게 할지 결정만 한다. 실질적인 작업은 PageController에서 이뤄지기 때문이다. 이러한 클래스들을 HandlerMapping, ViewResolver 클래스라고 한다.

7. DispatcherServlet은 servlet-context.xml(spring-mvc.xml)을 로딩한다.

8. 두번째 Spring Container가 구동되면 응답에 맞는 PageController들이 동작한다. 이때 첫번째 Spring Container가 구동되면서 생성된 DAO, VO, ServiceImpl 클래스들과 협업하여 알맞은 작업을 처리하게 된다.


서블릿 컨테이너는 웹애플리케이션 서버중에서 HTTP 요청을 받아 처리하는 기초 역할을 맡고있다. 대부분의 웹프레임워크가 제공하는 기능은 서블릿 컨테이너 위에서 동작하는 서블릿, 필터, 이벤트 리스너 등을 적절하게 구현한 것이다. 따라서 사용자가 웹 프레임워크로 작성한 웹 애플리케이션은 결국 서블릿 컨테이너 위에서 동작한다. 서블릿 컨테이너의 종류로는 아파치 톰캣, 제티 등이 서블릿 컨테이너로 널리 사용되고 있다.

 

서블릿 컨테이너에 의해 프로그램이 실행되기 위해서는 표준 즉 Servlet Interface를 구현해줘야 한다. 사용자 정의 서블릿은 서블릿 컨테이너 내에 등록된 후 서블릿 컨테이너에 의해 생성, 호출, 소멸이 이루어진다.

 

때로는 서블릿은 자신의 상태 변경 시점을 알아내 적절한 리소스 획득/반환 등의 처리를 해야하므로 Servlet 인스터페이스에 init/destroy 메서드가 정의된다. 다시 말해 서블릿 컨테이너는 서블릿의 생명주기에 따라 서블릿의 상태를 변경하면서 서블릿 인터페이스에 정의된 각 메서드를 불러준다.

 

HTTP 프로토콜로 전달된 메시지는 서블릿 컨테이너에서 해석되고 재조합되어 웹프로그래머가 작성한 서블릿으로 전다로디는 과정을 거친다.

1) Servlet

Servlet은 서블릿 프로그램을 개발할때 반드시 구현해야 하는 메서드를 선언하고 있는 인터페이스이다. 이 표준을 구현해야 서블릿 컨테이너가 해당 서블릿을 실행할수 있다.

2) GenericServlet

GenericServlet은 Servlet 인터페이스를 상속하여 클라이언트-서버 환경에서 서버단의 애플리케이션으로서 필요한 기능을 구현한 추상클래스이다. service() 메서드를 제외한 모든 메서드를 재정의하여 적절한 기능으로 구현했다. GenericServlet 클래스를 상속하면 애플리케이션의 프로토콜에 따라 메서드 재정의 구문을 적용해야 한다.

3) HttpServlet

일반적으로 서블릿이라하면 거의 대부분 HttpServlet을 상속받은 서블릿을 의미한다. HttpServlet은 GenericServlet을 상속받았으며, GenericServlet의 유일한 추상 메서드인 service를 HTTP 프로토콜 요청 메서드에 적합하게 재구현해놨다.

 

이미 DELETE, GET, HEAD, OPTIONS, POST, PUT, TRACE를 처리하는 메소드가 모두 정의되어 있다.


서블릿 실행순서

서블릿의 실행 순서는 개발자가 관리하는게 아닌 서블릿 컨테이너가 관리를 한다. 즉 서블릿에 의해 사용자가 정의한 서블릿 객체가 생성되고 호출되고 사라진다. 즉 이렇게 개발자가 아닌 프로그램에 의해 객체들이 관리되는 것을 IoC(Inversion of Control)이라고 한다.

 

다음은 서블릿 컨테이너의 생명주기를 도식화한 것이다.

- 서블릿 컨테이너는 클라이언트로부터 처음 요청이 들어오면 현재 실행할 서블릿이 최초의 요청인지 판단하고 없으면 해당 서블릿을 새로 생성한다. 이 작업은 최초 1회만 일어난다.

- init() 메소드는 해당 사용자 서블릿이 최초 생성되고 바로 호출되는 메소드이다.

- service() 메소드는 최초의 요청이든 2번째 요청이든 계속 호출되는 메소드이다.

 

여기에서 서블릿컨테이너가 종료된다면 사용자 정의 HttpServlet의 destroy() 메서드가 호출될 것이다.

 

다음은 HttpServlet의 구현체 내부이다.

package javax.servlet.http;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public abstract class HttpServlet extends GenericServlet
            implements java.io.Sericalizable {
    private static final String METHOD_DELETE = "DELETE";
    private static final String METHOD_HEAD = "HEAD";
    private static final String METHOD_GET = "GET";
    private static final String METHOD_OPTIONS = "OPTIONS";
    private static final String METHOD_POST = "POST";
    private static final String METHOD_PUT = "PUT";
    private static final String METHOD_TRACE = "TRACE";
    
    private static final String HEADER_IFMODSINCE = "If-Modified-Since";
    private static final String HEADER_LASTMOD = "Last-Modified";
    
    private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
    private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);
    
    public HttpServlet() {
    }
    
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_get_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
        } else {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
        }
    }

    protected long getLastModified(HttpServletRequest req) {
        return -1;
    }

    protected void doHead(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        NoBodyResponse response = new NoBodyResponse(resp);

        doGet(req, response);
        response.setContentLength();
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_post_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
        } else {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
        }
    }

    protected void doPut(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_put_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
        } else {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
        }
    }

    protected void doDelete(HttpServletRequest req,
            HttpServletResponse resp)
            throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_delete_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
        } else {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
        }
    }

    private Method[] getAllDeclaredMethods(Class c) {
        if (c.getName().equals("javax.servlet.http.HttpServlet"))
            return null;

        int j = 0;
        Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());
        Method[] thisMethods = c.getDeclaredMethods();

        if (parentMethods != null) {
            Method[] allMethods =
                    new Method[parentMethods.length + thisMethods.length];
            for (int i = 0; i < parentMethods.length; i++) {
                allMethods[i] = parentMethods[i];
                j = i;
            }
            j++;
            for (int i = j; i < thisMethods.length + j; i++) {
                allMethods[i] = thisMethods[i - j];
            }
            return allMethods;
        }
        return thisMethods;
    }

    protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        Method[] methods = getAllDeclaredMethods(this.getClass());

        boolean ALLOW_GET = false;
        boolean ALLOW_HEAD = false;
        boolean ALLOW_POST = false;
        boolean ALLOW_PUT = false;
        boolean ALLOW_DELETE = false;
        boolean ALLOW_TRACE = true;
        boolean ALLOW_OPTIONS = true;

        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];

            if (m.getName().equals("doGet")) {
                ALLOW_GET = true;
                ALLOW_HEAD = true;
            }
            if (m.getName().equals("doPost"))
                ALLOW_POST = true;
            if (m.getName().equals("doPut"))
                ALLOW_PUT = true;
            if (m.getName().equals("doDelete"))
                ALLOW_DELETE = true;
        }

        String allow = null;
        if (ALLOW_GET)
            if (allow == null) allow = METHOD_GET;
        if (ALLOW_HEAD)
            if (allow == null)
                allow = METHOD_HEAD;
            else
                allow += ", " + METHOD_HEAD;
        if (ALLOW_POST)
            if (allow == null)
                allow = METHOD_POST;
            else
                allow += ", " + METHOD_POST;
        if (ALLOW_PUT)
            if (allow == null)
                allow = METHOD_PUT;
            else
                allow += ", " + METHOD_PUT;
        if (ALLOW_DELETE)
            if (allow == null)
                allow = METHOD_DELETE;
            else
                allow += ", " + METHOD_DELETE;
        if (ALLOW_TRACE)
            if (allow == null)
                allow = METHOD_TRACE;
            else
                allow += ", " + METHOD_TRACE;
        if (ALLOW_OPTIONS)
            if (allow == null)
                allow = METHOD_OPTIONS;
            else
                allow += ", " + METHOD_OPTIONS;

        resp.setHeader("Allow", allow);
    }

    protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        int responseLength;

        String CRLF = "\r\n";
        String responseString = "TRACE " + req.getRequestURI() +
                " " + req.getProtocol();

        Enumeration reqHeaderEnum = req.getHeaderNames();

        while (reqHeaderEnum.hasMoreElements()) {
            String headerName = (String) reqHeaderEnum.nextElement();
            responseString += CRLF + headerName + ": " +
                    req.getHeader(headerName);
        }

        responseString += CRLF;

        responseLength = responseString.length();

        resp.setContentType("message/http");
        resp.setContentLength(responseLength);
        ServletOutputStream out = resp.getOutputStream();
        out.print(responseString);
        out.close();
        return;
    }

    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req, resp);

        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req, resp);

        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);

            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

    private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
        if (resp.containsHeader(HEADER_LASTMOD))
            return;
        if (lastModified >= 0)
            resp.setDateHeader(HEADER_LASTMOD, lastModified);
    }

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        HttpServletRequest request;
        HttpServletResponse response;

        try {
            request = (HttpServletRequest) req;
            response = (HttpServletResponse) res;
        } catch (ClassCastException e) {
            throw new ServletException("non-HTTP request or response");
        }
        service(request, response);
    }
}

위에서 설명한 Servlet들의 상속 구조는 다음과 같을 것이다. 개발자가 정의한 MemberServlet이 구현한 service를 서블릿 컨테이너가 실행할 것이다. 만약 service가 아닌 method 방식으로 처리하고 싶다면 해당 HTTP Method 방식을 구현하면 된다.

개발자는 HttpServlet을 상속받고 HTTP Method에 맞게 서블릿을 구현해 기능을 구현할 수 있다. 그런데 여기에서 HTTPServlet을 상속하고 구현한 클래스들이 많을텐데 어떻게 요청된 URL에 따라 각각에 서블릿으로 보내줄 수 있을까.

 

답은 web.xml 또는 @WebServlet 어노테이션이다. 서블릿 2.5까지만 해도 web.xml로만 제어 가능했지만 3.0부터 어노테이션으로 클라이언트 접근을 제어할 수 있게 되었다. 다음과 같이 URL에 맞게 서블릿을 매핑할 수 있다.

<servlet>
    <servlet-name>member</servlet-name>
    <servlet-class>com.test.MemberServlet</servlet-class>
</servlet>
<Servlet-mapping>
    <servlet-name>member</servlet-name>
    <url-pattern>/member</url-pattern>
</Servlet-mapping>
@WebServlet("/member")
public class MemberServlet extends HttpServlet {
    ...
}

하지만 위의 방식대로 url마다 모두 매핑해서 사용해야 한다면 유지보수, 확장성 등을 생각하면 무모하다. 그래서 여기서 한단계 더 진보한 것이 MVC 패턴이다. MVC 패턴은 모델(비즈니스로직), 뷰(화면), Controller(최초 Request를 받는 곳)으로 나누고 개발을 하는 것이다.

 

그럼 MVC 패턴에서는 어떻게 사용자 URL을 받는 것일까. 그것은 FrontController 패턴이다. FrontController 패턴은 모든 클라이언트에 요청을 최 앞단에 FrontController를 두고 각각에 컨트롤러에 매핑해주는 방식이다.

 

모든 요청을 다음과 같이 FrontController로 넘긴다.

<servlet>
    <servlet-name>front</servlet-name>
    <servlet-class>com.test.FrontController</servlet-class>
</servlet>

<servlet-name>front</servlet-name>
<uri-pattern>*</uri-pattern>
public class FrontController extends HttpServlet {
    HashMap<String, Controller> controllerUrls = null;
    
    @Override
    public void init(ServletConfig sc) throws ServletException {
        controllerUrls = new HashMap<String, Controller>();
        controllerUrls.put("/memberInsert.do", new MemberInterController());
        controllerUrls.put("/memberDelete.do", new MemberDeleteController());
    }
    
    public void service(HttpServletRequest request, HttpServletResponse response) {
        String uri = request.getRequestURI();
        Controller subController = controllerUrls.get(uri);
        subController.execute(request, response);
    }
}

FrontController는 이제 요청되어진 URI에 따라 등록되어진 Controller를 실행할것이다. 컨트롤러는 그 뒤에 서비스, 리파지토리르 실행하고 최종적으로 뷰를 화면에 그리게 될것이다.


Spring boot와 Servlet

spring boot는 내부적으로 내장 톰캣을 가지고 있다. 즉 스프링 부트가 실행되면서 내부적으로 내장톰캣 즉 서블릿 컨테이너가 실행된다. 스프링 부트에서 사용자 정의 프로그램을 구현한 프로그램인 서블릿은 DispatchServlet이다. 스프링부트에서 DispatchServlet이 FrontController 역할을 한다.

1) 스프링 부트 실행과정

Spring boot는 ServletContainerInitializer를 구현한 TomcatStarter의 onStartup 메소드를 먼저 실행한다. 톰캣이 실행되고 다음 조건이 만족하면 Dispatcher Servlet이 등록되어 진다.

 

DispatcherServletAutoConfiguration.class에 구성되어져 있는 DispatchServlet 빈 등록으로 자동 등록되어진다. 다음은 해당 소스 내용이다.

@Configuration
@Conditional({DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition.class})
@ConditionalOnClass({ServletRegistration.class})
@EnableConfigurationProperties({HttpProperties.class, WebMvcProperties.class})
protected static class DispatcherServletConfiguration {
    private final HttpProperties httpProperties;
    private final WebMvcProperties webMvcProperties;
    
    public DispatcherServletConfiguration(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
        this.httpProperties = httpProperties;
        this.webMvcProperties = webMvcProperties;
    }
    
    @Bean(
        name = {"dispatcherServlet"}
    )
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        dispatcherServlet.setDispatchOptionsRequest(this.webMvcProperties.isDispatchOptionsRequest());
        dispatcherServlet.setDispatchTraceRequest(this.webMvcProperties.isDispatchTraceRequest());
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
        dispatcherServlet.setEnableLoggingRequestDetails(this.httpProperties.isLogRequestDetails());
        return dispatcherServlet;
    }
    
    @Bean
    @ConditionalOnBean({MultipartResolver.class})
    @ConditionalOnMissingBean(
        name = {"multipartResolver"}
    )
    public MultipartResolver multipartResolver(MultipartResolver resolver) {
        return resolver;
    }
}

- Dispatcher 서블릿이 스프링에 빈으로 등록되어진다.

- 서블릿 컨테이너(DispatcherServlet) 컨텍스트에 서블릿을 등록한다.

- 서블릿 컨테이너 필터에 등록설정 해놓은 필터들을 등록한다.

- DispatcherServlet에 각종 핸들러 매핑(자원 url)들이 등록된다. (컨트롤러 빈들이 다 생성되어 싱글톤으로 관리되어 진다.)

2) 클라이언트 요청으로부터 DispatcherServlet의 전체 흐름

FrameworkServlet은 HttpServlet을 상속받고 있다. DispatcherServlet은 FrameworkServlet을 상속받고 있다. 이말은 즉슨 DispatcherServlet이 FrontController라는 것이다. 다음은 요청후 실행 순서를 설명한 것이다. 여기에서 주의할점은 서블릿컨테이너처럼 요청이 왔을때 객체를 생성하는게 아닌 이미 컨트롤러들이 빈으로 등록되어져 있다는 것을 생각해야 한다.

 

- FrameworkServlet.service()를 먼저 탄다.

- FrameworkServlet.service()는 dispatch.doService()를 호출한다.

- dispatch.doService()는 dispatch.doDispatch()를 실행한다.

     - doDispatch는 AbstractHandlerMapping 매핑에서 핸들러(컨트롤러)를 가져온다.

     - 인터셉터 등을 지나서 해당 컨트롤러 메소드로 이동한다.

     - 해당 핸들러는 MV를 리턴한다.

     - @RestController 컨트롤 같은 경우는 컨버터를 이용해 바로 결과값을 리턴한다.

     - 만약 view에 대한 정보가 있으면 viewresolver에 들려 뷰객체를 얻는다.

     - 뷰를 통해 렌더링을 한다.

3) Application context

여기에서 Dispatcher 서블릿이 생성되면서 주의할 점이 하나 있다. 디스패처 서블릿이 생성되면서 Webapplicationcontext가 생성된다. 하나는 dispatch에 의해 생성되는 WebApplicationContext 그리고 스프링에 contextLoader에 의해 생성되는 Root WebapplicationContext가 있다. 이 둘은 부모 자식 관계이다. 구조는 아래와 같을 수 있다. 그러면 최종적으로 아래와 같은 구조로 스프링이 돌아가게 된다.

위와 같이 구성이유는 2개 이상의 DispatcherServlet을 등록하게되면 RootWebApplicationContext를 공유하기위해서 사용할 수 있다.