Argument Resolver

김준석·2021년 2월 20일
0
post-custom-banner

Example-Code

Argument Resolver는 Controller에 들어오는 파라미터 값을 바인딩할 때 사용된다. Spring에서 HandlerMethodArgumentResolver 인터페이스를 상속하여 Class를 만들어 사용하면 된다.

Argument Resolver를 왜 사용하는 걸까? 그건 body에 데이터를 담아 보내거나 /post/1같이 path로 보내면 @RequestBody@PathVariable을 사용하여 파라미터로 받을 수 있지만, 간접적인 방법(예: 세션, 헤더, 쿠키)으로 데이터를 제공하여 파라미터로 받아야 할 경우가 있기 때문이다.

There are cases when we want to bind data to objects, but it comes either in a non-direct way (for example, from Session, Header or Cookie variables) or even stored in a data source. In those cases, we need to use a different solution.

Binding Domain Objects

사용법

간단하게 사용법을 알아보자. 특정 어노테이션이 있을 때 파라미터를 바인딩할 것이다. 예를 들면 Post를 생성하려면 User가 로그인한 상태일 때 가능하다. 로그인한 상태는 요청을 보낼 때 header에 userId를 담아 보내는 것으로 알 수 있다고 가정하자.

@Login 어노테이션

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

@Login 어노테이션이 있을 때 파라미터를 바인딩할 것이기 때문에 커스텀으로 만들어 주었다.

HandlerMethodArgumentResolver를 상속받아 Custom Argument Resolver를 만들어 보자. Argument Resolver는 2개의 메서드를 Override해서 구현할 수 있다.

HandlerMethodArgumentResolver 상속받은 클래스 구현

@Component
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

    private final UserService userService;

    public AuthArgumentResolver(final UserService userService) {
        this.userService = userService;
    }

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

    @Override
    public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
            final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
        final String userId = webRequest.getHeader("userId");
        try {
            return userService.findByIdentification(userId);
        } catch (Exception e) {
            throw new IllegalArgumentException();
        }
    }
}

userId는 사용자가 로그인할 때 사용하는 ID이다. PK와 헷갈릴 수 있어서 userId의 컬럼명을 identification로 네이밍했다.

각 메서드의 역할은 아래와 같다.

  • supportsParameter

    • 지정된 메서드의 파라미터를 검사하여 resolveArgument()를 실행할지의 여부를 boolean 값으로 리턴
  • resolveArgument

    • 어떤 파라미터로 바인딩할 것인지 로직이 담겨있는 메서드
    • 바인딩한 파라미터를 리턴(위의 코드에서는 User 리턴)

구현한 Argument Resolver 등록

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final AuthArgumentResolver authArgumentResolver;

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

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

Controller 구현

@RequestMapping("/posts")
@RestController
public class PostController {

    // ...

    @PostMapping
    public ResponseEntity<PostResponse> create(@Login final User user, @RequestBody final PostRequest postRequest) {
        final PostResponse postResponse = postService.create(user, postRequest);
        return ResponseEntity.created(URI.create("/posts/" + postResponse.getId())).body(postResponse);
    }
    
    // ...
}

이렇게 구현하면 /posts로 Post 요청이 오면 다음과 같이 동작할 것이다.

  • 먼저 supportsParameter() 메서드에서 @Login 어노테이션이 있는지 검증
  • 있다면 supportsParameter() 메서드는 True를 리턴
  • supportsParameter() 메서드가 True를 리턴하면 resolveArgument() 메서드 실행
  • resolveArgument() 메서드 로직 실행 후 User 리턴

Test

잘 동작하는지 테스트해보자. 테스트 과정은 이러하다.

  • create
    • User 생성
    • Post 생성 요청 보낼 때 header에 사용자의 로그인 ID 정보를 담아 전송
    • 응답 결과 확인
  • create_Exception
    • 사용자 정보가 없을 때 예외 처리(현재 예외처리를 따로 안 했기 때문에 상태 코드는 INTERNAL_SERVER_ERROR로 설정)
@Test
void create() {
    final UserResponse userResponse = post("/users", new UserRequest("tigger", "1234"), UserResponse.class);

    final PostResponse postResponse = post("/posts", "userId",
                                           userResponse.getIdentification(), new PostRequest("title", "content"), PostResponse.class);

    assertAll(
        () -> assertThat(postResponse.getId()).isNotNull(),
        () -> assertThat(postResponse.getTitle()).isEqualTo("title"),
        () -> assertThat(postResponse.getContent()).isEqualTo("content"),
        () -> assertThat(postResponse.getUser()).isNotNull(),
        () -> assertThat(postResponse.getUser().getId()).isNotNull(),
        () -> assertThat(postResponse.getUser().getIdentification()).isEqualTo("tigger")
    );
}

@Test
void create_Exception() {
    postInternalServerError("/posts", "userId", "notExistUser", new PostRequest("title", "content"));
}

public <T> T post(final String path, final Object request, final Class<T> responseType) {
    return given()
            .body(request)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .accept(MediaType.APPLICATION_JSON_VALUE)
    .when()
	    .post(path)
    .then()
            .log().all()
            .statusCode(HttpStatus.CREATED.value())
            .extract().as(responseType);
}

public <T> T post(final String path, final String headerName, final Object headerValue,
                  final Object request, final Class<T> responseType) {
    return given()
            .header(headerName, headerValue)
            .body(request)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .accept(MediaType.APPLICATION_JSON_VALUE)
    .when()
            .post(path)
    .then()
            .log().all()
            .statusCode(HttpStatus.CREATED.value())
            .extract().as(responseType);
}

public void postInternalServerError(final String path, final String headerName, final Object headerValue,
                                final Object request) {
    given()
            .header(headerName, headerValue)
            .body(request)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .accept(MediaType.APPLICATION_JSON_VALUE)
    .when()
            .post(path)
    .then()
            .log().all()
            .statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
profile
내 몸에는 꼰대의 피가 흐른다.
post-custom-banner

0개의 댓글