Spring MVC란?

Spring을 활용하여 만든 애플리케이션은 Web 기반인 경우가 많습니다. 이때 Front Controller Pattern을 이용해서 만든 방식이 Spring MVC입니다. MVC는 Model, View, Controller 클래스로 분할하여 유연하고 확장성을 장점으로 갖추었습니다.

 

Model

  • 데이터와 비즈니스 로직을 관리
  • 애플리케이션이 포함해야 할 데이터가 무엇인지를 정의
  • 일반적으로 POJO로 구성

View

  • 레이아웃과 화면을 처리
  • 애플리케이션의 데이터를 보여주는 방식을 정의

Controller

  • VIew와 Model 사이의 인터페이스 역할
  • 애플리케이션 사용자의 입력에 대한 응답으로 Model 및 View를 업데이트하는 로직을 포함
  • Model/View에 대한 사용자 입력 및 요청을 수신하여 그에 따라 적절한 결과를 Model에 담아 View에 전달
  • 즉, Model Object와 이 Model을 화면에 출력할 View Name을 반환

Spring MVC 프로세스

하나씩 알아가 보도록 하겠습니다.

Filter와 Interceptor에 관한 내용은 생략하도록 하겠습니다.

1. 요청(Request) 

  • Client로부터 요청을 받을 시 Filter를 거쳐 통과되는 요청은 DispatcherServlet에게 전달됩니다.
  • DispatcherServlet 전달된 요청은 doDispatch를 통해서 handler를 찾는 과정을 수행합니다.
  • hadler를 찾는 과정에서는 RequestMappingHandlerMapping 객체를 통해서 찾게 됩니다. 이때 URI를 통해서 찾습니다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	// 중략
    
    // URL에 맵핑된 Handler를 찾습니다.
    mappedHandler = getHandler(processedRequest);
    
    // Handler를 수행할 수 있는 HandlerAdapter를 찾습니다.
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

	// 중략
	
    // HandlerAdapter를 통해서 Handler를 실행합니다. 
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    // ModelAndView와 응답을 반환합니다. 
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

2. HandlerMapping

mappedHandler = getHandler(processedRequest);
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;
}
  • getHandler()를 통해서 Handler를 찾습니다.

우선순위로 RequestMappingHanlderMapping이 사용되고, 없을 시 BeanNameUrlHanlderMapping이 사용됩니다.

 

0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.

3. HandlerAdapterList

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
   if (this.handlerAdapters != null) {
      for (HandlerAdapter adapter : this.handlerAdapters) {
         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");
}
  • Handler를 수행할 수 있는 HandlerAdapter를 찾습니다. 없다면 예외를 날립니다.

아래 우선순위로 찾게 됩니다. 저희는 @RequestMapping으로 찾으므로, RequestMappingHandlerAdapter가 사용됩니다. 

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

1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션 X, 과거에 사용) 처리

4. Adapter, Handler 실행 ModelAndView 반환

Intercepter를 만족하여 통과하거나, 혹은 없을 경우 Adapter를 실행합니다.

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

HandlerAdapter 인터페이스를 구현하고 있는 RequestMappingHandlerAdapter 이용합니다. 이것은 어노테이션 기반의 Controller인 @RequestMapping에서 사용됩니다.

protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

   ServletWebRequest webRequest = new ServletWebRequest(request, response);
   try {
      WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
      ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

      ServletInvocableHandlerMethod invocableMethod = 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()) {
         Object result = asyncManager.getConcurrentResult();
         mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
         asyncManager.clearConcurrentResult();
         LogFormatUtils.traceDebug(logger, traceOn -> {
            String formatted = LogFormatUtils.formatValue(result, !traceOn);
            return "Resume with async result [" + formatted + "]";
         });
         invocableMethod = invocableMethod.wrapConcurrentResult(result);
      }

      invocableMethod.invokeAndHandle(webRequest, mavContainer);
      if (asyncManager.isConcurrentHandlingStarted()) {
         return null;
      }

      return getModelAndView(mavContainer, modelFactory, webRequest);
   }
   finally {
      webRequest.requestCompleted();
   }
}
invocableMethod.invokeAndHandle(webRequest, mavContainer);
  • 위의 함수가 실행되면, Controller가 동작합니다. 그리고 getModelAndView()를 통해서 ModelAndView를 반환합니다.

