Spring MVC Request Life Cycle - DispatcherServlet

출처 : https://all-record.tistory.com/164

목차

DispatcherServlet 이란

사용자의 요청을 제일 먼저 받는 Front Controller, 스프링 MVC는 Front Controller 패턴 으로 구현돼 있다.
DispatcherServlet도 HttpServlet을 상속 받고 Servlet으로 동작한다. 스프링 부트는 DispatcherServlet을 Servlet으로 자동으로 등록하면서 모든 경로(urlpattern = “/“) 에 대해 매핑한다.

  • 사용자로 부터 들어오는 모든 요청을 제일 먼저 받아 적절한 Handler(Controller) 를 찾아 호출한다.
  • 모든 요청은 DispatcherServlet을 거치기 때문에 모든 요청에 공통적인 작업 이 가능한다.

요청 흐름

  • 서블릿이 호출 되면 HttpServlet에서 제공하는 service 메소드가 호출된다.
  • 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet 에서 service를 오버라이드 했다.
  • FrameworkServlet service를 시작으로 여러 메소드를 호출되면서 DispatcherServlet의 doDispatch 메소드가 호출된다.

DispatcherServlet 동작 과정 살펴보기

  1. Handler 조회
    • HandlerMapping을 이용해 요청에 매핑된 핸들러를 조회한다.
    • 핸들러를 조회할 때 요청 URL을 이용해 많이 일치하는 순서대로 Handler들을 반환받는다.
  2. HandlerAdapter 조회
    • Handler를 실행할 수 있는 HandlerAdapter를 조회한다.
  3. HandlerInterceptor의 preHandler 메소드 실행
  4. HandlerAdapter를 실행
    • HandlerAdapter는 요청을 처리할 수 있는 Handler를 실행시킨다.
    • 처리 결과로 ModelAndView를 반환받는다.
  5. 반환된 ModelAndView 객체에 View가 없을 경우 RequestToViewNameTranslator를 이용해 적절한 View를 가져온다.
  6. HandlerInterceptor가 있다면 postHandler를 실행
  7. DispatcherServlet 결과에 대한 후처리를 진행
    • 예외에 대한 처리
    • View 렌더링
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. 현재 요청을 처리하기 위한 Handler(HandlerExecutionChain 객체)를 가져온다.
mappedHandler = getHandler(processedRequest);

if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 2. 현재 요청에 해당하는 HandlerAdapter를 가져온다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 3. HandlerInterceptor가 있다면 preHandler를 실행시킨다.
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;
}
}

// 3. 등록된 인터셉터의 preHandle 메서드를 적용합니다.
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 4. HandlerAdapter 객체를 이용해 Request를 Handler로 처리한 후 ModelAndView 객체를 반환 받는다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

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

// 5. ModelAndView객체에 View name이 없을 경우 RequestToViewNameTranslator를 이용해 View name을 설정한다.
applyDefaultViewName(processedRequest, mv);

// 6. HandlerInterceptor가 있다면 postHandler를 실행시킨다.
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);
}

// 7. DispatcherServlet 결과에 대한 후처리를 진행한다.
// Handler 결과에서 예외가 떨어질 경우 예외처리를 진행하고 적절한 View가 있을 경우 Rendering을 진행한다.
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);
}
}
}
}

1. 요청을 처리하기 위한 Handler 가져오기

getHandler 메소드는 DispatcherServlet이 가지고 있는 HandlerMapping 객체들 중에서 전달 받은 Request를 처리할 수 있는 Handler 객체를 가져온다.

  • HandlerMapping 객체는 Request(요청)와 Request를 처리하기 위한 Handler의 Mapping 정보를 관리하는 관리하는 객체다.
  • HandlerMapping 객체는 getHandler 메소드를 통해 Handler 와 HandlerInterceptor 를 갖고 있는 HandlerExecutionChain 객체를 가져온다.
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

2. Handler를 사용하기 위한 HandlerAdapter 가져오기

getHandlerAdapter 메소드는 DispatcherServlet이 가지고 있는 HandlerAdapter 객체들 중에서 전달 받은 Handler를 지원 HandlerAdapter 객체를 가져온다.

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
// HandlerAdapter가 Handler를 지원하는지 확인한다.
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");
}

3. ModelAndView 객체에 View 정보를 연결

ModelAndView 객체에 View가 없을 경우 RequestToViewNameTranslator 객체를 이용해 View를 얻어온다.

  • RequestToViewNameTranslator 기본적인 구현체는 DefaultRequestToViewNameTranslator 가 있다.
  • DefaultRequestToViewNameTranslator 는 요청 URLPrefixPostfix 를 붙인 결과를 반환한다.
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
mv.setViewName(defaultViewName);
}
}
}

반환 받은 View 객체가 null 일 경우

Controller의 반환 값이 null일 경우 DispatcherServlet에서 applyDefaultViewName 메소드를 통해 View를 가져온다.

applyDefaultViewName 메소드의 기본 정책은 Request URL로부터 View name을 가져온다.

// 아무것도 반환하지 않는 Controller
@RequestMapping("/response/hello")
public void responseViewV3(Model model){
model.addAttribute("data", "hello!");
}

ModelAndView 객체내의 View 값이 null 인 것을 확인할 수 있다.

요청 URL을 이용해 View Name 을 가져온 것을 확인할 수 있다.

4. DispatcherServlet 결과에 대한 후 처리를 진행

processDispatchResult 메소드는 전달 받은 Exception 객체가 null이 아닐 경우 적절한 예외처리를 진행한다.
ModelAndView 객체가 있을 경우 View or Model 데이터가 있는지 확인하고 문제가 없으면 View를 렌더링 한다.

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;

// 예외가 있을 경우 예외처리를 진행한다. 1. ModelAndViewDefiningException, 2. ProcessHandlerException
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}

// 1. ModelAndView 객체가 비었는지 확인, 2. View or Model 데이터가 있는지 확인한다.
if (mv != null && !mv.wasCleared()) {
// View Rendering을 호출한다.
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}

if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}

if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

View를 렌더링 한다.

render 메소드에서는 ViewResolver를 이용해 View 객체를 가져온 후 View 객체를 렌더링 해준다.

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);

View view;
String viewName = mv.getViewName();

if (viewName != null) {
// ViewResolver를 이용해 View 객체를 가져온다.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}

// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// View 객체를 렌더링 해준다.
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}

resolveViewName 메소드는 viewName과 일치하는 View 객체를 가져온다.

protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {

if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {

// viewName과 일치하는 View를 가져온다.
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
Share