zenn.skin 무료버전 배포중!
자세히보기

Web/Spring

[Spring Web 6.1] DispatcherServlet 뜯어보기 - SSR 렌더링 과정

koosco! 2024. 10. 7. 20:16

 

구글링을 하면 쏟아져 나오는 SpringMVC의 구조

DispatcherServlet은 SpringMVC의 메인이라고 할 수 있을만큼 여러 기능을 수행한다. 그림에서 볼 수 있듯이 여러 컴포넌트들의 사이에서 중개자 역할을 하며 요청을 HandlerAdapter에 넘겨 처리하고, 처리한 결과를 응답에 맞게끔 후처리해주는 역할을 한다.

 

코드를 뜯어보면서 DispatcherServlet의 각 메서드가 어떤 역할을 하는지 살펴봤는데, 이번에는 정리하는 겸 각 역할을 하는 메서드를 살펴보며 마무리해보려 한다.

 

HandlerMapping 조회

// DispatcherServlet
@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;
}

 

dispatcherServlet은 handlerMappings라는 handlerMapping 목록을 유지하고 있다. request를 매개변수로 받게 되면, handlerMappings를 순회하며 요청을 처리할 수 있는 handler를 찾게 된다. 이 때, handler는 HandlerExecutionChain 타입으로 반환되며, 요청을 처리하는 handler뿐만 아니라 handler에 대한 여러 interceptor까지 함께 포함하고 있다.

 

주요 HandlerMapping은 다음과 같다

1. RequestMappingHandlerMapping

2. SimpleUrlHandlerMapping

3. BeanNameUrlHandlerMapping

 

요새는 @Controller, @RestController 기반의 handler를 사용하기 때문에 사실상 이를 처리하는 RequestMappingHandlerMapping이 거의 다 사용된다고 볼 수 있다.

 

HandlerAdapter 목록 조회

// DispatcherServlet
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");
}

getHandler를 통해 찾은 handler는 getHandlerAdapter 메서드를 통해 handler를 처리할 수 있는 HandlerAdapter를 찾게 된다.

여기서도 adapter가 해당하는 handler를 처리할 수 있는지를 확인하고 해당하는 adapter를 반환하게 된다.

 

주요 HandlerAdapter는 다음과 같다.

1. RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용

2. HttpRequestHandlerAdapter : HttpRequestHandler 처리

3. SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션x, 과거에 사용) 처리

 

마찬가지로, RequestMappingHandlerAdapter가 대부분 사용된다고 보면 된다.

 

HandlerAdapter 호출

이렇게 찾은 handlerAdapter는 doDispatch 메서드 내에서 handle 메서드를 실행하게 된다.

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

 

RequestMappingHandlerAdapter는 AbstractHandlerMethodAdapter로부터 handle 메서드를 호출하고, 다시 handleInternal을 호출하게 된다.

// AbstractHandlerMethodAdapter.handle
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return this.handleInternal(request, response, (HandlerMethod)handler);
}

 

handleInternal

handleInternal은 요청을 처리하기 전에 세션 동기화 여부, 캐시 설정 등을 확인하는 로직을 포함한다.

이런 전처리 과정이 끝나면 this.invokeHandlerMethod를 통해 실질적으로 handler 호출 로직이 실행된다.

// RequestMappingHandlerAdapter
@Nullable
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;
}

 

invokeHandlerMethod

비동기 관련 코드가 많고, 중간에 ModelFactory와 관련된 코드들이 많은데, RequestMappingHandlerAdapter는 추후에 따로 정리할 계획이기 때문에, 핵심만 보자면 가장 아래 줄에 getModelAndView 호출 부분이다.

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ...
    
    // 비동기 처리가 시작된 경우 null 반환 (동기 처리가 끝났으므로 ModelAndView 반환하지 않음)
    return asyncManager.isConcurrentHandlingStarted() ? null : this.getModelAndView(mavContainer, modelFactory, webRequest);
}

 

getModelAndView 메서드에서 드디어 mavContainer로부터 model을 받아오고, 이를 이용해 ModelAndView 인스턴스를 만들어 반환하는 것을 확인할 수 있다.

@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
    modelFactory.updateModel(webRequest, mavContainer);
    if (mavContainer.isRequestHandled()) {
        return null;
    } else {
        ModelMap model = mavContainer.getModel();
        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
        if (!mavContainer.isViewReference()) {
            mav.setView((View)mavContainer.getView());
        }

        if (model instanceof RedirectAttributes) {
            RedirectAttributes redirectAttributes = (RedirectAttributes)model;
            Map<String, ?> flashAttributes = redirectAttributes.getFlashAttributes();
            HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
            if (request != null) {
                RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
            }
        }

        return mav;
    }
}

 

postHandle

다시 dispatcherServlet으로 돌아오면, 이렇게 받은 ModelAndView에 대해 interceptor를 통해 postHandle을 적용시켜 후처리를 해준다.

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
    for(int i = this.interceptorList.size() - 1; i >= 0; --i) {
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
        interceptor.postHandle(request, response, this.handler, mv);
    }

}

 

processDispatchResult

doDispatch 메서드는 handler로부터 받은 결과를 응답하기 위해 processDispatchResult 메서드를 호출하는데 여기서 에러 화면의 처리와 render 메서드를 호출하게 된다.

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
    boolean errorView = false;
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            ModelAndViewDefiningException mavDefiningException = (ModelAndViewDefiningException)exception;
            this.logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = mavDefiningException.getModelAndView();
        } else {
            Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
            mv = this.processHandlerException(request, response, handler, exception);
            errorView = mv != null;
        }
    }

    if (mv != null && !mv.wasCleared()) {
        this.render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    } else if (this.logger.isTraceEnabled()) {
        this.logger.trace("No view rendering, null ModelAndView returned.");
    }

    if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
        }

    }
}

 

render

호출된 render 메서드는 ModelAndView로부터 view를 받아, 이를 render하게 되고 템플릿 엔진에 맞게 렌더링되게 된다.

JSP를 사실 많이 사용을 안해봐서 JSP의 내부 동작은 제대로 모르지만 view의 구현체 중 Thymleaf가 있는 것을 보면 전체적인 동작은 비슷하나 JSP는 InternalResourceView를 사용한다고 하니, 아마 내부적으로 조금 다른 동작을 갖는 것 같다.

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale();
    response.setLocale(locale);
    String viewName = mv.getViewName();
    View view;
    if (viewName != null) {
        view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
        if (view == null) {
            String var10002 = mv.getViewName();
            throw new ServletException("Could not resolve view with name '" + var10002 + "' in servlet with name '" + this.getServletName() + "'");
        }
    } else {
        view = mv.getView();
        if (view == null) {
            throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
        }
    }

    if (this.logger.isTraceEnabled()) {
        this.logger.trace("Rendering view [" + view + "] ");
    }

    try {
        if (mv.getStatus() != null) {
            request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
            response.setStatus(mv.getStatus().value());
        }

        view.render(mv.getModelInternal(), request, response);
    } catch (Exception var8) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Error rendering view [" + view + "]", var8);
        }

        throw var8;
    }
}

 

RestController를 사용할 때는 HttpMessageConverter를 사용하는데, 이에 대해서는 따로 다시 얘기를 해보려 한다.

'Web/Spring'의 다른글

  • 현재글 [Spring Web 6.1] DispatcherServlet 뜯어보기 - SSR 렌더링 과정

관련글