DispatcherServlet in Spring MVC

jiho·2021년 6월 7일
2

Spring

목록 보기
5/13

개발자가 비지니스 로직을 Servlet으로 정의하고 Servlet Container인 Tomcat을 통해 서비스를 제공할 수 있다는 것을 알았습니다. 하지만 아직 스프링 프레임워크의 @Controller 속 메서드가 어떻게 호출되는지 모르니 스프링 프레임워크가 마법같이 남아있을 수 있습니다.

이번 정리는 Servlet 에서 스프링 프레임워크의 연결점인 DispatchServlet에 대해 자세히 알아보겠습니다.

Spring Framework 핵심을 공부해보면 ApplicationContext라는 인터페이스가 핵심이며 IoC 컨테이너의 역할 및 개발에 도움되는 여러 유틸리티 기능들을 제공합니다. 그리고 Spring Web MVC는 이런 Core 기술들을 잘 활용해서 편리하고 높은 수준의 웹서비스를 제공할 수 있는 틀을 제공해줍니다.

Servlet 기반의 웹서비스를 제공할 때 진입점이며 가장 중요한 요소가 Spring Web MVC 속 DispatchServlet class 입니다. 공식 API Reference를 살펴보면 HttpServlet를 상속한 것을 볼 수 있습니다.

Spring MVC 가 왜 필요할가? - DispatcherServlet이 핵심

우선 늘 그렇듯 좋은 접근은 해당 기술이 왜 생겼는지를 생각해보면 이해에 도움이 됩니다. 우선 Servlet 기반의 웹 서비스의 경우, Servlet Container에서 특정 URL에 알맞는 서비스를 제공하기 위해 web.xml에 url-mapping 설정과 그에 맞는 Servlet을 정의해서 추가해야합니다. 그리고 뷰(JSP)와 모델, 비지니스 로직이 하나의 서블릿에서 관리하면 관심사 분리가 제대로 되지않습니다. 즉, 역할 구분이 정확하지 않아 유지보수가 어려워집니다.

이러한 문제를 해결하는 것이 FrontController패턴으로 하나의 서블릿에서 모든 요청을 받아들이고 각 요청에 알맞은 처리를 하는 handler들에게 dispatch해서 역할을 위임하는 방식입니다. 그러한 역할을 해주는 것이 Spring의 DispatchServlet입니다.

Front Controller 패턴은 들어오는 요청들에 대한 중앙 집중적으로 처리해서 중복되는 코드를 제거할 수 있습니다.

여기까지만 살펴봐도 스프링을 공부하면서 DispatchServlet을 모르는다는 것은 말이 안될 수준입니다.

DispatchServlet에 대해 더 자세히 알아보겠습니다.

DispatcherServlet 유연함

Spring DispatchServlet API 문서

This servlet is very flexible: It can be used with just about any workflow, with the installation of the appropriate adapter classes

문서를 살펴보면 위와 같은 말이 있습니다. 적절한 어뎁터 클래스의 설정(?)으로 어떠한 workflow에 대해서도 사용될 수 있다. 매우 유연하다.

HTTP를 통해 할 수 있는 수 많은 처리들(파일업로드, 헤더파일, METHOD별 구분, 웹소켓처리 등등)을 DispatchServlet이라는 하나의 클래스가 모든 로직을 가지고 있지는 않습니다. 어플리케이션에 따라 처리해야할 방식도 달라집니다. 예를들어 REST API인지 아니면 웹페이지로 응답을 돌려줘야하는지 등등..

각 기능들을 인터페이스로 두고 적절한 구현체로 설정(교체)해주면 손쉽고 유연하게 처리방식을 변경할 수도 있습니다.

DispatchServlet의 동작방식을 살펴보면서 어떤 기능들을 변할 수 있는지 알아보겠습니다.

DispatcherServlet의 동작 방식

우선 DispatcherServlet은 서버 어플리케이션의 초기화 단계에서 ApplicationContext의 Bean으로 부터 의존성을 주입받습니다.

