Framework/Spring

DispatcherServlet를 중심으로 본 스프링 MVC 동작 원리

Joonfluence 2024. 1. 31.

오늘은 Spring MVC를 이해하는 핵심인 DispatcherServlet의 동작원리를 알아봄으로써 스프링 웹 어플리케이션이 어떻게 동작하는지 알아보도록 하겠습니다.

정의

  • 디스패처 서블릿(DispatcherServlet)은 Dispatch(보내다) + Servlet(클라이언트의 요청을 처리하고 결과를 반환하는 Servlet의 구현 규칙을 지킨 웹 프로그래밍 기술)의 합성어입니다. 디스패처 서블릿은 서버로 들어오는 모든 HTTP 요청을 가장 먼저 받아서 적합한 컨트롤러에게 응답을 위임하는 프론트 컨트롤러(FrontController)입니다.

역할

  • 이러한 역할은 위와 같은 다이어그램으로 도식화될 수 있습니다. 그런데 프론트 컨트롤러(FrontController), 핸들러(Handler), 핸들러 어댑터(HandlerAdapter), 핸들러 매핑(HandlerMapping), 뷰 리졸버(viewResolver) 등 아직 저희가 모르는 용어들이 많이 등장합니다. 차근 차근 알아보십다.

프론트 컨트롤러

  • 프론트 컨트롤러(FrontController)는 공통 작업을 처리하고, 차이가 발생하는 세부 작업은 자식 클래스에 위임하여, 코드 재사용성을 높인 디자인패턴입니다.
  • 웹 요청을 처리하기 위해서 해야 할 공통된 작업에는 View Resolve(HTML 렌더링을 위한 경로 지정), Dispatcher의 forward 메서드 호출 등이 있습니다.

핸들러, 어댑터 그리고 어댑터 패턴

  • 핸들러란 컨트롤러를 다르게 표현한 용어입니다. 다르게 표현한 이유는 디스패처 서블릿(DispatcherServlet)에는 어댑터 패턴이 적용되어 있는데, 핸들러라는 용어가 해당 디자인 패턴에서 사용되기 때문입니다.

  • 어댑터란 110V를 사용하는 서구권 국가에서 220V 전압 충전기를 사용할 때, 전압 변환을 위해 사용되는 기계입니다. 어댑터 패턴이란 호환되지 않는 인터페이스들을 변환함으로써 연결해 사용할 수 있도록 한 디자인 패턴입니다. 이 패턴은 기존의 클래스를 수정하지 않고도, 특정 인터페이스를 필요로 하는 코드에서 사용할 수 있게 해준다는 장점이 있습니다.
  • 어댑터 패턴이 적용된 까닭은 컨트롤러를 구현하는 방법이 여러가지기 때문입니다. 과거에는 컨트롤러를 Controller 인터페이스로 구현하였는데, 현재는 어노테이션 기반으로 작성됩니다. 스프링에서는 다양하게 작성되는 컨트롤러에 대응하기 위해 HandlerAdapter라는 어댑터 인터페이스를 통해 어댑터 패턴을 적용함으로써 컨트롤러의 구현 방식에 상관없이 요청을 핸들러에 위임할 수 있도록 하였습니다.

핸들러 매핑(HandlerMapping)

  • 요청 url에 맞는 컨트롤러(핸들러)를 찾는 역할의 인터페이스입니다. 프론트 컨트롤러에서 컨트롤러를 등록하는 역할도 수행하는데, 등록된 컨트롤러를 탐색하는 것이죠.

DispatcherServlet의 동작 과정

  • 클라이언트가 스프링 MVC 서버로부터 응답을 받기까지의 과정은 아래와 같습니다.
    • 클라이언트의 요청을 디스패처 서블릿이 받는다.
    • 유저의 요청을 보고 요청을 처리할 수 있는지 HandlerMapping에서 핸들러 목록을 조회한다.
    • 해당 요청을 처리할 수 있는 Adapter가 있는지 HandlerAdapter에서 어댑터 목록을 조회한다.
    • 어댑터에서 핸들러를 호출한다.
    • 컨트롤러에서 요청을 서비스 계층으로 위임한다.
    • 서비스 계층에서 실제 비지니스 로직을 처리한다.
    • 핸들러에서 처리된 값을 반환한다.
    • 서버의 응답을 클라이언트로 반환한다.

코드로 보는 설명

  1. 클라이언트의 요청을 디스패처 서블릿이 받습니다. 서블릿 컨텍스트의 Filter를 먼저 거칩니다.

  1. 유저의 요청을 보고 요청을 처리할 수 있는지 HandlerMapping에서 핸들러 목록을 조회합니다.
  • 대략 아래와 같은 로직입니다. HandlerMapping 역시, 어댑터 패턴에 따라 다양한 구현 방법에 따라 등록된 Controller들을 찾도록 되어 있습니다.
