[Spring] Argument Resolver란 (HandlerMethodArgumentResolver, WebMvcConfigurer)

 

✍️ Argument Resolver, 아규먼트 리졸버 개념

Argument Resolver란, Client가 요청한 Request로부터 값을 참조하거나 객체를 생성해서 Handler(Controller)의 파라미터에 바인딩 할 때 사용하는 객체이다.

 

가령, 로그인 인증을 마친 Client가 Server로부터 자신의 UserId가 기입된 인증용 Token을 발급받았다고 가정해 보자. Client는 Request를 보낼 때마다 인증의 일환으로 Token을 보낼 것이고, Server는 유효한 토큰인지 검증을 거친 후 (필요에 따라) 토큰에 저장된 UserId를 꺼내서 Client의 요청을 처리할 것이다.

Argument Resolver 없이 단순 User 조회 Controller는 다음과 같이 세 단계에 걸쳐서 작성될 것이다. 1. 토큰 인증 2. 토큰에서 UserId 추출 3. 조회, 반환

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private AuthTokenService authTokenService;

    @Autowired
    private UserService userService;

    @GetMapping({"", "/"})
    public User getUser(HttpServletRequest request) {
        // 토큰 인증
        String token = authTokenService.extractToken(request);
        authTokenService.validateToken(token);

        // 유저 Id 추출
        int userId = authTokenService.extractUserId(token);

        // user반환
        return userService.findUser(userId);
    }
}

 

User의 정보를 업데이트할 경우는 어떨까? 단순 조회와 유사하게 토큰 인증, UserId 추출, 조회-갱신-반환 과정을 거칠 것이다. 이처럼 토큰 인증과 UserId 추출 코드가 중복되어 작성될 뿐만 아니라 하나의 Controller가 인증, 추출, 비즈니스 로직 호출과 반환이라는 여러 책임을 떠안고 있다는 문제가 발생한다.    

 

결론부터 말하면 이러한 문제를 InterceptorArgument Resolver를 통해 해결하는 데, 일반적으로 1. 토큰 인증의 경우 Interceptor를 통해 검증하고, 2. UserId 추출은 Argument Resolver를 통해 해결한다.

 

Argument Resolver의 호출 시점으로는, Handler Adapter가 Handler 호출을 위해 필요한 Argument를 생성하기 위해 호출된다. 

https://maenco.tistory.com/entry/Spring-MVC-Argument-Resolver%EC%99%80-ReturnValue-Handler

 

 

놀랍게도 우리는 Controller를 작성하면서 Argument Resolver를 매우 매우 자주 사용하는데 @PathVariable, @RequestParam이 그 대표적인 예시이다. 각각의 어노테이션에 대응하는 PathVariableMethodArgumentResolver, RequestParamMethodArgumentResolver는 해당 어노테이션이 붙은 Parameter에 전달될 Argument를 만들어 반환한다. 여기서 자연스럽게 한 가지 사실을 알 수 있는데 Argument Resolver를 사용하기 위해선 어노테이션이 필요하단 것이다.

 

🍊 HandlerMethodArgumentResolver

User defined Argument Resolver를 추가하기 위해선 HandlerMethodArgumentResolver의 구현체를 정의해야 한다.

먼저, HandlerMethodArgumentResolver의 메서드들을 살펴보면

 

1. supportsParameter

주어진 파라미터를 본 Argument Resolver가 처리해야 할지 판단하는 메서드로, 반환값이 true인 경우 후에 서술할 resolveArgument 메서드가 호출된다.

boolean supportsParameter(MethodParameter parameter);

 

2. resolveArgument

supportsParameter가 true를 반환했을 때 호출되는 메서드로, parameter를 만족하는 Argument로 바인딩해서 반환하는 메서드이다.

@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

 

🌱Argument Resolver 예제 코드

토큰 인증에 관한 코드는 Interceptor의 영역이므로 본 예제 코드에선 생략되었습니다.

 

토큰에서 UserId를 추출하는 Argument Resolver를 정의한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface UserArg {
}
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private AuthTokenService authTokenService;

    /**
     * 호출될 Handler의 아규먼트를 검사하는 메서드
     *
     * @return ArgumentResolver 적용 여부
     * @parameter 메서드에 명시된 파라미터
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // UserArg 어노테이션 적용되어 있고, int 타입인지
        return parameter.getParameterAnnotation(UserArg.class) != null
                && parameter.getParameterType().equals(String.class);
    }

    /**
     * supportsParameter 메서드가 true를 반환 했을 때 호출되는 메서드로
     * resolveArgument에서 반환하는 값이 실제 파라미터에 주입되는 값이다
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 토큰 추출하고
        HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();
        String token = authTokenService.extractToken(httpServletRequest);

        // 토큰에서 UserId 추출
        String userId = authTokenService.extractUserId(token);

        return userId;
    }
}

 

Argument Resolver는 WebMvcConfigurer addArgumentResolvers 메서드를 통해 등록해야 한다.

@Configuration
public class ResolverWebConfig implements WebMvcConfigurer {

    @Bean
    public UserArgumentResolver userArgumentResolver(){
        return new UserArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver());
    }
}

 

Controller는 더 이상 HttpServletRequest request를 받지 않고 바인딩 된 UserId를 직접 받는다. 그리고 여기에 @UserArg 어노테이션이 사용되었다.

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping({"", "/"})
    public User getUser(@UserArg String userId) {
        return userService.findUser(userId);
    }

//    @GetMapping({"", "/"})
//    public User getUser(HttpServletRequest request) {
//        // 토큰 인증
//        String token = authTokenService.extractToken(request);
//        authTokenService.validateToken(token);
//
//        // 유저 Id 추출
//        int userId = authTokenService.extractUserId(token);
//
//        // user반환
//        return userService.findUser(userId);
//    }
}

 

Controller는 토큰 인증과 UserId 추출이라는 책임에서 벗어났다. 그저 비즈니스 로직을 호출할 뿐.