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.
간단하게 사용법을 알아보자. 특정 어노테이션이 있을 때 파라미터를 바인딩할 것이다. 예를 들면 Post를 생성하려면 User가 로그인한 상태일 때 가능하다. 로그인한 상태는 요청을 보낼 때 header에 userId를 담아 보내는 것으로 알 수 있다고 가정하자.
@Login
어노테이션@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@Login
어노테이션이 있을 때 파라미터를 바인딩할 것이기 때문에 커스텀으로 만들어 주었다.
HandlerMethodArgumentResolver를 상속받아 Custom Argument Resolver를 만들어 보자. Argument Resolver는 2개의 메서드를 Override해서 구현할 수 있다.
@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
@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);
}
}
@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
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());
}