Spring MVC | @Valid는 어떻게 동작하는거야? (feat. DispatcherServlet)
@Valid
Spring Boot에서 @Valid를 @RequestBody 객체에 붙이게 되면, 이때 스프링은 Hibernate Validator(javax.validation 패키지에 있는 JSR-303/JSR-380인 BeanValidation을 활용함)를 사용하게 됩니다. Spring MVC에서는 Validation이 어떻게 동작하는지 MVC 동작원리와 함께 살펴보겠습니다.
Dispatcher Servlet
아래는 Spring MVC의 동작구조를 도식화한 것입니다. 하나하나 차근히 살펴보겠습니다.
- 클라이언트 요청을 제일 먼저 받는 디스패처 서블릿(프론트 컨트롤러)입니다. DispatcherServlet의 HandlerMapping 전략을 통해 클라이언트의 요청을 처리해 줄 Controller를 찾습니다.
- HandlerMapping 전략은 HTTP Method, URL, Argument 등의 정보들을 이용합니다.
- 적절한 HandlerAdapter를 찾았다면, DispatcherServlet는 요청을 위임하게 됩니다.
- 여러 Adapter가 존재하지만, @RequestMapping을 통해 컨트롤러를 빈으로 등록했다면 RequestMappingHandlerAdapter가 위임받을 것 입니다.
- DispatcherServlet은 HandlerAdapter 하나 이상의(없으면 바로 컨트롤러 실행) HanderInterceptor를 거쳐서 컨트롤러가 실행되게 끔 구성되어 있습니다. (아래 사진 참조)
- HandlerInterceptor 인터페이스는 다음 3개의 메서드를 가지고 있습니다.
- preHandle: 컨트롤러를 호출하기 전에 실행됩니다. 이 단계에서 RequestBody 등 Arguments의 Validation을 수행하고 검증이 실패했다면 false를 리턴합니다.
- preHandler에서 false가 리턴되면 컨트롤러에게 요청이 도달하지 않습니다.
- postHandle: Controller 호출한 다음 실행됩니다. ModelView 객체를 가지고 있습니다. 뷰로 넘기기 전에 ModelView 객체를 핸들링할 수 있을 것 같습니다.
- afterCompletion: 실행 중 발생한 예외 객체를 가지고 있습니다.
- preHandle: 컨트롤러를 호출하기 전에 실행됩니다. 이 단계에서 RequestBody 등 Arguments의 Validation을 수행하고 검증이 실패했다면 false를 리턴합니다.
실제로 어떻게 HandlerAdapter가 Validation을 실행하는지에 대해서 코드를 통해 들어가면서 살펴보겠습니다.
RequestMappingHandlerAdapter
실제 컨트롤러를 호출하는 HandlerAdapter입니다. API 문서를 참조하면 멤버 변수로 HandlerMethodArgumentResolverComposite를 가지고 있습니다. 아마 이 MethodArgumentResolver를 통해 Validation이 실행될 것으로 예상됩니다.
HandlerMethodArgumentResolverComposite
HandlerMethodArgumentResolver의 구현체입니다. paramter를 통해 적절한 ArgumentResolver를 찾고 해당 리졸버를 호출하고 있습니다.
✨ RequestResponseBodyMethodProcessor
💡 Validation이 어떻게 동작하는지 확인하고 싶다면, RequestResponseBodyMethodProcessor의
resolveArgument 메소드를 breakPoint로 설정하여 확인해보시면 됩니다.
얘가 바로 이번 포스팅의 주범입니다. HandlerMethodArgumentResolver 구현체인 RequestResponseBodyMethodProcessor API 문서 설명을 번역하면 다음과 같습니다.
요청 또는 응답의 본문을 읽고 HttpMessageConverter로 작성하여 @RequestBody로 어노테이션 된 메서드 인수를 확인하고 @ResponseBody로 어노테이션된 메서드의 반환값을 처리합니다.
유효성 검사를 트리거하는 어노테이션(ex. @Valid..)이 있는 경우 @RequestBody 메서드 인수도 유효성 검사를 거칩니다. 유효성 검사에 실패할 경우 MethodArgumentNotValidException이 발생하고 DefaultHandlerExceptionResolver가 구성된 경우 HTTP 400 응답 상태 코드가 발생합니다.
네! 바로 이 녀석이 RequestBody를 유효성 검사를 하고, 실패하면 MethodArgumentNotValidException 예외를 발생시키고 있었습니다.
validateIfApplicable을 breakpoint로 잡고 디버깅을 해보면, binder의 bindingResult를 까보면 내부에 에러가 포함되어 있습니다.
최종적으로 binder의 BindingResult를 가지고 MethodArgumentNotValidException을 던지게 됩니다.
Outro
Spring Validation을 사용하면서 컨트롤러 메서드에 breakpoint를 걸고 왜 안 걸리지? 했던 경험이 있는데요,, 이번 시간에 내부 동작 원리를 알아보면서 Validation이 실패하면 왜 컨트롤러 메서드까지 요청이 도달하지 않는지, 그리고 스프링은 내부적으로 어떻게 요청이 흐르는지에 대해서 알게 되었던 시간이었습니다. 아직 봐야 할 부분들이 산더미지만 차근차근 배워나가도록 하겠습니다. 글 읽어주셔서 감사합니다.
REFERENCES
https://product.kyobobook.co.kr/detail/S000000935360 - 토비의 스프링 3.1
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/special-bean-types.html
https://www.linkedin.com/pulse/introduction-interceptor-spring-mvc-aneshka-goyal/