Spring ArgumentResolver

후니팍·2023년 5월 6일
1

controller의 매핑된 uri 요청으로 들어오는 데이터를 원하는 객체로 만들어 줄 수 있는 역할을 합니다.

학습 계기

지금까지 request body나 request parameter, path variable를 사용하면서 어노테이션을 이용하여 요청 데이터들을 처리해줬습니다. 이를 위한 어노테이션이 있기 때문에 어노테이션 사용만으로 쉽게 원하는 데이터를 바인딩할 수 있었습니다. 하지만 이번 우테코 미션에 추가된 인증과 인가와 같은 특수 상황인 경우에는 이를 위한 어노테이션이 존재하지 않았습니다.

우테코 학습 테스트로 주어진 Interceptor와 ArgumentResolver를 보고 인증과 인가를 처리하는 것을 익혔는데요. controller의 코드에 아무런 표시없이 작업을 처리하는 Interceptor보다 @RequestBody처럼 어노테이션으로 표시를 해주고 작업 처리를 하는 ArgumentResolver를 이용해 사용자 확인 코드를 작성했습니다.

Interceptor로는 요청의 header 안에 Authorization이 있는지 확인하는 역할을 주려했는데요, header에 값을 넣어주어도 Interceptor에서는 보이지 않고 ArgumentResolver에서는 보였습니다. 그래서 Interceptor는 미션에 적용하지 못했습니다. 이 문제는 추후 해결해보겠습니다.


사용하지 않았을 때

사용하지 않았을 경우, 유저 인증이 필요한 모든 API에서 유저를 찾는 로직을 controller에 중복적으로 넣어야합니다. 아래 코드는 제 예전 프로젝트 코드입니다.

@PostMapping
public ResponseMessage createProject(
        @Valid @RequestBody ProjectRequestDto projectRequestDto, HttpServletRequest request) {

    Long userId = Long.valueOf(request.getUserPrincipal().getName());

    return ResponseMessage.toResponseEntity(
            ResponseCode.CREATE_PROJECT_SUCCESS,
            projectService.createProject(projectRequestDto, userId)
    );
}

@PatchMapping("/{projectId}")
public ResponseMessage modifyProject(@PathVariable Long projectId,
                                     @Valid @RequestBody ProjectRequestDto projectRequestDto, HttpServletRequest request) {

    Long userId = Long.valueOf(request.getUserPrincipal().getName());

    return ResponseMessage.toResponseEntity(
            ResponseCode.MODIFY_PROJECT_SUCCESS,
            projectService.modifyProject(userId, projectId, projectRequestDto)
    );
}

코드를 보면 userId를 찾는 부분을 중복으로 쓰고 있는 것을 볼 수 있습니다. 중복을 없애기 위해 HandlerMethodArgumentResolver를 사용해보도록 하겠습니다.


HandlerMethodArgumentResolver

HandlerMethodArgumentResolver를 implements하면 두 개의 메소드를 오버라이드 해야합니다.
supportsParameter()resolveArgument()입니다. 각각 알아보겠습니다.

  • boolean supportsParameter() : 요청 파라미터에 대한 조건을 걸고 조건을 충족할 때 true를 반환합니다.
  • Object resolveArgument() : 앞의 supportsParameter()에서 true가 반환되면 이 메서드를 실행시켜 parameter를 원하는 형태로 바꾸어주는 메소드입니다.

두 메소드를 통해 특정 조건에 유저 인증을 할 수 있도록 설정할 수 있습니다. 저는 @Auth라는 어노테이션을 만들어 특정 조건을 만들어주었습니다.

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

간단하게 해당 어노테이션을 런타임시에 파라미터에만 사용할 수 있도록 설정했습니다.


구현 코드

먼저 supprotsParameter()에서 파라미터가 Integer 타입이고 @Auth 어노테이션을 사용했을 때 true를 반환하도록 했습니다. controller 메소드의 인자에 @Auth 어노테이션과 Integer 타입이 엮여있으면 유저 인증을 진행합니다.

resolveArgument()의 반환 타입이 Object인데, 다형성을 살리기 위해서 자바 개발자들이 가장 큰 Object를 반환하도록 설계했다고 생각했습니다. 저는 인증된 유저의 id를 반환하도록 했기 때문에 Integer 자료형으로 반환이 될 것입니다.
Integer형으로 반환되기 때문에 supportsParameter()의 조건에 파라미터 타입이 Integer여야 한다는 것을 걸어 동료 개발자가 같은 파일에서 resolveArgument()Object 타입을 보고 어떤 타입으로 반환될 지 굳이 다른 파일을 보지 않고 알 수 있게끔 했습니다.

resolveArgument()에서는 요청 헤더에 담긴 Basic Authorization을 읽었습니다. 값을 디코딩한 후에 이메일과 패스워드로 사용자의 id를 알아내는 방식입니다.

코드는 아래와 같습니다.

@Component
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

    private static final String AUTHORIZATION = "Authorization";

    private final AuthDao authDao;

    public AuthArgumentResolver(final AuthDao authDao) {
        this.authDao = authDao;
    }

    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.getParameterType().equals(Integer.class) &&
                parameter.hasParameterAnnotation(Auth.class);
    }

    @Override
    public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
                                  final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
        final HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();
        final String authorizationHeader = httpServletRequest.getHeader(AUTHORIZATION);
        final AuthenticationDto authenticationDto = BasicAuthorizationExtractor.extract(authorizationHeader);
        return authDao.findIdByEmailAndPassword(authenticationDto.getEmail(), authenticationDto.getPassword());
    }
}

이제 추가적으로 이 ArgumentResolver를 WebMvcConfigurer에 등록만 해주면 됩니다!

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    private final AuthArgumentResolver authArgumentResolver;

    public WebMvcConfiguration(final AuthArgumentResolver authArgumentResolver) {
        this.authArgumentResolver = authArgumentResolver;
    }

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

이렇게 하여 컨트롤러에서 아래와 같이 간단한 어노테이션 하나로 유저를 인증할 수 있었습니다.

@PostMapping
public ResponseEntity<Void> create(@Auth final Integer userId,
                                   @RequestBody @Valid final CartRequestDto cartRequestDto) {
    cartService.create(cartRequestDto, userId);
    return ResponseEntity.created(URI.create(REDIRECT_URL)).build();
}

@GetMapping
public ResponseEntity<List<CartItemResponseDto>> read(@Auth final Integer userId) {
    final List<CartItemResponseDto> response = cartService.getProductsInCart(userId);
    return ResponseEntity.ok().body(response);
}

마무리

점점 중복을 줄이는 방법을 알게되는 것 같아 행복합니다.

profile
영차영차

1개의 댓글

comment-user-thumbnail
2023년 5월 6일

오오 이거 계속 궁금했는데 신기하네요

답글 달기