Validation은 @ModelAttribute 혹은 @RequestBody의 ArgumentResolver가 동작한 후, 검증을 진행합니다. 밑에서 알아보겠습니다. 

ArgumentResolver의 자세한 동작과정은 아래 링크에서 확인 부탁드립니다. 

https://dingdingmin-back-end-developer.tistory.com/entry/Springboot-MVC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B08-ModelAttribute-RequestParam-PathVariable-%EB%8F%99%EC%9E%91-%EA%B3%BC%EC%A0%95

 

Springboot MVC 파헤치기(8) @ModelAttribute, @RequestParam, @PathVariable @ResponseBody @RequestBody 동작 과정

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

dingdingmin-back-end-developer.tistory.com

1. RequestMappingHandlerAdapter

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

 

@RequestMapping으로 찾은 핸들러(Controller)를 Adapter를 이용해서 실행합니다. 해당 invokeAndHandle은  ServletInvocableHandlerMethod의 메서드입니다. 

 

2. ServletInvocableHandlerMethod

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {

   Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
   if (logger.isTraceEnabled()) {
      logger.trace("Arguments: " + Arrays.toString(args));
   }
   return doInvoke(args);
}

 

해당 요청을 실행하기 위해 invokeForRequest를 실행합니다. getMethodArgumentValues를 통해서 결과 값을 반환받기 위해서 ArgumentResolver를 동작을 준비합니다. 

 

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

 

resolvers.resolveArgument()를 통해서 ArgumentResolver가 동작하게 됩니다. 여기서 데이터 바인딩 Factory 가 넘어가게 되고, 여기에는 BeanValidation과 Validator가 담겨있습니다.

resolvers는 HandlerMethodArgumentResolverComposite로서 모든 ArgumentResolver를 가지고 있고, Resolver를 수행할 수 없는 요청이라면 예외를 발생시킵니다. 

 

아래의 경우에 따라 다른 ArgumentResolver가 동작합니다. 

@ModelAttribute -> ModelAttributeMethodProcessor

@RequestBody -> RequestResponseBodyMethodProcessor

 

@ModelAttribute의 과정을 예로 보겠습니다.

 

3. ModelAttributeMethodProcessor

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

   Object attribute = null;
   BindingResult bindingResult = null;

   if (mavContainer.containsAttribute(name)) {
      attribute = mavContainer.getModel().get(name);
   }
   else {
      // Create attribute instance
      try {
         attribute = createAttribute(name, parameter, binderFactory, webRequest);
      }
      catch (BindException ex) {
         if (isBindExceptionRequired(parameter)) {
            // No BindingResult parameter -> fail with BindException
            throw ex;
         }
         // Otherwise, expose null/empty value and associated BindingResult
         if (parameter.getParameterType() == Optional.class) {
            attribute = Optional.empty();
         }
         else {
            attribute = ex.getTarget();
         }
         bindingResult = ex.getBindingResult();
      }
   }

   if (bindingResult == null) {
      // Bean property binding and validation;
      // skipped in case of binding failure on construction.
      WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
      if (binder.getTarget() != null) {
         if (!mavContainer.isBindingDisabled(name)) {
            bindRequestParameters(binder, webRequest);
         }
         validateIfApplicable(binder, parameter);
         if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new BindException(binder.getBindingResult());
         }
      }
      // Value type adaptation, also covering java.util.Optional
      if (!parameter.getParameterType().isInstance(attribute)) {
         attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
      }
      bindingResult = binder.getBindingResult();
   }

   // Add resolved attribute and BindingResult at the end of the model
   Map<String, Object> bindingResultModel = bindingResult.getModel();
   mavContainer.removeAttributes(bindingResultModel);
   mavContainer.addAllAttributes(bindingResultModel);

   return attribute;
}

protected Object createAttribute(String attributeName, MethodParameter parameter,
      WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {

   MethodParameter nestedParameter = parameter.nestedIfOptional();
   Class<?> clazz = nestedParameter.getNestedParameterType();

   Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
   Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
   if (parameter != nestedParameter) {
      attribute = Optional.of(attribute);
   }
   return attribute;
}

 

createAttribute를 통해서 binding을 시도합니다. 여기서 잡는 예외는

1. 매개변수가 있는 생성자가 있는 경우 

2. 기본 생성자 + setter가 있는 경우

1, 2번 중 하나라도 있으면 예외를 발생시키지 않지만 없을 경우 처리할 수 없으므로 예외를 발생시킵니다. 

 

if (bindingResult == null) {
   // Bean property binding and validation;
   // skipped in case of binding failure on construction.
   WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
   if (binder.getTarget() != null) {
      if (!mavContainer.isBindingDisabled(name)) {
         bindRequestParameters(binder, webRequest);
      }
      validateIfApplicable(binder, parameter);
      if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
         throw new BindException(binder.getBindingResult());
      }
   }
   // Value type adaptation, also covering java.util.Optional
   if (!parameter.getParameterType().isInstance(attribute)) {
      attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
   }
   bindingResult = binder.getBindingResult();
}

 

여기서 binderFactory가 동작하여, BeanValidation 혹은 Validator로 설정해놓은 유효성 검증을 수행하게 됩니다.  

만약 유효성 검증에서 실패하여 예외가 발생할 경우 위에서 볼 수 있듯이 BindingResult에 담기게 됩니다. 


정리하기

유효성 검증 Validation은 ArgumentResolver가 동작하는 과정에서 수행되게 되며, @ModelAttribte, @RequestBody모두 다른 ArgumentResolver가 동작하게 되지만, binding과정에서 발생하는 예외는 BindingResult에 담습니다.