ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Webflux - Functional Endpoints
    spring 2019. 7. 25. 12:52

    개요


    Spring WebFlux는 WebFlux.fn이라는 요청을 라우팅하고 처리하는 경량의 함수형 프로그래밍 모델을 포함하고 있다. 물론 Webflux는 기존 MVC처럼 어노테이션 기반의 프로그래밍 모델도 지원하기 때문에, 두 가지 모델 중 하나를 개인 선호에 따라 선택하여 쓸 수 있다.

     

    WebFlux.fn에는 다음과 같은 두 가지 핵심 클래스가 있다.

     

    - HandlerFunction

    : ServerRequest를 인자로 받아 Mono<ServerResponse>를 리턴한다. 어노테이션 기반 모델에서 @RequestMapping 메소드의 바디와 동일한 역할이다.

     

    - RouterFunction

    : ServerRequest를 인자로 받아 Mono<HandlerFunction>를 리턴한다. router function이 매치가 되면, 그에 맞는 handler fucntion이 리턴이 되고, 그렇지 않으면 빈 Mono (Mono.empty())를 리턴한다.

     

    예시

    PersonRepository repository = ...
    PersonHandler handler = new PersonHandler(repository);
    
    RouterFunction<ServerResponse> route = route()
        .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson)
        .build();
    
    
    public class PersonHandler {
    
        // ...
    
        public Mono<ServerResponse> listPeople(ServerRequest request) {
            // ...
        }
    
        public Mono<ServerResponse> createPerson(ServerRequest request) {
            // ...
        }
    
        public Mono<ServerResponse> getPerson(ServerRequest request) {
            // ...
        }
    }

     

    HandlerFunction


    ServerRequest와 ServerResponse는 별도 서블릿 제약이 없는 JDK 8 친화적인 HTTP request와 response의 인터페이스이다. 또한, 두 객체 모두 reacitve type을 지원한다. request body는 Reactor의 Flux나 Mono로 추출할 수 있고, response body는 Reactive Streams 표준의 Publisher로 추출할 수 있다.

     

    ServerRequest / ServerResponse

    ServerRequest 예시

    Mono<String> string = request.body(BodyExtractors.toMono(String.class));
    Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

    위의 코드는 아래처럼 축약해서 쓸 수 있다.

    Mono<String> string = request.bodyToMono(String.class);
    Flux<Person> people = request.bodyToFlux(Person.class);

    다음은 form data를 추출하는 예시이다.

    Mono<MultiValueMap<String, String> map = request.body(BodyExtractors.toFormData());

    다음은 multipart 데이터를 map으로 추출하는 예시이다.

    Mono<MultiValueMap<String, Part> map = request.body(BodyExtractors.toMultipartData());
    

    다음은 여러 개의 multipart들을 한 번에 추출하는 예시이다.

    Flux<Part> parts = request.body(BodyExtractors.toParts());
    

     

    ServerResponse 예시

    Mono<Person> person = ...
    ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);

    다음은 body 없이 201 (created) status의 response를 만드는 예시이다.

    URI location = ...
    ServerResponse.created(location).build();

     

    Handler Classes

    handler function을 다음과 같이 람다식으로 작성할 수 있다.

    HandlerFunction<ServerResponse> helloWorld =
      request -> ServerResponse.ok().body(fromObject("Hello World"));

    그러나, 일반적인 어플리케이션에서는 여러 개의 handler function들이 필요할 것이고, 각각을 모두 람다로 쓴다면 지저분해질 것이다. 그래서 일반적으로 다음과 같이 관련있는 handler fucntion을 하나의 handler class에 모아둔다. MVC 모델의 @Controller와 비슷한 역할이라고 생각하면 된다.

     

    예시

    public class PersonHandler {
    
        private final PersonRepository repository;
    
        public PersonHandler(PersonRepository repository) {
            this.repository = repository;
        }
    
        public Mono<ServerResponse> listPeople(ServerRequest request) { 
            Flux<Person> people = repository.allPeople();
            return ok().contentType(APPLICATION_JSON).body(people, Person.class);
        }
    
        public Mono<ServerResponse> createPerson(ServerRequest request) { 
            Mono<Person> person = request.bodyToMono(Person.class);
            return ok().build(repository.savePerson(person));
        }
    
        public Mono<ServerResponse> getPerson(ServerRequest request) { 
            int personId = Integer.valueOf(request.pathVariable("id"));
            return repository.getPerson(personId)
                .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromObject(person)))
                .switchIfEmpty(ServerResponse.notFound().build());
        }
    }

     

    Validation

    functional endpoint 방식으로도 스프링의 기존 validation을 적용할 수 있다. 다음은 Validator를 구현하고 있는 PersonValidator에 대한 예시이다. 스프링 validation에 관한 내용은 여기를 참고

     

    public class PersonValidator implements Validator {
    
        /**
         * This Validator validates *only* Person instances
         */
        public boolean supports(Class clazz) {
            return Person.class.equals(clazz);
        }
    
        public void validate(Object obj, Errors e) {
            ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
            Person p = (Person) obj;
            if (p.getAge() < 0) {
                e.rejectValue("age", "negativevalue");
            } else if (p.getAge() > 110) {
                e.rejectValue("age", "too.darn.old");
            }
        }
    }
    
    public class PersonHandler {
    
        private final Validator validator = new PersonValidator(); 
    
        // ...
    
        public Mono<ServerResponse> createPerson(ServerRequest request) {
            Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); 
            return ok().build(repository.savePerson(person));
        }
    
        private void validate(Person person) {
            Errors errors = new BeanPropertyBindingResult(body, "person");
            validator.validate(body, errors);
            if (errors.hasErrors) {
                throw new ServerWebInputException(errors.toString()); 
            }
        }

     

    RouterFunction


    Router function은 요청을 그에 상응하는 HandlerFunction으로 매치시켜주는 역할을 한다. 일반적으로 직접 router function을 작성할 일은 거의 없고, RouterFunctions라는 유틸리티 클래스를 이용하여 빌드하는 것이 좋다. RouterFunctions.route()와 RouterFunctions.route(RequestPredicate, HandlerFunction) 두 빌더 형태가 있는데, RouterFunctions.route()를 쓰는게 function들을 체이닝하기에 편리하다.

     

    RouterFunction에 routing 조건을 RequestPredicate로 작성할 수 있는데, 이것도 RequestPredicates 유틸리티 클래스를 사용하면 편리하다.

    아래 예시는 RequestPredicates.accept()를 사용하였다.

    RouterFunction<ServerResponse> route = RouterFunctions.route()
        .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
            request -> Response.ok().body(fromObject("Hello World")));

     

    router function은 아래처럼 여러 개의 router function들을 체이닝할 수 있는데, 당연하게도 순서에 따라 차례대로 검증된다. 첫 번째 router fucntion에 맞지 않으면, 다음 function으로 보내는 방식이다. 따라서 가장 구체적인 route가 첫 번째 순서에 오도록 해야한다. 어노테이션 기반 모델에서는 자동으로 가장 구체적인 컨트롤러 메소드가 수행된다는 점과 차이가 있기 때문에, 유의해야 한다.

    PersonRepository repository = ...
    PersonHandler handler = new PersonHandler(repository);
    
    RouterFunction<ServerResponse> otherRoute = ...
    
    RouterFunction<ServerResponse> route = route()
        .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) 
        .GET("/person/*", accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson)
        .add(otherRoute) 
        .build();

    위 예제를 아래처럼 중복되는 내용을 없앨 수 있도록 nest()메소드로 nested code를 작성할 수 있다.

    RouterFunction<ServerResponse> route = route()
        .path("/person", b1 -> b1
            .nest(accept(APPLICATION_JSON), b2 -> b2
                .GET("/{id}", handler::getPerson)
                .GET("/*", handler::listPeople))
            .POST("/person", handler::createPerson))
        .build();

     

    Running a Server


    위에서 작성한 RouterFunction은 단순히 자바 오브젝트에 불과하다. 이 것을 서버에서 실행시키는 가장 단순한 옵션은 HttpHandler로 변환시켜 각 서버 어댑터들이 사용할 수 있게 만들어주는 것이다. 다음과 같이 HttpHandler로 변환시킬 수 있다.

     

    - RouterFunctions.toHttpHandler(RouterFunction)

    - RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

     

    위 내용을 reactor netty에 적용한 예제이다.

    HttpHandler handler = ...
    ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
    HttpServer.create(host, port).newHandler(adapter).block();

    다른 예제는 여기를 참고

     

    보다 일반적인 경우는 지난 포스트의 DispatcherHandler를 이용하는 방식이다. functional endpoints에 쓰이는 special bean은 RouterFunctionMapping, HandlerFunctionAdapter, ServerResponseResultHandler이다.

     

    RouterFunctionMapping에서, RouterFunction타입의 bean들을 모아 List형태로 보관하기 때문에, 이 방식을 사용하기 위해서는 RouterFunction을 빈으로 등록해주어야 한다.

    @Configuration
    @EnableWebFlux
    public class WebConfig implements WebFluxConfigurer {
    
        @Bean
        public RouterFunction<?> routerFunctionA() {
            // ...
        }
    
        @Bean
        public RouterFunction<?> routerFunctionB() {
            // ...
        }
    
        // ...
    
        @Override
        public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
            // configure message conversion...
        }
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // configure CORS...
        }
    
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
            // configure view resolution for HTML rendering...
        }
    }

     

    Filtering Handler Functions


    handler function들을 before(), after(), filter() 메소드들로 필터링할 수 있다. 어노테이션 기반 모델에서 @ControllerAdvice, ServletFilter과 비슷한 기능을 한다고 생각하면 된다. 

     

    before(), after()

    RouterFunction<ServerResponse> route = route()
        .path("/person", b1 -> b1
            .nest(accept(APPLICATION_JSON), b2 -> b2
                .GET("/{id}", handler::getPerson)
                .GET("", handler::listPeople)
                .before(request -> ServerRequest.from(request) 
                    .header("X-RequestHeader", "Value")
                    .build()))
            .POST("/person", handler::createPerson))
        .after((request, response) -> logResponse(response)) 
        .build();

     

    filter()

    RouterFunction<ServerResponse> route = route()
        .path("/person", b1 -> b1
            .nest(accept(APPLICATION_JSON), b2 -> b2
                .GET("/{id}", handler::getPerson)
                .GET("", handler::listPeople))
            .POST("/person", handler::createPerson))
        .filter((request, next) -> {
            if (securityManager.allowAccessTo(request.path())) {
                return next.handle(request);
            }
            else {
                return ServerResponse.status(UNAUTHORIZED).build();
            }
        })
        .build();

     

    'spring' 카테고리의 다른 글

    Spring WebFlux - Config  (0) 2019.08.29
    Spring Webflux - CORS  (0) 2019.07.31
    Spring Webflux - DispatcherHandler  (0) 2019.07.18
    Spring WebClient  (0) 2019.07.10
    Spring Webflux + Reactor  (0) 2019.05.13

    댓글

Designed by Tistory.