@WebServlet(name = "frontControllerServlet", urlPatterns = "/front-controller/v1/*")
class FrontControllerServlet extends HttpServlet {
  private void initHandlerMappingMap() {
       handlerMappingMap.put("/front-controller/v1/a", new SampleControllerA());
       handlerMappingMap.put("/front-controller/v1/b", new SampleControllerB());
       handlerMappingMap.put("/front-controller/v1/c", new SampleControllerC());
       handlerMappingMap.put("/front-controller/v2/d", new SampleControllerD());
       handlerMappingMap.put("/front-controller/v2/e", new SampleControllerE());
       handlerMappingMap.put("/front-controller/v2/f", new SampleControllerF());
  }

  private Object getHandler(HttpServletRequest request) {
      String requestURI = request.getRequestURI();
      return handlerMappingMap.get(requestURI);
  }
}
  • 물론 실제 로직은 이와 조금 다릅니다. 오늘날 흔한 @Controller 방식은 RequestMappingHandlerMapping가 처리합니다. 이는 @Controller로 작성된 모든 컨트롤러를 찾고 파싱하여 HashMap으로 <요청 정보, 처리할 대상> 관리합니다. 여기서 처리할 대상HandlerMethod 객체로 컨트롤러, 메소드 등을 갖고 있는데, 이는 스프링이 리플렉션을 이용해 요청을 위임하기 때문입니다.
  • 그래서 요청이 오면 (Http Method, URI) 등을 사용해 요청 정보를 만들고, HashMap에서 요청을 처리할 대상(HandlerMethod)를 찾은 후에 HandlerExecutionChain으로 감싸서 반환합니다. HandlerExecutionChain으로 감싸는 이유는 컨트롤러로 요청을 넘겨주기 전에 처리해야 하는 인터셉터 등을 포함하기 위해서입니다.
  1. 해당 요청을 처리할 수 있는 Adapter가 있는지 HandlerAdapter에서 어댑터 목록을 조회합니다.
public interface HandlerAdapter {
    boolean supports(Object handler);
    @Nullable
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
  1. 어댑터에서 핸들러를 호출한다.
public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
public interface HandlerMethodReturnValueHandler {
    boolean supportsReturnType(MethodParameter returnType);
    void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
  • 핸들러 어댑터가 컨트롤러로 요청을 위임한 전/후에 공통적인 전/후처리 과정이 필요합니다. 대표적으로 인터셉터들을 포함해 요청 시에 @RequestParam, @RequestBody 등을 처리하기 위한 ArgumentResolver들과 응답 시에 ResponseEntity의 Body를 Json으로 직렬화하는 등의 처리를 하는 ReturnValueHandler 등이 핸들러 어댑터에서 처리됩니다. ArgumentResolver 등을 통해 파라미터가 준비 되면 리플렉션을 이용해 컨트롤러로 요청을 위임합니다.
  1. 컨트롤러에서 요청을 서비스 계층으로 위임한다.
  2. 서비스 계층에서 실제 비지니스 로직을 처리한다.
  • 비지니스 로직이 처리된 후에는 컨트롤러가 반환값을 반환합니다. 응답 데이터를 사용하는 경우에는 주로 ResponseEntity를 반환하게 되고, 응답 페이지를 보여주는 경우라면 String으로 View의 이름을 반환할 수도 있습니다. 요즘 프론트엔드와 백엔드를 분리하고, MSA로 가고 있는 시대에서는 주로 ResponseEntity를 반환합니다.
  1. 핸들러에서 처리된 값을 반환한다.
  • HandlerAdapter는 컨트롤러로부터 받은 응답을 응답 처리기인 ReturnValueHandler가 후처리한 후에 디스패처 서블릿으로 돌려줍니다. 만약 컨트롤러가 ResponseEntity를 반환하면 HttpEntityMethodProcessor가 MessageConverter를 사용해 응답 객체를 직렬화하고 응답 상태(HttpStatus)를 설정합니다. 만약 컨트롤러가 View 이름을 반환하면 ViewResolver를 통해 View를 반환합니다.
  1. 서버의 응답을 클라이언트로 반환한다.
  • 디스패처 서블릿을 통해 반환되는 응답은 다시 필터들을 거쳐 클라이언트에게 반환됩니다. 이때 응답이 데이터라면 그대로 반환되지만, 응답이 화면이라면 View의 이름에 맞는 View를 찾아서 반환해주는 ViewResolver가 적절한 화면을 내려줍니다.

전체 로직

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);

                // 1. 핸들러 조회
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // 2. 핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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

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

                // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
                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 ServletException("Handler dispatch failed: " + err, err);
            }
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new ServletException("Handler processing failed: " + err, 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);
                }
            }
        }
    }

  private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
          render(mv, request, response);
  }

  protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    View view;
        String viewName = mv.getViewName(); 

        //6. 뷰 리졸버를 통해서 뷰 찾기, 7.View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

        // 8. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
  }

결론

이상으로 DispatcherServlet의 동작 원리에 대해 알아보았습니다. 감사합니다.

레퍼런스

https://mangkyu.tistory.com/18
https://docs.spring.io/spring-framework/reference/web/webmvc.html

반응형

댓글