5. ViewResolver실행, View 반환

InternerResourceViewResolverd와 BeanNameViewResolver 자동으로 등록되고 사용됩니다.

JSP를 사용하고자 하면 application.yml에 suffix와 prefix를 명시해주면 됩니다.

spring:
  mvc:
    view:
      prefix: /WEB-INF/views/
      suffix: .jsp

Thymeleaf를 dependency에 추가하였다면 ThymeleafViewResolver가 등록되고 사용됩니다.

prefix와 suffix의 기본값은 각각 'classpath:/templates/'와 '. html'입니다. 

 

우선순위에 따라서 Resolver가 실행됩니다. 더 많은 Resolver가 존재하지만 많이 사용되는 Resolver만 적었습니다.

 

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {

   private int order = Ordered.LOWEST_PRECEDENCE;  // default: same as non-Ordered


   /**
    * Specify the order value for this ViewResolver bean.
    * <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
    * @see org.springframework.core.Ordered#getOrder()
    */
   public void setOrder(int order) {
      this.order = order;
   }

   @Override
   public int getOrder() {
      return this.order;
   }


   @Override
   @Nullable
   public View resolveViewName(String viewName, Locale locale) throws BeansException {
      ApplicationContext context = obtainApplicationContext();
      if (!context.containsBean(viewName)) {
         // Allow for ViewResolver chaining...
         return null;
      }
      if (!context.isTypeMatch(viewName, View.class)) {
         if (logger.isDebugEnabled()) {
            logger.debug("Found bean named '" + viewName + "' but it does not implement View");
         }
         // Since we're looking into the general ApplicationContext here,
         // let's accept this as a non-match and allow for chaining as well...
         return null;
      }
      return context.getBean(viewName, View.class);
   }

}
  • Bean의 이름으로 매칭하고 View를 반환합니다.
public class InternalResourceViewResolver extends UrlBasedViewResolver {

   private static final boolean jstlPresent = ClassUtils.isPresent(
         "javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());

   @Nullable
   private Boolean alwaysInclude;

   public InternalResourceViewResolver() {
      Class<?> viewClass = requiredViewClass();
      if (InternalResourceView.class == viewClass && jstlPresent) {
         viewClass = JstlView.class;
      }
      setViewClass(viewClass);
   }

   public InternalResourceViewResolver(String prefix, String suffix) {
      this();
      setPrefix(prefix);
      setSuffix(suffix);
   }
	// 중략
    
    @Override
	protected AbstractUrlBasedView buildView(String viewName) throws Exception {
		InternalResourceView view = (InternalResourceView) super.buildView(viewName);
		if (this.alwaysInclude != null) {
			view.setAlwaysInclude(this.alwaysInclude);
		}
		view.setPreventDispatchLoop(true);
		return view;
	}
}
  • InternalResourceView 클래스는 내부에 forward()를 가지고 있습니다. 
public class InternalResourceView extends AbstractUrlBasedView {

   @Override
   protected void renderMergedOutputModel(
         Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
         
        // 중략
		String dispatcherPath = prepareForRendering(request, response);
		RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
      // 중략
        rd.forward(request, response);
      }
      //중략
}
  • JSP의 경우 Dispatcher를 찾고 해당 Dispatcher에게 forward()를 진행합니다. 

6. View Rendering

Dispatcher의 render 함수를 통해서 데이터가 rendering 됩니다.

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) {
      // We need to resolve the view name.
      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.render(mv.getModelInternal(), request, response);
   }
   catch (Exception ex) {
      if (logger.isDebugEnabled()) {
         logger.debug("Error rendering view [" + view + "]", ex);
      }
      throw ex;
   }
}

7. 응답 (Response) 

마지막으로 Filter를 거치고 Client에게 응답이 됩니다.

지금까지 Spring MVC의 프로세스를 하나씩 확인해봤습니다.다음은 Controller 계층에서 @RequestBody, @ModelAttribute 같은 어노테이션을 사용하여 객체를 받을 수 있게하는 ArgumentResolver에 대해서 뜯어보겠습니다. 

(직접 클래스들을 뜯어보면서 작성한 글입니다. 틀린 부분이 있다면 지적해주시면 감사합니다.)