오늘은 Spring WebFlux의 HandlerMethodArgumentResolver에 대해서 알아보도록 하자.

사실 WebFlux 이전에 WebMvc에도 동일한 기능이 존재한다. 인터페이스명까지 동일하니 거부감은 사실 없다. 기존의 mvc의 기능과 동일은 하나 WebFlux API에 맞춰진 형태라 생각하면 된다. 어떤 기능인지는 여기를 참고해도 되고 다른 블로그 혹은 문서를 살펴봐도 좋다.

WebMvc 클래스는 org.springframework.web.method.support.HandlerMethodArgumentResolver와 같고 WebFlux의 클래스는 org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver 이와 같다.

HandlerMethodArgumentResolverSupport

위의 내용을 알아보기 전에 WebMvc에는 존재 하지 않지만 WebFlux에 존재하는 클래스인 HandlerMethodArgumentResolverSupport 를 살펴보자. HandlerMethodArgumentResolverSupport 에는 protected 메서드가 3개 존재하는데 checkParameterType, checkParameterTypeNoReactiveWrapper, checkAnnotatedParamNoReactiveWrapper 라는 메서드이다.

어떤 일들은 하는지 한번살펴보자. 일반적으로는 HandlerMethodArgumentResolver.supportsParameter 메서드에서 많이 사용하고 있다. 물론 위의 메서드를 사용하지 않아도 된다.

checkParameterType

checkParameterType 메서드는 reactive 랩퍼 클래스를 허용한다는 메서드이다. 한마디로 파라미터에 reactive 랩퍼 클래스를 사용해도 된다는 의미를 갖고 있다.

사용법은 아래와 같다.

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return checkParameterType(parameter, UserInfo.class::isAssignableFrom);
}

두번째 파라미터는 Predicate<Class<?>>로 해당 조건이 만족하면 true를 리턴하고 그렇지 않으면 false를 리턴한다.
UserInfo 라는 클래스를 파라미터로 허용한다는 뜻인데 그게 만약 reactive 클래스라도 허용한다는 뜻이다.

@GetMapping("/")
public Mono<Message> message(Mono<UserInfo> userInfo) {

    //...
}

@GetMapping("/")
public Mono<Message> message(UserInfo userInfo) {

     //...
}

위와 같이 Mono<UserInfo>UserInfo 둘다 가능하다는 뜻이다.

checkParameterTypeNoReactiveWrapper

checkParameterType 메서드와는 다르게 reactive 스타일을 허용하지 않는다.
사용법은 다음과 같다.

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return checkParameterTypeNoReactiveWrapper(parameter, UserInfo.class::isAssignableFrom);
}

두번째 파라미터는 이 또한 Predicate<Class<?>>로 해당 조건이 만족하면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

@GetMapping("/")
public Mono<Message> message(Mono<UserInfo> userInfo) {

    //...
}

만약 위와 같이 작성했지만 파라미터를 reactive 스타일로 받는다음 에러가 발생한다.

FooHandlerMethodArgumentResolver does not support reactive type wrapper: reactor.core.publisher.Mono<ml.wonwoo.example.UserInfo>

위의 메서드를 사용할 경우에는 아래와 같이 사용해야 된다.

@GetMapping("/")
public Mono<Message> message(UserInfo userInfo) {

    //...
}


checkAnnotatedParamNoReactiveWrapper

위의 두 내용은 Type과 관련있었지만 이 메서드는 애노테이션과 관련이 있다. 이 메서드는 리액티브 스타일을 허용 하지 않는다.

사용법은 다음과 같다.

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return checkAnnotatedParamNoReactiveWrapper(parameter, CurrentUser.class,
            (annotation , clazz) -> !UserInfo.class.isAssignableFrom(clazz));
}

두번째 파라미터는 BiPredicate<A, Class<?>>로 해당 조건이 만족하면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

위와 같이 작성하면 아래와 같이 파라미터로 사용가능하다.

@GetMapping("/")
public Mono<Message> message(@CurrentUser User user) {

     //...
}

근데 왜 checkAnnotatedParamReactiveWrapper 메서드는 만들지 않았을까?

어쨌든 WebFlux에서는 HandlerMethodArgumentResolverSupport 대부분 상속받아서 구현하고 있으니 HandlerMethodArgumentResolverSupport 상속받아서 구현해도 되며 HandlerMethodArgumentResolver 를 직접 구현해도 상관없다.

하지만 필자는 HandlerMethodArgumentResolverSupport 상속받아서 구현했다.

resolveArgument

위에서는 어떤 파라미터로 받을지 체크하는 부분이였다면 resolveArgument는 해당 파라미터에 대한 구현을 직접해줘야 한다.

만약 reactive 스타일로 파라미터로 받지 않는다면 예전처럼 webmvc 구현해도 되지만 그렇지 않고 reactive 스타일도 지원한다면 추가적으로 조금 구현해야 되는게 있다.

참고로 동기적으로 구현하고 싶다면 SyncHandlerMethodArgumentResolver 를 구현하면 된다.

해당 파라미터가 reactive 스타일의 파라미터인지 알아야 한다. 하지만 이것보다 중요한건 대부분 reactor를 사용하겠지만 만약 다른 Reactive Streams API(rxjava1, rxjava2, jdkFlow) 혹은 coroutine 을 사용한다면 그에 맞게 변환을 해줘야 한다. 그 클래스가 ReactiveAdapterRegistry 이며 자세한건 문서를 찾아보자.

참고로 HandlerMethodArgumentResolverSupport 의 메서드들도 ReactiveAdapterRegistry를 사용한다.

@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {

    ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);

    ReactiveAdapter adapter = (resolvableType != null ? getAdapterRegistry().getAdapter(resolvableType.resolve()) : null);

    Mono<UserInfo> userMono = Mono.just(new UserInfo("wonwoo", "wonwoo@test.com"));

    return userMono
            .map(userInfo -> adapter != null ? adapter.fromPublisher(userMono) : userInfo);
}

위와 같이 해당 타입의 ReactiveAdapter를 가져와 Publisher 하면 된다. 아주 간단하다. 크게 복잡한 내용은 없는 것 같다.
그럼 다음과 같이 사용가능하다.

//reactor
@GetMapping("/")
public Mono<UserInfo> hello(Mono<UserInfo> userInfo) {

    return userInfo;
}

@GetMapping("/")
public Flux<UserInfo> hello(Flux<UserInfo> userInfo) {

    return userInfo;
}

//rxjava 1,2 
@GetMapping("/")
public Single<UserInfo> hello(Single<UserInfo> userInfo) {

    return userInfo;
}

@GetMapping("/")
public Observable<UserInfo> hello(Observable<UserInfo> userInfo) {

    return userInfo;
}

@GetMapping("/")
public Maybe<UserInfo> hello(Maybe<UserInfo> userInfo) {

    return userInfo;
}

@GetMapping("/")
public Flowable<UserInfo> hello(Flowable<UserInfo> userInfo) {

    return userInfo;
}


//jdk
@GetMapping("/")
public Flow.Publisher<UserInfo> hello(Flow.Publisher<UserInfo> userInfo) {

    return userInfo;
}

rxjava는 잘 몰라서 그냥 테스트만 해봤다.

오늘은 이렇게 Spring WebFlux의 HandlerMethodArgumentResolver 대해서 알아봤다. 만약 사용할 일이 있다면 언젠든지 사용해도 좋다. 어려운 내용이 아니므로 한번씩 해보는 것도 나쁘지 않아 보인다.

WebFlux도 공부할게 많다.