woniper

Spring-MVC 읽기 #6. DispatcherServlet - @Controller는 어떻게 실행될까? 본문

Spring

Spring-MVC 읽기 #6. DispatcherServlet - @Controller는 어떻게 실행될까?

woniper 2019.01.15 08:46

Spring-MVC로 웹 애플리케이션을 개발하면 아래와 같은 Controller를 만든다.

@Controller
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello Spring~";
    }
}

어떻게 @Controller 선언 만으로 메소드가 실행되고, Controller 실행 전/후로 전처리, 후처리 가능한 Interceptor가 동작할까? 뿐만 아니라, 파일 업로드, View 반환 등 Spring-MVC에서 제공되는 여러 가지 기능들은 어떻게 실행될까?

Spring-MVC 읽기 #5. AbstractDispatcherServletInitializer와 AbstractAnnotationConfigDispatcherServletInitializer 에서 봤던 DispatcherServlet에 의해 동작된다. 이 번 글은 Spring-MVC의 핵심이라 할 수 있는 DispatcherServlet 클래스 코드를 살펴볼 것이다.

DispatcherServlet


DispatcherServlet의 Hierarchy를 보자. 위 이미지에 빨간색 박스를 누르면 DispatcherServlet의 상위 클래스를 볼 수 있다.

Spring-MVC는 Servlet 스펙을 기반으로 만들어진 프레임워크다. 때문에 당연히(?) Servlet interface를 구현하고 있다. 앞선 글에서 WAS를 자세히 설명하지 않았지만, 우리가 만든 웹 애플리케이션을 구동하고 실행하는 건 알고 있다. 그리고 WAS의 종류 중 하나인 Tomcat을 기준으로 설명했고, Tomcat은 Servlet 웹 애플리케이션을 실행한다.

HTTP를 요청하면, Tomcat은 이 Servlet 인터페이스를 실행한다.

Servlet#service

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;

    public ServletConfig getServletConfig();

     /**
     * Called by the servlet container to allow the servlet to respond to 
     * a request.
     *
     * <p>This method is only called after the servlet's <code>init()</code>
     * method has completed successfully.
     * 
     * <p>  The status code of the response always should be set for a servlet 
     * that throws or sends an error.
     *
     * 
     * <p>Servlets typically run inside multithreaded servlet containers
     * that can handle multiple requests concurrently. Developers must 
     * be aware to synchronize access to any shared resources such as files,
     * network connections, and as well as the servlet's class and instance 
     * variables. 
     * More information on multithreaded programming in Java is available in 
     * <a href="http://java.sun.com/Series/Tutorial/java/threads/multithreaded.html">
     * the Java tutorial on multi-threaded programming</a>.
     *
     *
     * @param req   the <code>ServletRequest</code> object that contains
     *          the client's request
     *
     * @param res   the <code>ServletResponse</code> object that contains
     *          the servlet's response
     *
     * @exception ServletException  if an exception occurs that interferes
     *                  with the servlet's normal operation 
     *
     * @exception IOException       if an input or output exception occurs
     *
     */
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
    
    public String getServletInfo();

    public void destroy();
}

Servlet 인터페이스는 몇 가지 선언 메소드가 있다. Servlet#service 메소드의 주석을 보면 Servlet 컨테이너에 의해 호출되어 서블릿이 요청에 응답할 수 있게 합니다. 라고 설명한다. 여기서 요청응답이 무엇을 의미할까? 바로 HTTP request, response를 의미한다. Servlet#service 메소드의 파라미터인 ServletRequestServletResponse를 봐도 Servlet#service 메소드가 어떤 의미를 하는지 추측할 수 있다.

GenericServlet#service

public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
    public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
}

Servlet 인터페이스의 구현체인 GenericServlet 클래스의 service 메소드는 abstract로 선언되어있다. 즉 GenericServlet의 하위 클래스가 구현하고 있다는 의미다.

HttpServlet#service

HttpServlet 클래스는 오버 로딩된 두 개의 service 메소드가 존재한다. ServletRequest와 ServletResponse를 파라미터로 받는 service 메소드를 보자.

HttpServlet.service(ServletRequest, ServletResponse)

public abstract class HttpServlet extends GenericServlet {

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest  request;
        HttpServletResponse response;
        
        if (!(req instanceof HttpServletRequest &&
                res instanceof HttpServletResponse)) {
            throw new ServletException("non-HTTP request or response");
        }

        request = (HttpServletRequest) req;
        response = (HttpServletResponse) res;

        service(request, response);
   }
}
  1. ServletRequest를 HttpServletRequest로 ServletResponse를 HttpServletResponse로 형 변환한다.
  2. HttpServlet.service(HttpServletRequest, HttpServletResponse) 메소드를 호출한다.

HttpServlet.service(HttpServletRequest, HttpServletResponse)

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) {
                // 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);
    }
}
  1. HTTP method를 구한다. (req.getMethod() 메소드)
  2. HTTP method에 맞게 do{method} 메소드(doGet, doPost 등)를 호출한다.

HttpServlet#doGet 메소드를 기준으로 코드를 살펴보자.

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);
    }
}
  • HttpServlet#doGet 메소드는 protocol을 구해 HttpServletResponse#sendError 메소드를 호출한다.

이상하다. 이게 끝일까? 단순히 sendError 메소드를 호출하고 doGet 메소드는 종료된다.

org.springframework.web.servlet.FrameworkServlet#doGet

HttpServlet 클래스의 하위 클래스인 FrameworkServlet 클래스도 doGet 메소드와 같은 HTTP method를 처리할 수 있는 메소드가 존재한다.

