데브코스 강의 실습을 따라 하던 와중에 다음과 같은 코드가 있었다.
@PostMapping("/customers/new")
public String addNewCustomer(CreateCustomerRequest createCustomerRequest) {...}
서블릿으로 개발할 때는 항상 HttpServletRequest로 request 객체를 받고 필요한 파라미터를 꺼내서 썼는데, 스프링에서는 어떻게 CreateCustomerRequest처럼 개발자가 원하는 타입으로 받을 수 있을까? 디버깅을 통해 비밀을 파헤쳐보자.
컨트롤러의 메소드는 누가 호출할까?
Spring MVC의 흐름을 보면 Controller는 HandlerAdapter가 호출한다. HandlerAdapter를 들여다보자.
public interface HandlerAdapter {
boolean supports(Object var1);
@Nullable
ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;
long getLastModified(HttpServletRequest var1, Object var2);
}
HandlerAdapter는 supports() 메소드로 handler를 처리할 수 있는지 확인하고, handle() 메소드로 handler를 실행한다.
여기서 handle() 메소드를 보면 HttpServletRequest 객체를 받는다. 그렇다면, handle() 메소드가 동작하는 과정에서 컨트롤러가 원하는 파라미터로 변환한다고 추측할 수 있다.
그럼, HandlerAdatper를 구현하고 애노테이션 기반의 컨트롤러인 @RequestMapping을 처리하는 RequestMappingHandlerAdapter를 살펴보자.
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
...
private HandlerMethodArgumentResolverComposite argumentResolvers;
...
}
RequestMappingHandlerAdapter는 필드로 HandlerMethodArgumentResolverComposite 타입의 argumenetResolvers를 가지고 있다. 이름부터 '매개변수를 해결해주는 애' 라는 느낌이 팍 온다. 비밀은 여기에 있다.
HandlerMethodArgumentResolverComposite는 이름만 봐도 예측할 수 있듯이 HandlerMethodArgumentResolver 들을 List로 가지고 있다. 바로 이 HandlerMethodArgumentResolver가 컨트롤러 메서드에서 특정 조건에 맞는 매개변수가 있을 때 원하는 값을 바인딩해주는 인터페이스이다.
public void afterPropertiesSet() {
this.initControllerAdviceCache();
List handlers;
if (this.argumentResolvers == null) {
handlers = this.getDefaultArgumentResolvers();
this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
}
...
위 코드는 RequestMappingHandlerAdapter가 생성될 때 초기화 시키는 메소드이다. 이때 getDefaultArgumentResolvers()가 호출되고 그 결과를 argumentResolvers에 add해주고 있다.
그럼 getDefaultArgumentResolvers() 메소드를 살펴보자.
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList(30);
resolvers.add(new RequestParamMethodArgumentResolver(this.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(this.getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(this.getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(this.getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(this.getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(this.getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(this.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());
}
if (this.getCustomArgumentResolvers() != null) {
resolvers.addAll(this.getCustomArgumentResolvers());
}
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
getDefaultArgumentResolvers() 에서는 List에 Spring이 기본적으로 제공하는 ArgumentResolver들을 추가해주고 있다. 여기서 우리가 자주 쓰는 @RequestParam , @PathVariable, @ModelAttribute 를 처리해주는 Resolver들도 추가되는 것이다.
→ 아하! 매개변수 타입마다 바인딩해주는 ArgumentResolver들이 있고, RequestMappingHandlerAdapter 클래스에서는 이 Resolver들을 가지고 있구나. 라고 이해하면 된다.
그럼, 요청이 들어왔을 때의 메소드 호출 흐름을 살펴보자. 이 부분은 가볍게 요청이 전달되는 흐름만 보자.
먼저, RequestMappingHandlerAdapter에서는 ServletInvocableHandlerMethod클래스의 invokeHandlerMethod() 메소드를 호출한다.
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
...
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
...
}
invokeAndHandle() 에서 다시 부모 클래스인 InvocableHandlerMethod의 invokeForRequest() 를 호출한다.
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
...
}
invokeForRequest() 에서는 getMethodArgumentValues() 를 호출한다.
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
...
return this.doInvoke(args);
}
!! 여기서부터 집중해서 살펴보자.
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
// 이 배열에 결과 파라미터들을 담는다.
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) {
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// resolvers에서 메소드 호출을 통해 값을 바인딩한다.
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
...
return args;
}
}
위 코드에서 this.resolvers가 RequestMappingHandlerAdapter의 필드로 있던 HandlerMethodArgumentResolverComposite 타입의 argumentResolvers 이다. 반복문 안에서 resolveArgument() 메소드 호출을 통해 args[i]에 변환된 parameter를 binding 한다.
그럼 HandlerMethodArgumentResolverComposite를 살펴보자.
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
...
public boolean supportsParameter(MethodParameter parameter) {
return this.getArgumentResolver(parameter) != null;
}
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
}
return result;
}
}
getArgumentResolver()를 보자. ArgumentResolver 리스트에서 하나씩 꺼내고 supportsParameter()로 지원하는 parameter 타입인지 확인을 하고 지원한다면 해당 argumentResolver를 반환한다. resolveArgument()에서는 반환된 resolver의 resolveArgument()를 호출해 타입에 맞게 파라미터를 바인딩해준다. 타입별로 파라미터를 바인딩하는 구체적인 동작은 ArgumentResolver마다 다르므로 전체적인 과정은 여기서 마무리하고 한 가지 예만 살펴보자.
@ModelAttribute 타입의 바인딩
@ModelAttribute 타입은 어떻게 처리하는지는 간단히 개념만 정리해보자.
@GetMapping("/hello")
public String hello(@ModelAttribute HelloData helloDate){...}
HandlerMethodArgumentResolver를 구현한 ModelAttributeMethodProcessor 클래스는 @ModelAttribute 애노테이션을 처리한다. 따라서, 매개변수로@ModelAttribute 타입이 있으면 다음을 실행한다.
- 디폴트 생성자로 HelloData 객체를 생성한다.
- 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
- 예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다
- 생성과 바인딩이 끝난 객체를 반환한다.
이런식으로 다양한 ArgumentResolver들은 자신이 처리할 수 있는 매개변수 타입에 맞게 parameter를 바인딩 해준다.
어떤 타입들을 Spring이 기본적으로 처리 해주는지 궁금하다면 https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments 여기서 확인할 수 있다.
참고로 일치하는 타입이 없을 경우, int나 String 처럼 단순형이면 @RequestParam, 그렇지 않을 경우 @ModelAttribute 로 처리한다.
정리
전체 흐름은 다음과 같다.
중요한 부분만 간략하게 정리 해보자.
- RequestMappingHandlerAdapter에는 HandlerMethodArgumentResolverComposite 를 필드로 갖고 있다.
- HandlerMethodArgumentResolverCompsoite는 특정 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩해주는 ArgumentResolver들을 List로 가지고 있다.
- getArgumentResolver() 메소드에서 루프를 돌면서 컨트롤러의 메소드 파리미터를 바인딩 해줄 수 있는 HandlerMethodArgumentResolver를 찾는다.
- 찾은 HandlerMethodArgumentResolver의 resolveArgument()를 호출해 파라미터를 바인딩하고 결과를 반환한다.
ArgumentResolver를 공부하면서 스프링이 내부적으로 정말 많은 일을 해준다는 걸 다시 한 번 느꼈다. 클라이언트가 편하게 서비스를 이용하기 위해 개발자가 힘든 일을 하는 것처럼, 개발자가 편하게 개발하게끔 프레임워크가 힘든 일을 대신 해주는 것 같다.
스프링 내부를 디버깅해본 적이 처음이라 그런지, 요청 흐름을 파악하고 이해하는데 시간이 엄청 많이 걸렸다. 이해하기 쉽진 않았지만 그래도 디버깅하는게 나름 재미있었다. 다음에는 Custom Argument Resolver를 만들어서 등록하고 사용해보는 글을 작성 해봐야겠다.
https://velog.io/@kingcjy/Spring-HandlerMethodArgumentResolver의-사용법과-동작원리
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments
https://www.inflearn.com/course/스프링-mvc-1/dashboard
'Spring' 카테고리의 다른 글
AOP가 동작하는 원리 (0) | 2021.11.11 |
---|---|
Spring에서의 예외 처리 및 에러 페이지 (1) | 2021.09.16 |