[Spring] 스프링 MVC 흐름도

MVC

  • Model은 데이터를 의미한다. 예를 들어, 회원, 상품, 주문과 같은 데이터.
  • View는 사용자에게 보여지는 화면을 의미한다. 예를 들어, jsp, html 파일.
  • Controller는 요청을 처리하고 응답을 주는 역할을 한다.
  • 비즈니스 로직과 프레젠테이션 로직을 분리하는 것이 핵심이다.
MVC 흐름도

왜 필요한가

  • 기존에는 하나의 서블릿 안에 비즈니스 로직과 뷰 렌더링까지 모두 처리한다. 너무 많은 역할을 담당하고 있다.
  • 비즈니스 로직을 수정하는 일과 UI를 수정하는 일은 별개로 발생할 가능성이 높고, 서로 영향을 주지 않는다.
  • 결과적으로 유지보수성을 높이기 위해 사용하는 것이고, 관련성이 높은 코드들을 묶어서 관심있는 코드끼리 볼 수 있다.

Front Controller

  • 모든 요청(Request)를 하나의 컨트롤러(Controller)를 통해 작업을 한 곳에서 수행할 수 있다.
  • 로직이 시작하기 전에 공통적인 요청에 대한 선처리 작업을 일괄적으로 처리할 수 있다.
  • Spring Web MVC에서 Front Controller 패턴을 사용하며, DispatcherServlet이 Front Controller 역할을 담당하고 있다.

Spring MVC 흐름도

Web MVC 흐름도

  1. 클라이언트의 요청은 DispatcherServlet에게 전달된다.
  2. DispatcherServlet은 해당 요청을 분석하여 HandlerMapping 목록에서 이 요청을 처리할 수 있는 핸들러를 가져온다.
  3. DispatcherServletHandlerAdapter 목록 중 2번 과정에서 가져온 핸들러를 지원(support)하는 HandlerAdapter를 가져온다.
  4. DispatcherServlet은 3번 과정에서 가져온 HandlerAdapter.handle()을 호출한다.
  5. HandlerAdapter를 통해서 핸들러, 즉 Controller를 호출한다.
  6. HandlerAdapter는 결과적으로 ModelAndView를 반환한다.
  7. DispatcherServletViewResolver.resolveViewName()를 호출한다.
  8. ViewResolver는 전달받은 뷰 이름에 맞는 View 객체를 반환한다.
  9. DispatcherServletView.render(model)를 호출하여 렌더링한다. response가 만들어진다.
  10. 응답은 클라이언트에게 전달된다.

더 자세히

여기 아래부터는 실제 코드를 보면서 이야기한다. 서블릿, 스프링(인터셉터, 컨트롤러 등)에 대한 배경지식이 필요할 수 있다.

Spring MVC 흐름에서 핵심은 DispatcherServlet이다. 일반적인 서블릿처럼 HttpServlet을 상속받고 doGet(), doPost()와 같은 함수를 오버라이드 한다.

부모 클래스인 FrameworkServlet에서 오버라이드 하고 있으며, doGet()을 따라가다보면 아래와 같이 호출된다.

1
2
3
4
FrameworkServlet.doGet()
  -> FrameworkServlet.processRequest()
    -> DispatcherServlet.doService()
      -> DispatcherServlet.doDispatch()

DispatcherServlet에서의 핵심 로직은 결국 doDispatch()에 있다.

doDispatch()를 분석해보자.

임의로 실제 코드에서 필요한 부분만 뽑아서 작성하였다. 정확한 코드는 직접 확인해보길 바란다.

checkMultipart(request)

multipart 요청이면, 요청을 감싼 MultipartHttpServletRequest로 만든다. 이는 파일에 접근할 수 있는 방법(메서드)을 제공한다.

1
2
3
4
5
6
7
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  ...
}

getHandler(request)

요청에 맞는 핸들러를 가져온다. 이 때 어떤 핸들러를 가져오냐의 기준은 URL뿐만 아니라 헤더 정보 등을 포함한다.

1
2
3
4
5
6
7
8
9
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  mappedHandler = getHandler(processedRequest);

  ...
}
1
2
DispatcherServlet.doDispatch()
  -> DispatcherServlet.getHandler()
