Spring MVC HandlerAdapter 분석하기

jiho·2021년 6월 15일
2

Spring

목록 보기
10/13
post-custom-banner

DispatcherServlet을 분석하면서 추상적으로 어떤 역할들을 하는지 살펴봤었습니다. 이번에는 구체적인 HandlerAdapter들을 살펴보고 각각의 차이점을 살펴보겠습니다.

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

Handler는 우리가 @Controller에 정의한 요청을 처리기라고 생각하면 되겠지만 Adapter는 무슨 역할을 해주는지 정확히 와닿지 않습니다.

그래서 이번에 코드를 직접 살펴보면서 이해해보겠습니다.

간단히 Spring MVC에서 어떤 Adapter들을 가지고 있는지 보겠습니다.

DispatcherServlet.properties 파일 속에 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
  • HttpRequestHandlerAdapter
  • SimpleControllerHandlerAdapter
  • RequestMappingHandlerAdapter
  • HandlerFunctionAdapter

Handler를 처리해줄 HandlerAdapter 찾기

우선, @Controller 에서 정의한 핸들러들을 처리해줄 어댑터를 어떻게 찾을지 부터 알아보겠습니다.

DispatcherServlet 내의 doDispatch 메소드 중 HandlerAdapter를 찾는 부분입니다.

// HandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); 

...

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

...

getHandlerAdapter 메소드는 단순히 DispatchServlet이 초기화해놨던 HandlerAdapter들을 순회하면서 mapping된 Handler를 처리할 수 있을 경우 반환하는 역할을 합니다.

 protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
      if (this.handlerAdapters != null) {
          Iterator var2 = this.handlerAdapters.iterator();

          while(var2.hasNext()) {
              HandlerAdapter adapter = (HandlerAdapter)var2.next();
              if (adapter.supports(handler)) {
                  return adapter;
              }
          }
      }

      throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
  }

실제 구현체의 supports 함수 내부에서 어떤 기준으로 adapter가 handler처리를 지원하는지 살펴보면 될 것 같습니다.

public final boolean supports(Object handler) {
    return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);
}
protected boolean supportsInternal(HandlerMethod handlerMethod) {
    return true;
}

단순히 HandlerMethod 인스턴스이면 해당 어댑터가 지원함을 알 수 있습니다.

결국 default 전략을 사용할 경우, 대부분의 Handler에 대한 처리는 초기화시기에 adapter등록 순서에 의해 정해지게 됩니다.

HandlerAdapter의 역할

handdler 를 실행시켜준다. 라고만 정리를 했었지만 여기서는 보다 구체적으로 정의해보겠습니다.

RequestMappingHandlerAdapterhandlerInternal 코드입니다.

protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    this.checkRequest(request);
    ModelAndView mav;
    if (this.synchronizeOnSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized(mutex) {
                mav = this.invokeHandlerMethod(request, response, handlerMethod);
            }
        } else {
            mav = this.invokeHandlerMethod(request, response, handlerMethod);
        }
    } else {
        mav = this.invokeHandlerMethod(request, response, handlerMethod);
    }

    if (!response.containsHeader("Cache-Control")) {
        if (this.getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
            this.applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
        } else {
            this.prepareResponse(response);
        }
    }

    return mav;
}
  1. checkReqeust arugment로 받은 HttpServletRequest의 HTTP Method값을 확인해서 처리가능한지 확인합니다. 처리할 수 없으면 Exception을 발생시킵니다.
protected final void checkRequest(HttpServletRequest request) throws ServletException {
        String method = request.getMethod();
        if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
            throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
        } else if (this.requireSession && request.getSession(false) == null) {
            throw new HttpSessionRequiredException("Pre-existing session required but none found");
        }
    }
  1. 그리고 Session을 가지고 있을 경우, mutex를 활용해서 thread-safe하게 처리하는 것을 볼 수 있습니다. 이러한 처리를 봐서 session을 사용할 경우와 사용하지 않을 경우, 성능차이가 생길 것을 알 수 있습니다.

  2. 마지막으로 handler처리후 보내줄 응답에서 Cache-Control Header와 관련된 처리를 해주는 코드가 있음을 확인 할 수 있습니다.

사실 가장 중요한 부분은 handler를 실행 시켜주는 this.invokeHandlerMethod 메소드입니다.

핵심은 invokeHandlerMethod!

@Controller를 사용할 때 핵심적으로 살펴봐야할 부분은 바로 아래 부분입니다. 아마 요청을 처리하는 handler를 정의하는 것이 Spring 으로 웹개발을할 때 가장 많이 알아야하고 사용하는 부분일 것 입니다.

각종 Annotation과 Argument 와 return 타입을 잘 알고 다루는 것이 중요한데 invokeHandlerMethod가 그 부분을 담당해서 유연하게 처리해줍니다.

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ServletWebRequest webRequest = new ServletWebRequest(request, response);

    Object result;
    try {
        WebDataBinderFactory binderFactory = this.getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = this.getModelFactory(handlerMethod, binderFactory);
        ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }

        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }

        invocableMethod.setDataBinderFactory(binderFactory);
        invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
        AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
        asyncWebRequest.setTimeout(this.asyncRequestTimeout);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.setTaskExecutor(this.taskExecutor);
        asyncManager.setAsyncWebRequest(asyncWebRequest);
        asyncManager.registerCallableInterceptors(this.callableInterceptors);
        asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
        if (asyncManager.hasConcurrentResult()) {
            result = asyncManager.getConcurrentResult();
            mavContainer = (ModelAndViewContainer)asyncManager.getConcurrentResultContext()[0];
            asyncManager.clearConcurrentResult();
            LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
                String formatted = LogFormatUtils.formatValue(result, !traceOn);
                return "Resume with async result [" + formatted + "]";
            });
            invocableMethod = invocableMethod.wrapConcurrentResult(result);
        }

        invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
        if (!asyncManager.isConcurrentHandlingStarted()) {
            ModelAndView var15 = this.getModelAndView(mavContainer, modelFactory, webRequest);
            return var15;
        }

        result = null;
    } finally {
        webRequest.requestCompleted();
    }

    return (ModelAndView)result;
}

아직 전체 코드를 완전히 파악할 수는 없지만

  • WebDataBinderFactory를 이용해서 data binding을 처리.
  • ModelFactory 를 이용한 모델생성.
  • WebAsyncManager를 통해 비동기 처리에 관여함을 추측할 수 있습니다.

그리고 마지막으로 ModelAndView형태로 handler를 처리함을 확인할 수 있습니다.

profile
Scratch, Under the hood, Initial version analysis
post-custom-banner

0개의 댓글