HandlerMethodArgumentResolver는 단어의 뜻대로, 어떠한 메소드의 파라미터로 선언된 것에 대한 처리를 담당하는 클래스이다.
특히 자주 사용하는 곳은, 일반적인 GET
요청 처럼 어떠한 주소로 파라미터가 같이 전달되어 지는 경우, POST
요청으로 백엔드에서 사용하는 객체 데이터를 전달하였을 때 사용한다.
말 그대로 서버로 전달된 데이터를 파라미터로 전달하기 전 가로채, 특정 조건이 맞는지 확인하고, 조건이 맞다면 메소드에서 요구하는 파라미터로 변환시켜 보내주는 역할을 한다.
그렇다면 HandlerMethodArgumentResolver는 어떻게 생겼을까?
Resolver가 선언된 클래스를 보는것이 좋을 것 같다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
HandlerMethodArgumentResovler는 인터페이스로 선언이 되어있으며 멤버 메소드로
아래의 두 메소드가 선언되어 있다.
supportParameter
는 클라이언트이 요청이 메소드로 들어가기 전, 해당 메소드의 파라미터가 선언하는 Resolver를 지원하는지에 대한 여부를 판단한다.
resolveArgument
는 supportParameter
의 return값이 true일 경우, 실질적으로 데이터를 파싱하여 클라이언트가 요청한 메소드로 전달해준다. 어떠한 메소드를 지원하는지 모르기 때문에, Object
타입을 리턴하도록 되어있다.
예를 들어, 아래와 같이 선언된 클래스가 있을 때,
@RestContrller
@RequestMapping("/api")
public class UserController{
@GetMapping("/user")
public ResponseEntity<Map<String,Object>> getUser(@LoginUser User user){
// do something
}
// ...
}
getUser
메소드에서 @LoginUser라는 어노테이션과 함께 User를 매개변수로 받는다.
HandlerMethodArgumentResovler는 대부분 메소드의 파라미터 지원 여부를 판단하기 위해 커스텀 어노테이션과 같이 사용한다.
그렇기 때문에 필자 또한 커스텀 어노테이션을 만들고 이를 이용하는 형태로 사용하는 방법을 작성하겠다.
말이 커스텀 어노테이션이지 별 다를게 없다, 내용은 비어있는 어노테이션을 만들어 주고, 이 어노테이션이 적용된 파라미터에 대해서 resovler를 적용시킬 것이다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser{
}
메소드 파라미터에 대한 처리를 다룰 것이므로, Target은 파라미터로 선언해주고, Retention 정책은 Runtime으로 지정해준다.
@Builder
public class User {
private Long id;
private String name;
}
간단한 Entity 클래스이다. 위의 커스텀 어노테이션과 user 클래스가 파라미터에 선언되어 있으면, 데이터를 파싱하여 메소드로 넘겨줄것이다.
HandlerMethodArgumentResolver
는 인터페이스이므로 이를 구현하는 CustomArgumentResovler 클래스를 생성해준다.
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
// LoginUser 어노테이션이 선언되어 있는지 확인한다.
boolean isLoginUser = methodParameter.getParameterAnnotation(LoginUser.class) != null;
// 데이터를 파싱해 넘겨줄 데이터 타입을 확인
boolean isUser = methodParameter.getParameterType().equals(User.class);
return isLoginUser && isUser;
}
isLoginUser
는 위에서 선언한 커스텀 어노테이션인 LoginUser
가 파라미터에 선언되어 있는지를 판단하는 구문이다.
getParameterAnnotation(LoginUser.class) 메서드를 이용해 LoginUser.class 타입의 어노테이션을 가져오는데, 선언이 안되어 있는 파라미터의 경우 null을 리턴하기 때문에 null 여부를 판단한 결과를 저장한다.
isUser
는 파라미터의 타입이 User.class
인지 판단하는 구문이다. getParameterType()을 통해 파라미터의 타입을 가져오고, 이를 equal메소드로 User.class와 비교했다.
resolveArgument 메서드는 위에서 작성한 supportsParameter
가 true를 리턴할 경우 실행되어 실질적으로 데이터를 파싱한다.
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
return User.builder()
.id(Long.valueOf(nativeWebRequest.getParameter("id")))
.name(nativeWebRequest.getParameter("name"))
.build();
}
지금 까지는 ArgumentResolver의 기능 설정이었다. 실질적으로 작성한 resolver를 사용하기 위해서는 스프링에 Bean으로 등록을 해주어야 한다
@Configuration
public class WebConfig implement WebMvcConfigurer {
@AutoWired
CustomArgumentResovler resolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(resolver);
}
}
WebMvcConfigurer
인터페이스를 구현한 WebConfig 클래스를 만들어 작성한 resover를 등록해주면 된다.
테스트에 사용할 요청은 다음과 같다.
GET localhost/api/user?id=1&name=first_user
GET localhost/api/user2?id=1&name=second_user
테스트를 위해 작성한 컨트롤러 코드는 다음과 같다.
@RestController
@RequestMapping("/api")
public class Usercontroller {
@GetMapping("/user")
public ResponseEntity<Map<String, Object>> getUser(@LoginUser User user) {
Map<String, Object> map = new HashMap<>();
if (user != null) {
map.put("data", user);
} else {
map.put("error", null);
}
return ResponseEntity.ok(map);
}
@GetMapping("/user2")
public ResponseEntity<Map<String, Object>> getUser2(User user) {
Map<String, Object> map = new HashMap<>();
if (user != null) {
map.put("data", user);
} else {
map.put("error", null);
}
return ResponseEntity.ok(map);
}
}
getUser()
메서드는 @LoginUser
어노테이션과 User
객체가 파라미터에 선언되어 있다. 그렇기 때문에 예상대로 데이터 매핑이 잘 될것이고, 응답에는 data 필드와 user 객체의 정보가 담겨있을 것이다.
getUser2()
메서드는 @LoginUser
어노테이션이 선언되어 있지 않다. 때문에 HandlerMethodArgumentResovler
의 supportsParameter
메소드가 false를 리턴할 것이고 매핑이 되지 않아 getUser2()
메소드의 User 파라미터는 null이 될것이다.
getUser()
getUser2()
두 케이스 모두 위에서 예상한 대로의 결과가 나왔다.
처음 Spring을 배울 때는 Jackson Binder가 데이터 매핑을 자동으로 해주었기 때문에 HandlerMethodArgumentResovler의 존재를 몰랐고, 다른 것에 대해 공부할 필요성을 못느꼈다.
하지만, 한번 ArgumentResovler를 써보고 난 뒤, 작성자가 원하는대로 데이터를 매핑하여 주기 때문에, Controller 클래스의 코드가 짧아지는 효과를 보았고 FrontEnd와 연동하는 과정에서도 편리함을 느꼈다.