1
2
3
4
5
6
7
8
9
10
11
protected HandlerExecutionChain getHandler(HttpServletRequest request) {
  if (this.handlerMappings != null) {
    for (HandlerMapping mapping : this.handlerMappings) {
      HandlerExecutionChain handler = mapping.getHandler(request);
      if (handler != null) {
        return handler;
      }
    }
  }
  return null;
}

HandlerMapping 리스트에서 for문 돌면서 적절한 핸들러를 찾는다. HandlerMapping은 어떤 요청에 어떤 핸들러를 맵핑할건지에 대해 담고있다. 일반적으로 많이 사용하는 @RequestMapping을 사용하면 RequestMappingHandlerMapping 객체를 이용하여 핸들러를 가져온다.

getHandlerAdapter(handler)

핸들러에 맞는 핸들러 어댑터를 가져온다.

1
2
3
4
5
6
7
8
9
10
11
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  mappedHandler = getHandler(processedRequest);

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

  ...
}
1
2
DispatcherServlet.doDispatch()
  -> DispatcherServlet.getHandlerAdapter()
1
2
3
4
5
6
7
8
9
protected HandlerAdapter getHandlerAdapter(Object handler) {
  if (this.handlerAdapters != null) {
    for (HandlerAdapter adapter : this.handlerAdapters) {
      if (adapter.supports(handler)) {
        return adapter;
      }
    }
  }
}

HandlerAdapter 리스트에서 for문을 돌면서 핸들러를 지원(support)하는 핸들러 어댑터를 찾는다. 일반적으로 많이 사용하는 @RequestMapping을 사용하면 RequestMappingHandlerAdapter 객체가 반환될 것이다.

preHandle(request, response) of HandlerInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  mappedHandler = getHandler(processedRequest);

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

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

  ...
}

스프링 인터셉터가 제공하는 기능 중 preHandle()를 실행시킨다. 결과 값은 boolean 값으로 false이면 메서드가 끝나버린다.

ha.handle(request, response, handler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  mappedHandler = getHandler(processedRequest);

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

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

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

  ...
}

핸들러 어댑터를 통해 핸들러를 호출한다. 실제로 해당하는 메서드를 리플렉션 API를 통해 호출한다. 그리고 handle()의 반환 타입은 항상 ModelAndView이다.

스프링에서 컨트롤러를 만들어본 사람이라면 여기서 의문이 들어야한다. 컨트롤러에서 @RequestMapping을 달고있는 메서드는 들어올 수 있는 인자(parameter) 타입도 다양하고, 반환하는 타입도 다양하다.

핸들러 어댑터가 필요한 이유가 이것이다. 핸들러(컨트롤러)에서 필요한 인자를 제공하고, 핸들러 호출 결과 타입이 뭐든 간에 ModelAndView 객체로 변환한다. 이름부터가 ‘어댑터’라는 단어를 포함하고 있다.

RestController 는?

여기서 만약 컨트롤러가 RestController라면 조금 다르다. RestController는 응답이 String이거나 JSON 형식이다.

@RestController@ResponseBody를 포함하고 있는데, @ResponseBody가 적용된 컨트롤러는 handle() 호출 결과가 null 이고 반환 시점에 이미 response에 값이 담겨있다. 내부적으로 이미 응답 처리가 완료되었다면 handle() 호출 결과는 null이다.

아래 코드에서 나오지만 mvnull이면, 다음 과정의 render() 는 실행되지 않는다. response가 다 만들어졌으므로 렌더링 과정을 건너뛰고 doDispatch()가 끝난다.

그럼 이러한 경우는 언제 response가 만들어지는가? 핸들러 어댑터에서 HandlerMethodReturnValueHandler가 동작하여 반환 결과를 조작하는데, 위에서 컨트롤러의 반환 타입이 다양할 수 있다고 말한 이유가 이 때문이다. RestController의 경우, 핸들러 어댑터가 핸들러 호출 결과를 다루기 위해 HandlerMethodReturnValueHandlerRequestResponseBodyMethodProcessor.handleReturnValue()가 실행되고 메시지 컨버터에 의해 body에 결과를 담는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  
  ...
  
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
      throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
  
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
  
    // Try even with null return value. ResponseBodyAdvice could get involved.
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
  } 
}

