@ModelAttribute: 클라이언트가 전달하는 값을 객체로 맵핑해주는 역할을 합니다. HTTP Body 데이터 혹은 HTTP 파라미터를 주입합니다. 이때 생성자나 Setter로 주입하기 때문에 Setter혹은 생성자가 있어야 합니다.

@RequestParam:
클라이언트가 전달하는 HTTP 요청 Parameter 혹은 HTTP Body의 정보를 전달받기 위해서 사용됩니다.

@PathVariable: @RequestMapping의 URI의 경로 변수를 넣어주는 역할을 합니다.


@RequestBody: HTTP Body에 담겨온 정보를 JSON 형식으로 변환하여 객체와 맵핑해주는 역할을 합니다.

@ResponseBody:  HTTP Body에 정보를 전달하기 전에 객체를 JSON 형식으로 변환하여 보내주는 역할을 합니다.


동작 과정

5개의 어노테이션 모두 별도로 개발자가 값을 꺼내오지 않고 바인딩이 됩니다. 이런 게 가능한 이유는
RequestHandlerMappingAdapter가 동작하며, 알맞은 ArgumentResolver가 존재한다면 동작합니다. 이러한 ArgumentResolver들은 
HandlerMethodArgumentResolver를 상속받은 구현체를 통해서 동작합니다. Custom ArgumentResolver를 만들고 싶다면  상속받고 구현하시면 사용할 수 있습니다. 

 

RequestHandlerMappingAdapter란?

- @RequestMapping 어노테이션을 이용해서 사용자의 요청을 처리할 수 있는 Controller와 맵핑해주는 역할을 합니다.

 

ArgumentResolver란?

- 위에서 봤듯이 컨트롤러에 들어온 요청 정보를 가공하여 값을 자동으로 넣어주는 역할을 합니다. 

RequestHandlerMappingAdapter 

spring container가 초기화될 때 생성되는 init Resolver들과 사용자가 만든 CustomResolver를 가지고 있습니다. 

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
   List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);

   // Annotation-based argument resolution
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
   resolvers.add(new RequestParamMapMethodArgumentResolver());
   resolvers.add(new PathVariableMethodArgumentResolver());
   resolvers.add(new PathVariableMapMethodArgumentResolver());
   resolvers.add(new MatrixVariableMethodArgumentResolver());
   resolvers.add(new MatrixVariableMapMethodArgumentResolver());
   resolvers.add(new ServletModelAttributeMethodProcessor(false));
   resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
   resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
   resolvers.add(new RequestHeaderMapMethodArgumentResolver());
   resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
   resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
   resolvers.add(new SessionAttributeMethodArgumentResolver());
   resolvers.add(new RequestAttributeMethodArgumentResolver());

   // Type-based argument resolution
   resolvers.add(new ServletRequestMethodArgumentResolver());
   resolvers.add(new ServletResponseMethodArgumentResolver());
   resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   resolvers.add(new RedirectAttributesMethodArgumentResolver());
   resolvers.add(new ModelMethodProcessor());
   resolvers.add(new MapMethodProcessor());
   resolvers.add(new ErrorsMethodArgumentResolver());
   resolvers.add(new SessionStatusMethodArgumentResolver());
   resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
   if (KotlinDetector.isKotlinPresent()) {
      resolvers.add(new ContinuationHandlerMethodArgumentResolver());
   }

   // Custom arguments
   if (getCustomArgumentResolvers() != null) {
      resolvers.addAll(getCustomArgumentResolvers());
   }

   // Catch-all
   resolvers.add(new PrincipalMethodArgumentResolver());
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
   resolvers.add(new ServletModelAttributeMethodProcessor(true));

   return resolvers;
}
  • ModelAttribute, PathVarialbe, RequestParam, RequestBody 등등 기본적인 ArgumentResovler들이 담겨있습니다.
  • customArgumentResovler 또한 등록이 가능합니다.

RequestMappingHandlerAdapter에서 ArgumentResolver 동작 과정