HttpServlet#service 메소드는 HttpServlet#doGet 메소드를 호출하는 게 아니라, FrameworkServlet#doGet 메소드를 호출한다.

FrameworkServlet 클래스에서 한 가지 주의 깊게 볼 것이 있다. FrameworkServlet부터는 Spring-MVC에서 구현한 클래스다. FrameworkServlet은 org.springframework.web.servlet 패키지에 존재한다. 이 클래스부터 하위 클래스는 Spring의 구현체다.

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }
}
  • FrameworkServlet#processRequest 메소드를 호출한다.

FrameworkServlet#processRequest

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        logResult(request, response, failureCause, asyncManager);
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}
  1. 현재 HTTP request의 Locale을 구한다. (LocaleContextHolder.getLocaleContext() 메소드)
  2. HTTP request의 속성(session, request, response)을 담고 있는 ServletRequestAttributes 클래스를 생성한다.
  3. 현재 요청된 정보(request, locale, requuest 속성 등)를 ThreadLocal에 담는다. (initContextHolders(request, localeContext, requestAttributes) 메소드)
  4. FrameworkServlet#doService 메소드를 호출한다.

FrameworkServlet#doService

protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;
  • FrameworkServlet#doService 메소드는 abstract 메소드다.

DispatcherServlet#doService

FrameworkServlet의 하위 클래스인 DispatcherServlet이 doService 메소드를 구현하고 있다.

public class DispatcherServlet extends FrameworkServlet {
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logRequest(request);

        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap<>();
            Enumeration<?> attrNames = request.getAttributeNames();
            while (attrNames.hasMoreElements()) {
                String attrName = (String) attrNames.nextElement();
                if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                    attributesSnapshot.put(attrName, request.getAttribute(attrName));
                }
            }
        }

        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

        if (this.flashMapManager != null) {
            FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
            if (inputFlashMap != null) {
                request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
            }
            request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
        }

        try {
            doDispatch(request, response);
        }
        finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                // Restore the original attribute snapshot, in case of an include.
                if (attributesSnapshot != null) {
                    restoreAttributesAfterInclude(request, attributesSnapshot);
                }
            }
        }
    }
}
  1. HTTP 요청 log를 찍는다. (logRequest(request) 메소드)
  2. attributesSnapshot에 HTTP reqeust 속성을 보관(snapshot)한다.
  3. WebApplicationContext, LocaleResolver, ThemeResolver, ThemeSource를 HTTP reuqest 속성에 담는다.

    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
  4. FlashMap에 속성을 저장한다. (this.flashMapManager.retrieveAndUpdate(request, response) 메소드)

    • FlashMap은 HTTP 요청, 응답 생명주기에도 살아있고, View가 렌더링 된 후에 삭제된다.
  5. DispatcherServlet#doDispatch(request, response) 메소드 호출

DispatcherServlet#doDispatcher

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}

DispatcherServlet은 여러 가지 Resolver와 클래스를 이용해 요청을 처리한다. 때문에 이 글에서 모두 설명하기는 어렵고 대략적인 것만 설명할 예정이다. 자세한 설명은 다음 글에서 여러 가지 Resolver를 하나씩 살펴볼 예정이다.

  1. Multipart 요청 인지 체크한다. (DispatcherServlet#checkMultipart(request) 메소드)
  2. HTTP 요청에 해당하는 HandlerExecutionChain을 구한다. (DispatcherServlet#getHandler(processedRequest) 메소드)
    • URL을 기준으로 HandlerMapping을 찾는다.
    • HandlerExecutionChain은 HandlerMapping에 의해 생성된다.
    • HandlerExecutionChain은 Interceptor List를 포함하고 있다.
  3. HandlerExecutionChain이 null이면 HTTP Status 404를 response 한다. (DispatcherServlet#noHandlerFound(processedRequest, response) 메소드)
    • 개발하며 자주 보게 되는 404 not found error를 이 메소드에서 실행한다.
  4. Controller를 실행하기 위해 HandlerAdapter를 구한다.(DispatcherServlet#getHandlerAdapter(mappedHandler.getHandler()) 메소드)
    • HandlerAdapter는 우리가 작성한 Controller를 실행하는 역할을 한다.
    • HandlerMapping은 URL을 기준으로 어떤 Handler로 매핑할지 결정한다면, HandlerAdapter는 결정된 Handler를 실행한다.
  5. HandlerInterceptor로 전처리(Interceptor#preHandle)를 실행한다. (mappedHandler.applyPreHandle(processedRequest, response) 메소드)
  6. HandlerAdapter#handle 메소드를 호출해 실제 로직(Controller)을 실행한 후 ModelAndView를 생성한다. (mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 메소드)
  7. Interceptor로 후처리(Interceptor#postHandle)를 실행한다. (mappedHandler.applyPostHandle(processedRequest, response, mv) 메소드)
  8. View를 렌더링 하거나, Exception을 핸들링한다. (processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException) 메소드)

DispatcherServlet#doDispatcher에서 알아야 할 것.

  1. Multipart (파일 업로드)를 처리하는 MultipartResolver
  2. Controller 실행에 필요한 HandlerMapping, HandlerAdapter 그리고 ModelAndView
  3. View 렌더링 또는 Exception 핸들링에 필요한 ViewResolver와 HandlerExceptionResolver

책 추천

이 글에서 설명한 DispatcherServlet에 대한 자세한 설명을 다룬 책이 있다. 스프링 MVC 프로그래밍

2 Comments
댓글쓰기 폼