Spring Framework

Spring Webflux - Request Mapping

흥부가귀막혀 2022. 3. 14. 12:48

Request Mapping

  • Spring Webflux 에선 아래와 같이 2가지 programming model 이 있고 그에 맞게 Request Mapping 을 해줄 수 있다.

Annotated Controllers

  • 스프링 MVC와 동일하며 spring-web 모듈에 있는 같은 어노테이션을 사용한다. 스프링 MVC와 웹플럭스 컨트롤러 모두 리액티브(Reactor, RxJava) 리턴 타입을 지원하기 때문에 이 둘을 구분하기 어렵다. 한 가지 눈에 띄는 차이는 웹플럭스에선 @RequestBody로 리액티브 인자를 받을 수 있다.
  • 스프링 웹플럭스는 어노테이션 기반 프로그래밍 모델을 지원하기 때문에, @Controller, @RestController 컴포넌트로 요청을 매핑하고, 입력을 받고, exception을 처리할 수 있다.
  • 컨트롤러는 메소드를 여러 가지로 활용할 수 있어서 클래스를 상속하거나 인터페이스를 구현하지 않아도 된다.
@RestController
public class HelloController {

    @GetMapping("/hello")
    public Mono<String> handle(@RequestBody Mono<String> request) {
        return request.map(name -> "Hello Webflux. My name is " + name);
    }
}

Functional Endpoints

  • 스프링 웹플럭스는 경량화된 함수형 프로그래밍 모델을 지원한다. WebFlux.fn이라고도 하는 이 모델은, 함수로 요청을 라우팅하고 핸들링하기 때문에 불변성(Immutablility)을 보장한다.
  • WebFlux.fn에선 HandlerFunction이 HTTP 요청을 처리한다. HandlerFunction은 ServerRequest를 받아 비동기 ServerResponse(i.e. Mono<ServerResponse>)를 리턴하는 함수다. 요청, 응답 객체 모두 불변(immutable)이기 때문에 JDK 8 방식으로 HTTP 요청, 응답에 접근할 수 있다. HandlerFunction 역할은 어노테이션 프로그래밍 모델로 치면 @RequestMapping 메소드가 하던 일과 동일하다.
  • 요청은 RouterFunction이 핸들러 펑션에 라우팅한다. RouterFunction은 ServerRequest를 받아 비동기 HandlerFunction(i.e. Mono<HandlerFunction>)을 리턴하는 함수다. 매칭되는 라우터 펑션이 있으면 핸들러 펑션을 리턴하고 그 외는 비어있는 Mono를 리턴한다. RouterFunction이 하는 일은 @RequestMapping 어노테이션과 동일하지만, 라우터 펑션은 데이터뿐 아니라 행동까지 제공한다는 점이 다르다.
  • 라우터를 만들 때는 아래 예제처럼 RouterFunctions.route()가 제공하는 빌더를 사용할 수 있다.
  • RouterFunction을 실행하는 방법 중 하나는 HttpHandler로 변환해 내장된 서버 어댑터에 등록하는 것인데, 라우터를 bean 으로 등록하면 Spring 에서 이러한 처리를 자동으로 해준다.
@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<?> personRouter(PersonHandler handler) {
        return RouterFunction<ServerResponse> route = RouterFunctions.route()
           .GET("/person/{id}", RequestPredicates.accept(APPLICATION_JSON), handler::getPerson)
           .GET("/person", RequestPredicates.accept(APPLICATION_JSON), handler::listPeople)
           .POST("/person", handler::createPerson)
           .build();
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }
}

RouterFunction

  • 라우터 펑션은 요청을 그에 맞는 HandlerFunction으로 라우팅한다.
  • 라우터 펑션을 직접 만들기보단, 보통 RouterFunctions 유틸리티 클래스를 사용한다.
  • RouterFunctions.route()가 리턴하는 빌더를 사용하거나, RouterFunctions.route(RequestPredicate, HandlerFunction)으로 직접 라우터를 만들 수 있다.

