ViewResolver란?

사용자가 요청한 정보를 랜더링(html을 만드는 과정)하는 역할을 합니다. BeanNameViewResolver의 경우 DispatcherServlet 내에서 랜더링이 render()로 진행되고,  InternalResourceViewResolver는 InternalResourceView의 forward()를 통해서 진행됩니다. Springboot는 container를 초기화할 때 InternalResoureceViewResolver와 BeanNameViewResolver를 bean으로 자동 등록합니다.

BeanNameViewResolver은 bean이름으로 찾아서 반환하고, InternalResourceViewResolver는 JSP를 사용할 때 사용됩니다. 별도로 Resolver를 custom해서 사용할 수도 있습니다.  

BeanNameViewResolver

View이름과  동일한 이름을 가지는 Bean을 View로 생성합니다.

public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {

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


   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)) {
         return null;
      }
      if (!context.isTypeMatch(viewName, View.class)) {
         if (logger.isDebugEnabled()) {
            logger.debug("Found bean named '" + viewName + "' but it does not implement View");
         }
         return null;
      }
      return context.getBean(viewName, View.class);
   }

}
  • resolveViewName()을 통해서 View를 찾고 반환합니다. 

그 후 DispatchServlet에서는 찾은 View를 랜더링합니다. 

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);

   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 {
      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() + "'");
      }
   }

   if (logger.isTraceEnabled()) {
      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 ex) {
      if (logger.isDebugEnabled()) {
         logger.debug("Error rendering view [" + view + "]", ex);
      }
      throw ex;
   }
}

InternalResourceViewResolver

JSP 파일이나 HTML 파일과 같이 웹애플리케이션의 내부 자원을 이용해서 AbstractUrlBasedView 타입의 객체를 반환합니다. prefix, suffix를 이용하여 url을 간편화 할 수 있습니다. 

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);
   }

   // prefix:앞에 붙는 String 
   // suffix:뒤에 붙는 String 
   public InternalResourceViewResolver(String prefix, String suffix) {
      this();
      setPrefix(prefix);
      setSuffix(suffix);
   }


   public void setAlwaysInclude(boolean alwaysInclude) {
      this.alwaysInclude = alwaysInclude;
   }


   @Override
   protected Class<?> requiredViewClass() {
      return InternalResourceView.class;
   }

   @Override
   protected AbstractUrlBasedView instantiateView() {
      return (getViewClass() == InternalResourceView.class ? new InternalResourceView() :
            (getViewClass() == JstlView.class ? new JstlView() : super.instantiateView()));
   }

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

}
  • buildView() 통해서 view를 반환합니다. 
  • prefix와 suffix로 조건에 맞는 view를 찾을 수 있게 해줍니다. 

InternalViewResource

@Override
protected void renderMergedOutputModel(
      Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

   exposeModelAsRequestAttributes(model, request);

   exposeHelpers(request);

   String dispatcherPath = prepareForRendering(request, response);

   RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
   if (rd == null) {
      throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
            "]: Check that the corresponding file exists within your web application archive!");
   }

   if (useInclude(request, response)) {
      response.setContentType(getContentType());
      if (logger.isDebugEnabled()) {
         logger.debug("Including [" + getUrl() + "]");
      }
      rd.include(request, response);
   }

   else {
      if (logger.isDebugEnabled()) {
         logger.debug("Forwarding to [" + getUrl() + "]");
      }
      rd.forward(request, response); // 반환
   }
}
  • 랜더링하여 반환을 합니다. 

WebMvcConfiguration ViewResolver 등록 메서드

public class WebMvcAutoConfiguration {

      // 중략
      @Bean
      @ConditionalOnMissingBean
      public InternalResourceViewResolver defaultViewResolver() {
         InternalResourceViewResolver resolver = new InternalResourceViewResolver();
         resolver.setPrefix(this.mvcProperties.getView().getPrefix());
         resolver.setSuffix(this.mvcProperties.getView().getSuffix());
         return resolver;
      }
      // 중략
}
  • application.properties 혹은 application.yml에서 prefix와 suffix를 지정하여 변경할 수 있습니다. 

HttpMessageConverters

ViewResolver가 View에 데이터를 랜더링해서 html 혹은 jsp를 반환하는 것이 었다면, HttpMessageConverters는 HTTP 요청 혹은 응답 본문을 변경할 때 사용됩니다. 이때 byte[] 타입은 ByteArrayHttpMessageConveter String 형식이라면 StringHttpMessageConverter 객체라면 MappingJackson2HttpMessageConverter가 작동합니다. 그렇다면 ViewResolver를 적용할지 HttpMessageConveter를 적용할지 결정하는 것은 @RequestBody, @ResponseBody의 여부입니다.

HttpMessageConverter 적용시점은 @RequestMapping이 처리되는 RequestMappingHandlerAdapter에서 적용됩니다. 

 

@RequestBody, @ResponseBody x -> ViewResolver

@RequestBody, @ResponseBody o -> HttpMessageConveter

HttpMessageConveterInterface

public interface HttpMessageConverter<T> {

   boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

   boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

   List<MediaType> getSupportedMediaTypes();

   default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
      return (canRead(clazz, null) || canWrite(clazz, null) ?
            getSupportedMediaTypes() : Collections.emptyList());
   }

   T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
         throws IOException, HttpMessageNotReadableException;

   void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
         throws IOException, HttpMessageNotWritableException;
}
  • canRead()와 canWrite()는 MediaType이 적합하여 변환할 수 있는지 체크합니다.
  • canRead(), canWrite()를 통과한다면, read()와 write()를 수행합니다.

canRead()가 작동하는지 디버깅을 해보겠습니다. 

url: localhost:8080/requestBody  Method: GET

{
    "size"10
}
@RestController
public class ParentController{

    @GetMapping("/requestParam")
    public int requestParam(@RequestBody int size) { // RequestBody가 있으므로 HttpMessageConverter 동작
        return size;
    }
    
}

HttpMessageConverter를 구현하고 있는 AbstractHttpMessageConverter canRead 메서드 입니다. 

  • Application/JSON 형식이기에 HttpMessageConverter를 구현하고 있는 AbstractMessageConverter가 동작하는 것을 알 수 있습니다. 

정리하기

ViewResolver

  • BeanNameViewResolver혹은 InternalResourceViewResolver를 통해서 랜더링을 진행한다.
  • html 혹은 jsp 같은 view를 반환할 때 사용된다.
  • ModelAndView가 반환되는 Dispatcher 시점에서 View를 랜더링한다.

HttpMessageConverter

  • HTTP 요청 혹은 응답 본문에 사용됩니다. 
  • @RequestBody, @ResponseBody가 붙은 것들에 적용됩니다.
  • byte[]의 경우 ByteArrayHttpMessageConveter
  • String의 경우 StringHttpMessageConveter
  • 객체의 경우 MappingJackson2HttpMessageConveter
  • 적용 시점은 RequestMappingHandlerAdapter에서 적용됩니다.

 

지금까지 ViewResolver와 HttpMessageConveter에 대해서 알아봤습니다. 감사합니다.