스프링 HandlerMethodArgumentResolver 컴포넌트
1. HandlerMethodArgumentResolver Interface
공식 문서를 보면 다음과 같은 설명을 볼 수 있다.
Strategy interface for resolving method parameters into argument values in the context of a given request.
스프링 서버 애플리케이션이 HTTP 요청을 처리하는 엔드포인트(endpoint) 메서드의 매개변수에 원하는 값을 매핑할 수 있는 컴포넌트의 인터페이스다. 자주 사용하는 @RequestBody, @RequestParam, @PathVariable 같은 애너테이션들도 각 HandlerMethodArgumentResolver 구현체들을 통해 값들이 매칭된다. 인터페이스의 책임을 살펴보자.
- supportsParameter 메서드
- 자신이 지원하는 메서드 파라미터 타입인지 확인한다.
- resolveArgument 메서드
- 메서드 파라미터 인수에 삽입할 값을 추출한다.
package org.springframework.web.method.support;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1,
@Nullable ModelAndViewContainer var2,
NativeWebRequest var3,
@Nullable WebDataBinderFactory var4) throws Exception;
}
2. When is HandlerMethodArgumentResolver instance invoked?
HandlerMethodArgumentResolver 인스턴스는 어느 시점에 호출될까? 사용자 요청이 들어오면 스프링 애플리케이션은 아래 이미지와 같은 실행 흐름을 갖는다.
- 사용자가 요청을 보낸다.
- 필터 체인을 통과 후 DispatcherServlet 인스턴스로 진입한다.
- DispatcherServlet 인스턴스는 해당 요청을 수행할 HandlerMethod 인스턴스를 결정한다.
- DispatcherServlet 인스턴스는 RequestMappingHandlerAdapter 인스턴스에게 HandlerMethod 인스턴스를 전달한다.
- RequestMappingHandlerAdapter 인스턴스는 HandlerMethod 인스턴스에게 필요한 인수 값을 준비하도록 요청한다.
- HandlerMethod 인스턴스는 자신에게 등록된
리졸버(resolver)들을 통해 필요한 메서드 인수 값을 셋팅한다.
- HandlerMethod 인스턴스는 자신에게 등록된
- HandlerMethod 인스턴스는 자신과 연결된 컨트롤러의 메서드를 호출(invoke)한다.
위에서 설명한 5번 과정을 자세히 들여다보자. InvocableHandlerMethod 클래스의 getMethodArgumentValues 메서드를 살펴보면 HandlerMethodArgumentResolver 인스턴스들이 호출되는 코드를 찾을 수 있다. 각 HandlerMethodArgumentResolver 인스턴스가 지원하는 파라미터가 사용되는 엔드포인트 메서드로 연결되는 경우 HTTP 요청 정보로부터 쿼리 파라미터(혹은 요청 메시지)로부터 필요한 값을 추출한다.
- supportsParameter 메서드
- HandlerMethodArgumentResolver 객체에게 해당 파라미터를 지원하는지 확인한다.
- resolveArgument 메서드
- 해당 파라미터를 지원하는 경우 요청 정보로부터 필요한 값을 추출한다.
public class InvocableHandlerMethod extends HandlerMethod {
// ...
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 {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
}
3. Example
예제 코드를 살펴보자. 쿼리 파라미터로부터 날짜 정보를 추출해서 LocalDate 객체로 변경하는 리졸버 객체를 구현한다.
- supportsParameter 메서드
- 컨트롤러 엔드포인트 메서드의 파라미터가 LocalDate 타입인지 확인한다.
- resolveArgument 메서드
- HTTP 요청에서 엔드포인트 메서드의 파라미터 이름을 가진 쿼리가 있는지 확인한다.
- 값이 존재하는 경우에만
yyyy-MM-dd포맷으로 파싱(parsing)한다. - 값이 없는 경우
null을 반환한다.
package blog.in.action.handler;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
@Component
public class LocalDateHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameter().getType().equals(LocalDate.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
String parameterName = methodParameter.getParameterName();
assert parameterName != null;
Optional<String> queryDate = Optional.ofNullable(nativeWebRequest.getParameter(parameterName));
return queryDate
.filter(StringUtils::hasText)
.map((stringDate) -> LocalDate.parse(stringDate, DateTimeFormatter.ISO_LOCAL_DATE))
.orElse(null);
}
}
리졸버를 구현했으면 스프링 컨텍스트에 등록한다. 설정 객체를 만들고 위에서 만든 HandlerMethodArgumentResolver 인스턴스를 등록한다.
- WebMvcConfigurer 인터페이스를 구현하여 addArgumentResolvers 메서드를 재구현한다.
package blog.in.action.config;
import blog.in.action.handler.LocalDateHandlerMethodArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LocalDateHandlerMethodArgumentResolver customLocalDateResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(customLocalDateResolver);
}
}
다음과 같은 엔드포인트를 갖는 컨트롤러 객체를 만든다.
package blog.in.action.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/users")
public List<String> searchByJoinDate(LocalDate beginDate, LocalDate endDate) {
return Arrays.asList(
String.format("user1 %s", beginDate),
String.format("user2 %s", endDate)
);
}
}
위에서 구현한 코드가 잘 동작하는지 검증해보자. 서비스를 실행하고 아래와 같은 cURL 요청을 보내면 쿼리 파라미터에서 파싱된 데이터가 사용된 응답을 받을 수 있다.
$ curl "http://localhost:8080/api/users?beginDate=2021-01-11&endDate=2021-02-01" | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 39 0 39 0 0 19519 0 --:--:-- --:--:-- --:--:-- 39000
[
"user1 2021-01-11",
"user2 2021-02-01"
]
댓글남기기