Predicates

  • RequestPredicate를 직접 만들어도 되지만, 요청 path, HTTP 메소드, 컨텐츠 타입 등 자주 사용하는 구현체는 RequestPredicates 유틸리티 클래스에 준비돼 있다.
  • 다음은 유틸리티 클래스로 Accept 헤더 조건을 추가하는 예제다.
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", RequestPredicates.accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().bodyValue("Hello World")).build();
  • and, or 조건을 걸 수 있다.
    • requestPredicate.and(RequestPredicate other) : requestPredicate 와 other predicate 가 둘다 만족해야한다.
    • requestPredicate.or(RequestPredicate other) : requestPredicate 와 other predicate 중 1개만 만족해도 된다.
  • RequestPredicates 가 제공하는 함수는 위의 and  or 를 조합해서 만든 것이 많다. 예를 들어 RequestPredicates.GET(String) API 는 내부적으로 RequestPredicates.method(Method)  RequestPredicates.path(String)  and 로 연결시켰다고 생각하면 된다.

Routes

  • 라우터 펑션은 정해진 순서대로 실행한다. 첫 번째 조건과 일치하지 않으면 두 번째를 실행하는 식이다. 따라서 구체적인 조건을 앞에 선언해야 한다. 어노테이션 프로그래밍 모델(Spring MVC)에선 자동으로 가장 구체적인 컨트롤러 메소드를 실행하지만, 함수형 모델에선 그렇지 않다 점에 주의해야한다.
  • build()를 호출하면 빌더에 정의한 모든 라우터 펑션을 RouterFunction 한 개로 합친다. 다음 방법으로도 여러 라우터 펑션을 조합할 수 있다.
    • RouterFunctions.route() 빌더의 add(RouterFunction)
    • RouterFunction.and(RouterFunction)
    • RouterFunction.andRoute(RequestPredicate, HandlerFunction) —  RouterFunctions.route()를 RouterFunction.and()로 감싸고 있는 축약 버전
@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<?> personRouter(PersonHandler handler) {
        return RouterFunction<ServerResponse> route = RouterFunctions.route()
           .GET("/person/{id}", RequestPredicates.accept(APPLICATION_JSON), handler::getPerson) // (1)
           .GET("/person", RequestPredicates.accept(APPLICATION_JSON), handler::listPeople) // (2)
           .POST("/person", handler::createPerson) // (3)
           .add(otherRoute) // (4)
           .build();
    }
}

(1) Accept 헤더가 JSON인 GET /person/{id}는 PersonHandler.getPerson으로 라우팅한다.
(2) Accept 헤더가 JSON인 GET /person은 PersonHandler.listPeople로 라우팅한다.
(3) POST /person은 다른 조건 없이 PersonHandler.createPerson로 라우팅한다.
(4) 마지막으로 나머지 요청을 처리할 otherRoute 펑션을 route에 추가한다.

Nested Routes

  • path가 같으면 대부분 같은 조건을 사용하므로, 라우터 펑션을 그룹핑하는 경우가 많다. 위의 Routes 예제는 라우터 펑션 세 개가 /person을 path 조건으로 사용했다.
  • 어노테이션을 사용했다면 클래스 레벨에 @RequestMapping을 선언해 중복 코드를 줄였을 거다.
  • WebFlux.fn 에선 공통 조건을 가지고있는 RouterFunction 을 공유하기 위해 Builder 를 받는 Consumer 인터페이스를 활용하도록 하고있다.
  • 위의 Routes 에 나온 예제를 아래와 같이 변경 가능하다.
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .path("/person", builder -> builder // (1)
        .GET("/{id}", RequestPredicates.accept(APPLICATION_JSON), handler::getPerson)
        .GET("", RequestPredicates.accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson))
    .add(otherRoute)
    .build();

(1) path 가 /person 인 조건의 RouterFunction 에 Builder 를 받는 Consumer 를 구현하여 분기처리를 하고있다.

  • builder 에서 제공하는 nest 함수를 활용하면 하위 RouterFunction 에 다른 공통 조건도 추가가 가능하다. 위 예에서 /person/{id}' 와 /person` 의 accept 조건이 같으므로 다음과 같이 변경이 가능하다.
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .path("/person", b1 -> b1
        .nest(RequestPredicates.accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .add(otherRoute)
    .build();