특별한 빈들을 찾거나, 혹은 기본 전략에 해당하는 빈들을 등록합니다.

  • HandlerMapping : 요청에 알맞은 핸들러는 찾아주는 인터페이스
  • HandlerAdapter : 핸들러를 실행하는 인터페이스
  • HandlerExcpetionResolver
  • ViewResolver
    ...

DispatcherServlet 동작 순서

  1. Request 분석(Locale, Theme, Multipart 등등)
  2. HandlerMapping에게 요청을 처리할 핸들러를 찾도록 위임합니다.
  3. 등록되어 있는 핸들러 어댑터 중에 해당 핸들러를 실행할 수 있는 HandlerAdapter를 찾습니다.
  4. 특정 HandlerAdapter를 사용해서 요청을 처리합니다.
    • 핸들러의 리턴값을 보고 어떻게 처리할 지 판단합니다.
    • 뷰 이름에 해당하는 뷰를 찾아서 모델 데이터를 랜더링합니다. (View)
    • @ResponseBody가 있다면 Converter를 사용해서 응답을 생성한다. (REST API)
  5. (부가적으로) 예외가 발생한다면 예외 처리 핸들러에 요청 처리를 위임합니다.
  6. 최종적으로 응답을 보냅니다.

위 각 단계에서 처리되는 방식은 앞서말했듯이 유연하게 변경될 수 있습니다.

DispatcherServlet Class의 doDispatch 메소드에서 넘겨 받은 HttpServlet을 이용해서 처리를 시작합니다.

public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	...
    }

1. Request 분석 (Multipart 체크)

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

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

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    
	...
}

this.checkMultipart(request) 내부를 들여다 보겠습니다.

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
		...
       return this.multipartResolver.resolveMultipart(request);
        ...    
    }

핵심은 ApplicationContext를 통해 받은 multipartResolver를 통해 요청을 처리하게 된다는 것 입니다. 즉,MultipartResolver 를 구현한 어떤 구현체로도 교체가 가능합니다.

 private void initMultipartResolver(ApplicationContext context) {
        this.multipartResolver = (MultipartResolver)context.getBean("multipartResolver", MultipartResolver.class);
 }

위는 ApplicationContext에서 Bean을 Dispatcher에서 얻어오는 초기화 코드입니다.

2. 요청한 핸들러를 찾아주는 MappingHandler

doDipatch 메서드를 계속해서 살펴보면

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
      ...
      mappedHandler = this.getHandler(processedRequest);
      if (mappedHandler == null) {
          this.noHandlerFound(processedRequest, response);
          return;
      }
      ...

this.getHandler 메서드를 통해 요청을 처리할 Handler를 찾게됩니다.

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        Iterator var2 = this.handlerMappings.iterator();

        while(var2.hasNext()) {
            HandlerMapping mapping = (HandlerMapping)var2.next();
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }

    return null;
}

초기화 단계에서 ApplicationContext 통해 빈으로 얻은 MappingHandler들을 순회하면서 알맞은 Handler를 찾게됩니다.

private void initHandlerMappings(ApplicationContext context) { 
...
  Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
  if (!matchingBeans.isEmpty()) {
      this.handlerMappings = new ArrayList(matchingBeans.values());
      AnnotationAwareOrderComparator.sort(this.handlerMappings);
  }
...
}

3~4. Handler를 이용해서 찾은 HandlerAdapter를 사용해서 요청을 처리합니다.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...
    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
    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;
    }

    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    if (asyncManager.isConcurrentHandlingStarted()) {
        return;
    }
}

HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); 실제 Handler를 처리할 Adapter를 얻고 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 를 통해 실제 요청 처리 후 response를 얻게 됩니다.

HandlerAdapter도 마찬가지로 ApplicationContext를 통해 빈 형태로 등록되어있습니다.

default로 등록되는 Adapter들은 아래와 같습니다.

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter

5. HandlerExceptionResovler를 통한 예외처리

 private List<HandlerExceptionResolver> handlerExceptionResolvers;

마찬가지로 예외를 처리하는 방식도 유연하게 처리가능합니다.

profile
Scratch, Under the hood, Initial version analysis

0개의 댓글