[Spring] Argument Resolver

김용범·2024년 12월 19일
0

Argument Resolver

개요 💁🏻‍♂️

JWT 토큰 방식으로 회원가입, 로그인, 로그아웃 기능을 구현했습니다. 구현한 내용 중에서 회원가입한 회원인지, 아닌지에 대해서 @RequsetHeader("Authorization") String token 코드를 통해서 Bearer 타입의 JWT 토큰을 받고, 이를 Service로 넘겨 Payload 내용을 확인하여 실제 유저인지 아닌지 체크하는 과정을 거칩니다.

    public void logout(String token) {

        if (!jwtTokenProvider.validateToken(token)) {
            throw new CustomException(Code.ACCESS_TOKEN_UNAUTHORIZED);
        }

        String nickname = jwtTokenProvider.getSubject(token);
        log.info("logout nickname check: {}", nickname);

        Member member = memberRepository.findByNickname(nickname).orElseThrow(
                () -> new CustomException(Code.NOT_EXIST_NICKNAME));

        refreshTokenRepository.delete(member.getId());
    }

위 사진과 같이 구현했을 때, 항상 의문이 들었던 점은 유저만 사용할 수 있는 모든 메소드에 해당 로직을 통해서 유저인지 확인하는 코드를 작성해야할까? 였습니다. 검색해 본 결과 해결할 수 있는 방법으로 Argument Resolver 라는 방식을 알게 되었습니다. Argument Resolver를 알아보기 전에 먼저 Binding 개념에 대해서 알아봅시다.

대표적인 Binding 방식

웹개발에서 바인딩(Binding)이란, 어떤 값들을 변수에 묶어버리는 것을 의미합니다. 대표적으로 3가지 바인딩 방식이 있습니다.

  1. @RequestBody - HTTP Body를 변수에 바인딩하기 위해 사용되는 어노테이션
    @PostMapping("/restaurants")
    public CommonResponse<?> createRestaurant(@RequestBody RestaurantCreateRequest restaurantCreateRequest) {
        restaurantService.createRestaurant(new RestaurantCreateParam().of(restaurantCreateRequest));
        return CommonResponse.of(Code.OK);
    }
  1. @RequestParam - Controller에서 쿼리 스트링을 변수에 바인딩하기 위해 사용되는 어노테이션
    @GetMapping("")
    public CommonResponse<?> readRestaurants(@RequestParam(value = "keyword", required = false) String keyword) {

        List<RestaurantItemResponse> restaurantItemResponses = restaurantService.readRestaurants(keyword);

        return CommonResponse.of(restaurantItemResponses);
    }
  1. @PathVariable - 가변적인 경로를 변수에 바인딩하기 위해 사용되는 어노테이션
    @DeleteMapping("/{id}/restaurants")
    public CommonResponse<?> deleteRestaurant(@PathVariable Long id) {
        restaurantService.deleteRestaurant(id);
        return CommonResponse.of(Code.OK);
    }

그 외 Binding 방식

대표적인 바인딩 방식 이외에도 HTTP Header, Session, Cookie 등 직접적이지 않은 방식 혹은 외부 데이터 저장소로부터 데이터를 바인딩해야할 때가 있는데, 이때 사용되는 것이 Argument Resolver 입니다. Argument Resolver를 사용하면 Controller Method Parameter 중에서 특정 조건에 맞는 Parameter가 있다면, 요청에 들어온 값을 이용해 원하는 객체를 만들어 바인딩해줄 수 있습니다.

정리해보자면 Argument Resolver(HandlerMethodArgumentResolver)는 Spring MVC에서 Controller Method의 Parameter를 자동으로 변환하고 주입해주는 편의를 제공해줍니다. 따라서, Argument Resolver 단에서 JWT Token의 Payload를 먼저 읽어 유저의 식별자를 추출하고, 해당 유저가 존재한다면 유저 Entity를 반환하고, 아니면 예외를 처리할 수 있습니다. 이로써 불필요한 인증 과정의 중복성을 줄여 보다 더 서비스 로직을 구현하는데 집중할 수 있습니다.

  • Controller 단에서 중복되는 코드들을 깔끔하게 처리할 수 있다.
  • Controller로 들어오는 Parameter를 가공하거나 수정하여 제공할 수 있다.

동작 방식

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

동작 방식은 위 사진과 같다.

  1. Client 요청
  2. Dispatcher Servlet에서 해당 요청 처리
  3. Client 요청에 대한 HandlerMapping 처리
    3-1. RequestMapping 처리 (RequestMappingHandlerAdapter 수행)
    3-2. Intercepter 처리
    3-3. Argument Resolver 처리 (실행 지점)
    3-4. Message Converter 처리
  4. Controller Method Invoke

예제 코드

Argument Resolver에 대한 개념과 동작 방식에 대해서 알았으니, 예제 코드를 확인해보도록 하자. 우선, Argument Resolver를 Spring FW에서 구현하기 위해서는 파라미터를 통해 바인딩 될 객체를 @interface로 만들어야 합니다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserArg {
}

그 다음, HandlerMethodArgumentResolver 인터페이스를 상속받은 Custom ArgumentResolver를 구현해줍니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    private final JwtTokenInterceptor jwtTokenInterceptor;
    private final JwtTokenProvider jwtTokenProvider;
    private final MemberRepository memberRepository;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UserArg.class);
    }

    @Override
    public Member resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String token = jwtTokenInterceptor.resolveToken(request);
        String nickname = jwtTokenProvider.getSubject(token);

        log.info("resolver nickname: {}", nickname);
        return memberRepository.findByNickname(nickname).orElse(null);
    }
}

그 다음, 열심히 만든 Custom ArgumentResolver를 WebMvcConfigurer에 저장해줍니다.

@Configuration
@RequiredArgsConstructor
public class ResolverWebconfig implements WebMvcConfigurer {

    private final UserArgumentResolver userArgumentResolver;

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

테스트

    @GetMapping("/api/test")
    public CommonResponse<?> test(@UserArg Member member) {
        return CommonResponse.of(member);
    }

@UserArg 어노테이션이 붙은 Parameter에 대해서 Argument Resolver가 바인딩 해주기 때문에 Controller에서는 바인딩된 객체를 그냥 사용해주면 됩니다.

위 사진과 같이 Postman 테스트 결과 정상적으로 Member Entity의 정보를 가져오는 것을 확인할 수 있습니다.

Reference

profile
꾸준함을 기록하며 성장하는 개발자입니다!

0개의 댓글

관련 채용 정보