[Spring] Resolver 구현
HandlerMethodArgumentResolver
HandlerMethodArgumentResolver는 Interface로써,
Controller의 Argument(Parameter)에 지정된 변수들을,
Annotation이나 객체의 Type에 따라서 Resolver를 먼저 거쳐,
실제 Data를 Controller에 넘겨주는 역할을 수행한다.
Controller에 들어오는 Argument(Parameter)를 가공(암호화 > 복호화) 하거나,
Argument(Parameter)를 추가하거나 수정해야 하는 경우에 사용한다.
실제 해당 Interface의 형태는 아래와 같다.
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given request.
*
* @author Arjen Poutsma
* @since 3.1
* @see HandlerMethodReturnValueHandler
*/
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request.
* A {@link ModelAndViewContainer} provides access to the model for the
* request. A {@link WebDataBinderFactory} provides a way to create
* a {@link WebDataBinder} instance when needed for data binding and
* type conversion purposes.
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param mavContainer the ModelAndViewContainer for the current request
* @param webRequest the current request
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return the resolved argument value, or {@code null}
* @throws Exception in case of errors with the preparation of argument values
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
HandlerMethodArgumentResolver 사용 이유
HandlerMethodArgumentResolver를 사용하는 이유는,
매개변수로 사용되는 인자에 대해 공통적으로 처리해야할 로직 등이 있을 경우,
중복 코드를 줄이고 공통 기능으로 추출하여 사용할 수 있다.
동작 방식
Spring에서 Resolver의 동작은 아래와 같은 과정으로 이루어진다.
- Client Request 요청
- Dispatcher Servlet에서 해당 요청 처리
- Client Request에 대한 Handler Mapping
3.1 RequestMapping에 대한 매칭 (RequestMappingHandlerAdapter가 수행)
3.2 Interceptor 처리
3.3 Argument Resolver 처리 <-- Argument Resolver 실행 지점
3.4 Message Converter 처리 - Controller Method invoke
정리하자면 특정 Request가 Handler로 Mapping되는 과정에서 invoke 되기 전,
Interceptor > Resolver > MessageConverter 순으로 처리된 후,
Controller의 Method가 invoke 된다.
Resolver 구현
먼저 HandlerMethodArgumentResolver를 implemnets하는 Class를 생성한다.
이때 구현해야 하는 method는 supportsParameter, resolveArgument 두 가지이다.
public boolean supportsParameter(MethodParameter parameter)
Parameter가 해당 Resolver에 의해 수행 되는 Type인지 체크하여 boolean을 return한다.
true로 return될 경우 resolveArgument method를 실행한다.
이때 Type 체크를 위해 Class를 .isAssignableFrom()을 이용해 비교 할 수도,
Annotation을 새로 생성하여 체크할 수도 있다.
.isAssignalbeFrom() 이용 Prameter 객체 Class 타입 체크
아래와 같이 Parameter에서 Binding 되길 원하는 객체의 Class Type을 .isAssignableFrom()을 이용해 구현하면,
isAssignableFrom이 궁금하다면 클릭
@Override
public boolean supportsParameter(MethodParameter parameter) {
return ResultJwt.class.isAssignableFrom(parameter.getParameterType());
}
아래에서 처럼 Handler에서 해당 Parameter 객체 Class 타입을 사용하는 Parameter가 Handelr의 처리 지점이 된다.
@GetMapping("")
public ResponseEntity<Contents<Board>> getNotices(ResultJwt resultJwt,
@RequestParam(required = false) String category, CommonParameter<NoticeType> parameter) {
String platformId = (String) resultJwt.getClaims().get(JwtClaims.PLATFORMID.getClaim());
Annotation 생성
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResultJWT {
}
사용된 Annotation들의 속성은 아래와 같다.
- @interface : 해당 파일을 Annotation Class로 지정, @ResultJWT 라는 Annotation이 생성됨
- @Target(ElementType.PARAMETER) : 해당 Annotation이 생성될 위치 지정,
PARAMETER로 지정 시 Method의 Parameter에서만 사용 가능 - @Retention(RetentionPolicy.RUNTIME) : Annotation 유지 정책을 설정,
RUNTIME은 Byte Code File까지 Annotation 정보를 유지,
reflection을 이용 Runtime시에 해당 Annotation 정보를 획득.
reflection : 구체적인 Class Type을 알지 못해도, 그 Class의 method, type, field들에 접근할 수 있도록 해주는 Java API
@GetMapping("")
public ResponseEntity<Contents<Board>> getNotices(@ResultJWT ResultJwt resultJwt,
@RequestParam(required = false) String category, CommonParameter<NoticeType> parameter) {
String platformId = (String) resultJwt.getClaims().get(JwtClaims.PLATFORMID.getClaim());
위와 같이 @ResultJWT Annotation 적용 후 supportsParameter()를 아래와 같이 구현 시,
해당 Parameter가 Resolver의 처리 지점이 된다.
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isResultJwtAnnotation = parameter.getParameterAnnotation(ResultJWT.class) != null;
boolean isResultJwtClass = ResultJwt.class.equals(parameter.getParameterType());
return isResultJwtAnnotation && isResultJwtClass;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception
실제 Parameter와 Binding하여 return할 Object를 생성하는 method이다.
NativeWebRequest Object에 접근하여 Client Request의 Parameter를 Controller 보다 우선적으로 받아 작업할 수 있다.
해당 Handler method안 Parameter에서 Binding을 원하는 객체 Type에 맞게 return해주면 된다.
public class ResultJwtArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private AuthService authService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return ResultJwt.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// return type은 본인이 Binding을 원하는 객체 Class
// supportsParameter에서 검증한 ResultJwt.class
return authService.getResultJwt(webRequest.getHeader("Authorization"));
}
}
위 두 가지 method를 구현하는 Class를 생성해준 후,
servlet-context.xml에 등록하거나, Configuration을 이용해 Resolver를 등록해주면 된다.
servlet-context.xml 등록
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="project.config.resolver.ResultJwtArgumentResolver"></bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
Configuration 이용
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final ResultJwtArgumentResolver resultJwtArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(resultJwtArgumentResolver);
}
}
실제 적용
실제 본인의 경우는 아래와 같이 @RequestHeader로 Header에서 JWT 토큰을 가져와 Claims를 추출하는 공통 로직 부분을
@GetMapping("")
public ResponseEntity<Contents<Board>> getNotices(@RequestHeader(value = "Authorization") String jwt,
@RequestParam(required = false) String category, CommonParameter<NoticeType> parameter) {
ResultJwt resultJwt = authService.getResultJwt(jwt);
String platformId = (String) resultJwt.getClaims().get(JwtClaims.PLATFORMID.getClaim());
간단한 Resolver를 구현하여 공통 로직을 추출하였고,
public class ResultJwtArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private AuthService authService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return ResultJwt.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return authService.getResultJwt(webRequest.getHeader("Authorization"));
}
}
아래와 같이 효과적으로 바로 Handler에서 매개변수로 사용할 수 있게 적용해 사용 중이다.
@GetMapping("")
public ResponseEntity<Contents<Board>> getNotices(ResultJwt resultJwt,
@RequestParam(required = false) String category, CommonParameter<NoticeType> parameter) {
String platformId = (String) resultJwt.getClaims().get(JwtClaims.PLATFORMID.getClaim(