HandlerMethodArgumentResolver는 spirng에서 제공하는 기능으로 어떤 경우에 사용하면 좋을지 알아보자.
예를 들어, 클라이언트 측에서 header에 토큰을 담아서 request를 보내는 경우가 있다고 가정해보자. 그리고 아래와 같이 거의 모든 API에서 해당 요청을 보낸 사용자가 누구인지 사용자 정보가 필요하다.
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
@GetMapping("/test1")
public void test1(@RequestHeader String token) {
// 이 API에서는 사용자 정보가 필요합니다!
}
@PostMapping("/test2")
public void test2(@RequestHeader String token) {
// 이 API에서도 사용자 정보가 필요합니다!
}
@GetMapping("/test3")
public void test3(@RequestHeader String token) {
// 이 API에서도 사용자 정보가 필요합니다!
}
@GetMapping("/test4")
public void test4(@RequestHeader String token) {
// 이 API에서도 사용자 정보가 필요합니다!
}
}
-> 이 경우, 사용자의 정보를 알아내는 코드가 여러 곳에서 중복될테니 토큰으로부터 사용자 정보를 알아내 User 객체를 리턴해주는 메소드를 따로 만들어야 한다.
🔖 토큰으로부터 사용자 정보를 알아내서 User 객체를 리턴하는 메소드
public User getLoginUser(String token) {
return userRepository.findByEmail(token).orElseThrow();
}
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
private final AuthService authService;
@GetMapping("/test1")
public void test1(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// ~~
}
@PostMapping("/test2")
public void test2(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// ~~
}
@GetMapping("/test3")
public void test3(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// ~~
}
@GetMapping("/test4")
public void test4(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// ~~
}
}
-> 맨 처음 작성했던 코드보다는 중복되는 코드가 적지만 그래도 사용자 정보가 필요한 컨트롤러마다 이렇게 코드가 반복되어 들어가있다.
헤더에 있는 토큰을 읽어서 User 객체를 반환해주는 이런 공통적인 부분을 어딘가에서 처리해서 컨트롤러의 매개변수로 User 객체를 짠! 하고 넘겨준다면?
@GetMapping("/test1")
public void test1(@LoginUser User user) {
// ~~
}
@PostMapping("/test2")
public void test2(@LoginUser User user) {
// ~~
}
@GetMapping("/test3")
public void test3(@LoginUser User user) {
// ~~
}
@GetMapping("/test4")
public void test4(@LoginUser User user) {
// ~~
}
위와 같이 @LoginUser와 같은 어노테이션 하나로 컨트롤러에서 쉽게 User 객체를 얻어오려면 세 가지를 해야한다.
1) @LoginUser 어노테이션을 만들어준다.
2) HandlerMethodArgumentResolver를 구현한 클래스를 만든다.
3) 이렇게 구현한 HandlerMethodArgumentResolver를 등록해주어야 한다.
public @interface LoginUser {
}
어노테이션을 만들 때는 interface 앞에 @를 붙이면 됩니다.
@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthService authService;
// supportsParameter 메소드는 해당 메소드의 매개변수가 해당 resolver가 지원하는지를 체크한다.
// true를 반환하면 지원한다는 것이고 false를 반환하면 지원하지 않는다는 의미이다.
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasLoginUserAnnotation = parameter.hasParameterAnnotation(LoginUser.class);
boolean isUserType = User.class.isAssignableFrom(parameter.getParameterType());
// LoginUser라는 어노테이션이 있고 해당 매개변수의 타입이 User 타입인지를 체크
return hasLoginUserAnnotation && isUserType;
}
@Override
// 매개변수로 넣어줄 값을 제공하는 메소드(User 객체를 반환)
// request의 헤더에서 토큰을 꺼내고 토큰을 통해 얻은 User 객체를 반환
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = request.getHeader("Authorization");
if (token == null) {
return null;
}
return authService.getLoginUser(token);
}
}
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}
}
LoginUserArgumentResolver를 빈으로 등록한 이유는 바로 여기에서 주입받아 사용하기 위함이다.
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
@GetMapping("/test1")
public void test1(@LoginUser User user) {
log.debug("여기는 test1, 사용자의 이름은 {}, 이메일은 {}", user.getName(), user.getEmail());
}
@PostMapping("/test2")
public void test2(@LoginUser User user) {
log.debug("여기는 test2, 사용자의 이름은 {}, 이메일은 {}", user.getName(), user.getEmail());
}
@GetMapping("/test3")
public void test3(@LoginUser User user) {
log.debug("여기는 test3, 사용자의 이름은 {}, 이메일은 {}", user.getName(), user.getEmail());
}
@GetMapping("/test4")
public void test4(@LoginUser User user) {
log.debug("여기는 test4, 사용자의 이름은 {}, 이메일은 {}", user.getName(), user.getEmail());
}
}
이렇게 HandlerMethodArgumentResolver를 사용하면 여러 곳에서 사용되는 공통적인 로직을 줄이고, 매개변수로 필요한 정보를 손쉽게 가져올 수 있다.