writeWithMessageConverters(...)에서 outputMessage에 JSON 형식의 데이터를 담을 것이다.

postHandle(request, response, mv) of HandlerInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  mappedHandler = getHandler(processedRequest);

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

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

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

  mappedHandler.applyPostHandle(processedRequest, response, mv);

  ...
}

스프링 인터셉터가 제공하는 기능 중 postHandle()을 실행시킨다. 핸들러 호출의 결과인 ModelAndView를 함께 인자로 전달한다.

위 코드에서는 try/catch 블록이 생략되어 있지만 ha.handle()에서 예외가 발생하면 postHandle()은 실행되지 않고 catch 블록으로 넘어간다.

processDispatchResult(request, response, handler, mv, exception)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

  processedRequest = checkMultipart(request);
  multipartRequestParsed = (processedRequest != request);

  mappedHandler = getHandler(processedRequest);

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

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

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

  mappedHandler.applyPostHandle(processedRequest, response, mv);

  processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

}

이 메서드를 호출하고 doDispatch()는 끝난다. 물론 위 코드는 try/catch/finally 블록이 생략되어있다.

이 함수를 통해 뷰를 렌더링한다.(응답을 만든다)

1
2
DispatcherServlet.doDispatch()
  -> DispatcherServlet.processDispatchResult()
1
2
3
4
5
6
7
8
9
10
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    @Nullable Exception exception) throws Exception {

  if (mv != null) {
    render(mv, request, response);
  }

  ...
}

render()를 호출하여 뷰를 렌더링한다.

아까 에서 RestController는 mvnull이라고 했는데, 렌더링할 필요 없으므로 여기서 분기되어 넘어간다.

render(mv, request, response)

1
2
3
DispatcherServlet.doDispatch()
  -> DispatcherServlet.processDispatchResult()
    -> DispatcherServlet.render()
1
2
3
4
5
6
7
8
9
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {

  View view;
  String viewName = mv.getViewName();
  view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

  ...

}

render()에서는 뷰 이름으로 실질적인 View 객체를 찾아야하므로, resolveViewName()을 호출한다.

1
2
3
4
DispatcherServlet.doDispatch()
  -> DispatcherServlet.processDispatchResult()
    -> DispatcherServlet.render()
      -> DispatcherServlet.resolveViewName()
1
2
3
4
5
6
7
8
9
10
11
12
13
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) {
      View view = viewResolver.resolveViewName(viewName, locale);
      if (view != null) {
        return view;
      }
    }
  }
  return null;
}

ViewResolver 리스트에서 for문을 돌면서 적절한 뷰를 찾는다.

1
2
3
DispatcherServlet.doDispatch()
  -> DispatcherServlet.processDispatchResult()
    -> DispatcherServlet.render()
1
2
3
4
5
6
7
8
9
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {

  View view;
  String viewName = mv.getViewName();
  view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

  view.render(mv.getModelInternal(), request, response);

}

View에게 model을 전달하여 렌더링한다.(응답을 만든다)

afterCompletion() of HandlerInterceptor

1
2
DispatcherServlet.doDispatch()
  -> DispatcherServlet.processDispatchResult()
1
2
3
4
5
6
7
8
9
10
11
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    @Nullable Exception exception) throws Exception {

  if (mv != null) {
    render(mv, request, response);
  }

  mappedHandler.triggerAfterCompletion(request, response, null);

}

렌더링이 끝나면 스프링 인터셉터가 제공하는 기능 중 afterCompletion()을 실행시킨다. 3 번째 인자에 null이 들어가 있는데, 예외가 들어가는 자리이다. processDispatchResult()이 정상실행되어 여기까지 도달했으므로 null이 들어간다.

만약 processDispatchResult() 실행 도중 예외가 발생하면, 호출자에게 예외 처리가 위임된다. 호출자는 doDispatch()인데, 위 코드에서는 생략되었으나 try/catch로 감싸져서 예외가 발생하면 catch 블록에서 triggerAfterCompletion()을 호출한다. 이 때는 예외가 전달되는 자리에 null이 아니라 예외가 들어간다.

마지막으로, 만들어진 응답은 서블릿에 의해 클라이언트에게 전달된다.

댓글 남기기