-
Spring Webflux - Functional Endpointsspring 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