Spring MVC의 ArgumentResolver 파헤치기

궁금한 점

스프링 MVC로 컨트롤러 코드를 작성하다보면 다음과 같이 컨트롤러 메서드의 파라미터에 다양한 값을 받을 수 있음을 알게 된다.

1
2
3
4
5
6
7
@RestController
public class SomeClass {
@GetMapping("/some")
public ResponseEntity<?> getSome(final Pageable pageable, @RequestBody final String body) {
return ResponseEntity.noContent().build();
}
}

여기서 파리미터 매핑을 스프링에서 해준다!
하지만 어떤 경우에는 어노테이션 (@RequestBody, @PathVariable)을 넣어줘야 되는 경우도 있고, 어떤 경우에는 어노테이션을 생략해도 된다.(@ModelAttribute, @RequestParam) 심지어 어느 경우는 어노테이션이 없는 경우도 있다.(Pageable)

이번 기회에 날잡아서 스프링에서 어떻게 파라미터에 값을 넣어주는지, 어떤 경우에 어노테이션이 필요한 지 살펴보자.

디스패처 서블릿 부터 시작한다

스프링 MVC는 프론트 컨트롤러 패턴을 사용한다. 요청을 처리하는 과정에서 중복되는 과정을 프론트 컨트롤러에서 모아서 처리한다.

우리가 궁금해하는 컨트롤러 메서드의 파라미터 처리도 디스패처 서블릿과 관련된 어디에선가 처리할 것이다!

doService

디스패처 서블릿은 doService라는 메서드를 통해 요청을 처리한다. doServicedoDispatch 메서드로 요청 처리를 넘긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DispatcherServlet extends FrameworkServlet {

// 생략
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);

// 생략...

try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
if (this.parseRequestPath) {
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
}

doDispatch

doDispatcher는 핸들러에게 요청을 처리하도록 한다. 정확하게 말하면 ha.handle(processedRequest, response, mappedHandler.getHandler());를 통해 핸들러 어댑터를 통해 핸들러에게 요청을 처리하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class DispatcherServlet extends FrameworkServlet {

// 생략

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
}

Handler와 HandlerAdapter

DispatcherServletha.handle(processedRequest, response, mappedHandler.getHandler()); 코드가 핸들러 어댑터를 활용해서 핸들러에게 요청을 처리하도록 한다.

여기서 mappedHandlerHandlerExecutionChain이라는 객체다. HandlerExecutionChain은 핸들러와 같이 실행되는 인터셉터들을 가지고 있다.
HandlerMapping 인터페이스는 getHandler(HttpServletRequest request)를 통해 해당 요청을 처리해야하는 핸들러와 적용되야 하는 인터셉터를 포함한 HandlerExecutionChain을 반환한다.

여기서 HandlerExecutionChain은 핸들러를 Object로 저장하고 있다. 즉 요청을 처리할 핸들러가 어떤 메서드를 통해 요청을 처리할 줄 모른다는 뜻이다.

1
2
3
4
5
6
7
8
9
10
11
12
public class HandlerExecutionChain {

private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);

private final Object handler;

private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

private int interceptorIndex = -1;

// 생략
}

결국 Object로 핸들러를 전달받으면 어떤 메서드를 호출해야 할 지 알 수 없다. 특히 스프링에서는 다양한 종류의 핸들러가 존재해서 하나의 타입으로 캐스팅 할 수도 없다.
이래서 HandlerAdapter가 존재한다. HandlerAdapter는 전달받은 핸들러가 어떤 객체이든 해당 핸들러를 호출할 수 있는 방법을 추상화한 인터페이스이다!!!

RequestMappingHandlerAdapter에서 ArgumentResolver를 관리한다.

그래서 ha.handle(processedRequest, response, mappedHandler.getHandler());를 디버깅을 해보면 AbstractHandlerMethodAdapterhandle메서드를 호출한다.

1
2
3
4
5
6
7
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

return handleInternal(request, response, (HandlerMethod) handler);
}

여기서 handlerInternal메서드는 AbstractHandlerMethodAdapter를 상속한 RequestMappingHandlerAdapter의 메서드가 호출된다.

RequestMappingHandelrAdapter에 우리가 그렇게 찾던 HandlerMethodArgumentResolver를 관리하고 있다!!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {

// 생략...

@Nullable
private List<HandlerMethodArgumentResolver> customArgumentResolvers;

@Nullable
private HandlerMethodArgumentResolverComposite argumentResolvers;

@Nullable
private HandlerMethodArgumentResolverComposite initBinderArgumentResolvers;

@Nullable
private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;

@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;

@Nullable
private List<ModelAndViewResolver> modelAndViewResolvers;

private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();

private final List<Object> requestResponseBodyAdvice = new ArrayList<>();
//생략...
}

RequestMappingHandlerAdapter에서는 argumentResolvercustomArgumentResolvers를 가지고 있다.

argumentResolvergetDefaultArgumentResolvers메서드를 통해 초기화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}

여기서 보면 어노테이션을 붙여야 하는 리졸버 -> 타입에 맞춰서 해주는 리졸버 -> 커스텀 리졸버(Pageable리졸버가 해당) -> 그외 모든 대상을 리졸브 대상으로 하는 리졸버 순으로 등록된다!

다시 handlerInternal 메서드로 돌아오면 결국 invokeHandlerMethod를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;
checkRequest(request);

// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}

// 생략...

return mav;
}

invokeHandlerMethodServletInvocableHandlerMethod를 만들어서 argumentResolver를 세팅해서 invokeAndHandle한다!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

// 생략

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

return getModelAndView(mavContainer, modelFactory, webRequest);
}

결론

@RequestMapping으로 매핑된 핸들러(메서드)는 RequestMappingHandlerAdapter에서 처리된다!
RequestMappingHandlerAdapter에서 핸들러의 인자를 리졸브하는 HandlerMethodArgumentResolver들을 관리한다!
RequestMappingHandlerAdapter에서는 정해진 우선순위 (어노테이션이 필요한 리졸버 - 타입으로 리졸브하는 리졸버 - 커스텀 리졸버 - 모든 것을 리졸브하려는 리졸버)로 인자를 리졸브한다!
@PathVariable은 어노테이션이 필요한 리졸버이다.
ServletRequestServletResponse는 타입 기반으로 리졸브하는 리졸버이다. (그래서 인자에 다른 매개변수가 필요없다!)
Pageable은 커스텀 리졸버이다!! (Spring Data에서 제공하는 리졸버이다!)
@ModelAttribute@RequestParam 은 어노테이션이 있어도 작동하고, 없어도 작동하는 리졸버이다!!! (가장 우선순위가 낮은 리졸버들이다.)

Share