필요한 부분만 주석으로 설명하겠습니다. 
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); //ArgumentResolver 사용 준비 하기 위해 넣는다. 
      }
      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); // 요청에 대해 동작을 하며, ArgumentResolver동작
      if (asyncManager.isConcurrentHandlingStarted()) {
         return null;
      }

      return getModelAndView(mavContainer, modelFactory, webRequest);
   }
   finally {
      webRequest.requestCompleted();
   }
}

RequestMappingHandlerAdapter -> ServletInvocableHandlerMethod -> InvocableHandlerMethod

InvocableHandlerMethod는 ArgumentResovler를 실행하는 주체입니다. 

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {

   MethodParameter[] parameters = getMethodParameters();
   if (ObjectUtils.isEmpty(parameters)) {
      return EMPTY_ARGS;
   }

   Object[] args = new Object[parameters.length];
   for (int i = 0; i < parameters.length; i++) {
      MethodParameter parameter = parameters[i];
      parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
      args[i] = findProvidedArgument(parameter, providedArgs);
      if (args[i] != null) {
         continue;
      }
      if (!this.resolvers.supportsParameter(parameter)) {
         throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
      }
      try {
         args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
      }
      catch (Exception ex) {
         // Leave stack trace for later, exception may actually be resolved and handled...
         if (logger.isDebugEnabled()) {
            String exMsg = ex.getMessage();
            if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
               logger.debug(formatArgumentError(parameter, exMsg));
            }
         }
         throw ex;
      }
   }
   return args;
}
  • resolvers.supportsParameter(): 요청에 ArgumentResolver 적용 조건을 확인합니다. 
  • 통과한다면, resovlerArgument()를 동작합니다. 그리고 데이터를 가공하여 넘겨줍니다. 

ArgumentResovler의 동작 과정을 알아봤으니 CustomArgumentResolver를 만들어보겠습니다. 

HandlerMethodArgumentResolver

public interface HandlerMethodArgumentResolver {

   boolean supportsParameter(MethodParameter parameter);

   @Nullable
   Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
  • ArgumentResolver를 사용하기 원한다면 HandlerMethodArgumentResolver를 상속받고 구현하면 됩니다.
  • supportsParameter(): resolveArgument를 실행할지 안 할지 결정하는 역할을 하는 메서드입니다. 판별은 parameter의 어노테이션의 유무로 판별합니다.
  • resolveArgument(): supportsParameter값이 true 넘어올 경우 동작합니다. 데이터를 가공하고 return 하면 됩니다.

예시를 보겠습니다. 

로그인을 해서 session이 있는 유저에 한하여 값이 넣어질 객체가 SessionDto이며, @LoginUser의 어노테이션이 있다면 session에 저장된 값을 넣어주는 역할을 합니다.

public class ArgumentResolver implements HandlerMethodArgumentResolver{

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(LoginUser.class); // 어노테이션 유무 판별
        boolean hasOwnerType = SessionDto.class.isAssignableFrom(parameter.getParameterType()); // 값을 넣을 객체가 SessionDto인지 확인

        return hasLoginAnnotation && hasOwnerType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); // 요청을 HttpServletRequest로 바꿔 사용가능하게함
        HttpSession session = request.getSession(false); // Session이 없다면 생성하지 않습니다.
        if(session==null){ // Session이 없다면 Resolver 동작 x
            return null;
        }
        return session.getAttribute(SessionConst.LOGIN_MEMBER); // Session이 있으니 Session에 저장된 값을 SessionDto에 넣어주는 동작을 합니다.
    }
}

정리하며

프로젝트를 만들어보지 않았다면, 굳이 ArgumentResolver를 사용해야 하나 싶을 수 있습니다. 하지만 ArgumentResovler를 사용하지 않는다면, Controller단에서 데이터를 받고 사용할 수 있도록 데이터를 가공하는 코드가 중복으로 들어가게 됩니다. ArgumentResovler를 사용함으로써 Controller단에서 중복된 코드가 없어지며, 값들을 손쉽게 처리할 수 있습니다. 이를 잘 활용하는 것이 코드 가독성과 유지보수에 기여할 수 있기 때문에 사용하는 것을 추천드립니다.