WebFlux framework 번역

2018. 2. 22. 18:03Java/Spring Framework

이것또한 번역하면서 훑어본 내용입니다.
함부러 막번역해서 송구스럽습니다. T.T 


WebFlux framework


이 섹션은 Spring Framework 5을 사용하는 Web application을 위한 반응형 프로그래밍 지원에 대한 기본 정보를 제공합니다. 

1) Introduction

1-1) What is Reactive Programming?

간단히 말하면, 반응형 프로그래밍은 비동기(asynchronous), 이벤트 기반(event-driven)인  
non-blocking 어플리케이션에 관한 것입니다.
수직적 확장(예를 들면, JVM안에서)보다 차라리 수평적인 확장(예를 들명 클러스터링)을 하는 작은 수의 스레드가 요구됩니다.

반응형 어플리케이션의 핵심 관점은 배압(backpressure) 컨셉 입니다.
이 배압 컨셉이라는 것은 producers가 consumers를 넘어서지(overwhelm,압도하다,전복하다,당황하다) 
않도록 하기 위한 메카니즘입니다.

예를 들어, HTTP 연결이 너무 느리면, 
데이터베이스에서부터 HTTP response 까지 확장하는 반응형 컴포넌트들의 파이프 라인안에서,
데이터 저장소도 역시 속도를 줄이거나, 네트워크 능력이 해결될때까지 완전히 멈출수 있습니다.  
(For example in a pipeline of reactive components extending from the database to the HTTP response 
when the HTTP connection is too slow, the data repository can also slow down or stop completely 
until network capacity frees up.)

반응형 프로그래밍 하는것은 필수적에서 선언적으로 역시 비동기 로직으로의 주요 전환이 이어집니다.
(Reactive programming also leads to a major shift from imperative to declarative async composition of logic)
블럭킹 코드를 작성하는 것과 Java 8에서 람다 식을 이용해서 follow-up actions들 구성하는 
completableFuture 을 사용하는 것과 비교할수 있습니다.

관련하여 보다 상세한 정보는 여기 링크에 있는 Notes on Reactive Programming (by Dave Syer) 을 읽어보시길 바랍니다.

1-2) Reactive API and Building Blocks

스프링 프레임워크 5은  비동기 컴포넌트들과 라이브러리들을 통해 
배압(backpressure)을 전달하기 위한 계약(constract)으로써  Reactive Stream를 포용합니다.(embrace)
Reactive Stream은 Java 9에서도 java.util.concurrent.Flow 로 채택한 
업계간의 협업(industry collaboration)을 통해 생성된 명세(specification)입니다.
스프링 프레임워크은 내부 반응형 지원을 위해 내부적으로 Reactor를 사용합니다.
Reactor는 0..N 과 0..1 의 데이터 순서(sequences)상에서 선언적인 동작들을 제공하기 위한 
Flux 와 Mono composable API 타입을 갖는 기본 Reactive Streams Publisher contract을 
확장한 반응형 스트림 구현체 입니다.

스프링 프레임워크는 수 많은 반응형 API 의 Flux 와 Mono를 표출합니다.
어플리케이션 레벨에서 하지만, 매번, 스프링은 선택과 RxJava의 사용에 완전한 지원을 제공합니다.
보다 많은 Rective types에 대해서는 "Understanding Reative Types"(by Sebastien Deleuze) 포스트를 참고하세요.

2) Spring WebFlux Module

스프링 프레임워크 5은 새로운 spring-webflux을 포함합니다.
그 모듈은 reactive HTTP 와 REST, HTML browser 그리고 WebSocket style interactions이 포함된
reactive server web applications 뿐만 아니라, WebSocket 클라이언트에 대한 지원을 포함합니다.

2-1) Server Side

server-side Webflux상에서는 두개의 특징적인 프로그래밍 모델을 제공합니다.

 -  @Controller와 같은 어노테이션 기반과 Spring MVC에서 역시 지원하는 다른 어노테이션들
 -  Functional, Java 8 lambda 식 라우팅과 핸들링

프로그래밍 모델들 모두는 Reactive Streams API에 non-blocking HTTP runtimes을 적용한 동일한 반응형 기반에서 실행됩니다.
아래 다이어그램은 왼쪽에는 spring-webmvc 모듈 기반인 전통적인, 서블릿기반 Spring MVC가 포함된 server-side stack
그리고 오른쪽에 spring-webflux 모듈 기반인  reactive stack를 보여줍니다. 


WebFlux은 Servlet 3.1 Non-Blocking IO API 뿐만아니라, 
다른 비동기 런타임, Netty나 Undertow 같은 것과 마찬가지로 Servlet Container들 상에 구동 할 수 있습니다.
각각의 런타임은 reactive backpressure와 함께 InpuStream 와 OutputStream 보다는 차라리,
Flux<DataBuffer>로써 request 와 response의 body을 노출하는 reactive "ServerHttpRequest" 와 "ServerHttpResponse"에 적용됩니다.
REST-style JSON 과 XML 직렬화(serialization)와 비직렬화(deserialization)은 Flux<Object>로 지원되며, 
HTML view rendering 과 Server-Send Events 도 지원됩니다.

Annotation-based Programming Model

Spring MVC에서 사용되는 동일한 @Controller 프로그래밍 모델 과 동일한 어노테이션들 역시 WebFlux에서도 지원됩니다.
주요한 차이점은 핵심, 프레임워크 계약 - 예를 들어 HandlerMapping, HandlerAdapter들은 non-blocking 이고,   
HttpServletRequest 와 HttpServletResponse 상에서 동작하는 것보다 차라리 
반응형 ServerHttpRequest 와 ServerHttpResponse 상에서 동작하는 것입니다
(The main difference is that the underlying core, framework contracts - i.e. HandlerMapping , HadlerAdapter, are non-blocking and operate on the reactive ServerHttpRequest and ServerHttpResponse rather than on the Http ServletRequest and HttpServletResponse.) 
아래 reactive controller에 대한 예제가 있습니다.
@RestController
public class PersonController {

    private final PersonRepository repository;

    public PersonController(PersonRepository repository) {
        this.repository = repository;
    }

    @PostMapping("/person")
    Mono<Void> create(@RequestBody Publisher<Person> personStream) {
        return this.repository.save(personStream).then();
    }

    @GetMapping("/person")
    Flux<Person> list() {
        return this.repository.findAll();
    }
    @GetMapping("/person/{id}")
    Mono<Person> findById(@PathVariable String id) {
        return this.repository.findOne(id);
    }
}

HandlerFunctions

HTTP request가 유입하는 것은 ServerRequest을 가져오고  Mono<ServerResponse> 을 
리턴하는 필수적인 함수HandlerFunction에 의해 다뤄집니다. 
하나의 핸들러 함수에 annotation 카운터파트너는 @RequestMapping을 갖는 하나의 메소드가 됩니다.

ServerRequest 와 ServerResponse은 JDK-8이 HTTP messages들을 기반으로하는 것에 
친숙한(friendly) 접근을 제공하는 불변의 인터페이스들입니다.
둘다 Reactor위에 구축하여  완전히 반응합니다 : request는 Flux 또는 Mono 로 body를 표출합니다; response는 body로써 어떤 Reactive Streams Publisher를 받아들입니다.

ServerRequest는 다양한 HTTP request 요소들을에 접근을 제공합니다.
method, URI, query parameter 그리고 header들 (나눠진 ServerRequest.Headers 인터페이스을 통해서)
body에 접근은  body 메소드를 통해서 제공됩니다.
예를 들면, 아래는 어떻게 Mono<String>에서 request body을 어떻게 추출하는지 나타냅니다.
Mono<String> string = request.bodyToMono(String.class);

그리고, body의 contents로 부터 비직렬화 할수 있는 클래스인 Person이 있는 Flux안에서 body를 추출하는 방법을 나타냅니다. 
(예를 들어 body에 JSON 또는 만약 XML이면 JAXB가 포함되었다면, Person은 Jackson에 의해 지원됩니다.)
Flux<Person> people = request.bodyToFlux(Person.class);

위의 (bodyToMono 와 bodyToFlux) 두 메소드는 사실상 일반적인 ServerRequest.body(BodyExtractor) 메소드를 사용하는 편리한 메소드들 입니다.
BodyExtractor는 자식의 추출 로직을 작성하도록 하는 하나의 함수형 전략 인터페이스 입니다.
하지만 공통 BodyExtractor 인스턴스들은 BodyExtractors Utility 클래스에서 찾을수 있습니다.
그래서, 위의 예제들은 아래와 같이 대신할수 있습니다.
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

이와 유사하게, ServerResponse는 HTTP response 에 대한 접근을 제공합니다.
그것이 불변이므로, 여러분은 builder를 가지고 ServerResponse를 생성합니다.
builder는 response status을 설정하고, response header들을 추가하고, body을 제공합니다.
예를들어서, 아래는 200 OK status, JSON content-type 과 하나의 body를 response로 만드는 방법을 보여줍니다.
Mono<Person> person = ...
ServerResponse.ok().contentType(Media.APPLICATION_JSON).body(person);

201 status, Location header 그리고 빈 body 는
URI Location = ...
ServerResponse.created(location).build();

이걸 함께 놓는 것은 우리가 HandlerFunction을 생성하도록 합니다.
예를 들어서200 status 와 String 으로 구성된 body를 response로 리턴하는 
 "Hello World" hander lambda  예제가 있습니다. 
HandlerFunction<ServerResponse> helloWorld = 
    request -> ServerResponse.ok().body(fromObject("Hello World"));

핸들러 함수들을 람다식으로 작성하는 것은, 위에서 보듯이 편리합니다.
하지만 다수의 함수들과 함께 다뤄질때는 아마도 가독성이 부족하고 보다 적은 유지 보수성이 될것입니다.
그러므로, 하나의 핸들러 또는 컨트롤러 클래스안에 관련된 핸들러 함수들을 모아놓는 것을 추천합니다. 
예를 들면, 여기 클래스는 반응형 Person repository을 노출하는 클래스 입니다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listPeople(ServerRequest request) { 1
        Flux<Person> people = repository.allPeople();
        return ServerResponse.ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) { 2
        Mono<Person> person = request.bodyToMono(Person.class);
        return ServerResponse.ok().build(repository.savePerson(person));
    }
    public Mono<ServerResponse> getPerson(ServerRequest request) { 3
        int personId = Integer.valueOf(request.pathVariable("id"));
        Mono<ServerResponse> notFound = ServerResponse.notFound().build();
        Mono<Person> personMono = this.repository.getPerson(personId);
        return personMono
                .then(person -> ServerResponse.ok().contentType(APPLICATION_JSON).body(fromObject(person)))
                .otherwiseIfEmpty(notFound);
    }
}

1) listPeople은 JSON으로  저장소에 있는 찾아낸  모든 Person 객체들을 리턴하는 핸들러 함수 입니다. 
2) createPerson은 request body에 포함된 것을 새로운 Person 에 저장하는 핸들러 함수 입니다. 
   기억해야 할 것은 PersonRepository.savePerson(Person)이 person을 request로 부터 읽을수 있고 저장했을때, 
    완성된 신호를 분출하는 Mono<Void>: 빈 Mono을 리턴한다는 것입니다.
    그래서, 우리는 그 완성 신호를 받을때, response 를 보내기 위해서 build(Publisher<Void>) 메소드를 사용합니다. 
   예를  들면 Person이 저장되었을때 입니다. 
3)  getPerson은 path variable id 을 통해 구별되어지는 단일 person을 리턴하는 핸들러 함수 입니다. 
    이르는 저장소를 통해 Person, 그리고 만일 그게 발견되었다면, JSON response을 생성하고 주고 받습니다.
     만약 발견하지 못하면, 우리는 404 Not Fount response를 리턴하기 위해서 otherwiseIfEmpty(Mono<T>)을 사용합니다. 
   
RouterFunctions

 요청이 들어오는 것은 RouterFunction과 함께 hander function에서 라우팅됩니다. 
이건 ServerRequest를 받고, Mono<HandlerFunction>을 리턴하는 하나의 함수 입니다.
요청이 특정한 라우터에 매칭되면, handler function은 리턴됩니다.
그렇지 않으면, 빈 Mono가 리턴됩니다. 
RouterFunction은 @Controller 클래스에 @RequestMapping 어노테이션과 비슷한 목적을 가지고 있습니다.

일반적으로, 여러분은 스스로 rotuer  함수를 작성하지 않습니다. 
그렇지만 request predicate와 handler function을 사용하는 것을 만들기 위해,
RouterFunctions.router(RequestPredicate, HandlerFunction) 을 차라리 사용합니다.
predicate가 적용되면, 요청은 제공된 handler에  의해 라우팅됩니다.
그렇지 않으면 어떤 라우팅도 동작하지 않습니다.
결과적으로 404 Not Found 응답을 리턴하게 되겠습니다.
여러분 스스로의 RequestPredicate을 작성할수 있음에도, 여러분은 그렇게 하지 말아야 합니다.
RequestPredicates utility 클래스는 경로 HTTP method, content-type 등등에 기반하여 매칭하는 공통으로 사용되는 predicates을 제공합니다.
route을 사용하는 것은, 우리가 우리 "Hello World" handler function에 라우팅 할수 있습니다.
 RouterFunction<ServerResponse> helloWorldRoute = 
          RouterFunctions.route(RequestPredicates.path("/hello-world"),
          request -> Response.ok().body(fromObject("Hello World")));
두개의 router function은 둘중에 하나의 handler function에 라우팅 하는 새로운 router function으로 구성할수 있습니다.
만약 첫번째 router predicate가 일치하지 않았다면, 두번째가 될것입니다.(be evaluated, 평가되다.)
구성된 router function은 순서대로 값을 구하게 됩니다. 
그래서 일반적인 함수 전에, 특정 함수를 놓는 것이 합리적입니다. 
(so it makes sense to put specific functions before generic ones)
여러분은 RouterFunctions.route()와 함게 RouterFunction.and() 의 편리한 결합인 RouterFunction.and(RouterFunction)나 또는 RouterFunction.andRoute(RequestPredicate, HandlerFunction) 을 호출하는 것에 따라, 두개의 router function을 구성 할 수 있습니다.

위에서 보여진, 주어진 PersonHandler, 우리는 지금 각자의(respective) handler functions에 
라우팅 하는 router function을 정의할수 있습니다. 
우리는 handler functions을 참조하는 method-references을 사용합니다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> personRouter = 
     route(GET("/person/{id}").and(accpt(APPLICATION_JSON)), handler::getPerson)
       .andRoute(GET("/person").and(accpt(APPLICATION_JSON)), handler::listPeople)
       .andRoute(POST("/person").and(contentType(APPLICATION_JSON)), handler::createPerson);
router function 옆으로, RequestPredicate.and(RequestPredicate) 나 RequestPredicate.or(RequestPredicate)을 호출하는 것으로 또한 request predicates을 구성할수 있습니다.
이건 예상한대로 동작합니다. 
"and"의 predicate 결과는 주어진 predicate 양자 모두 일치하면 일치합니다.
(for "AND" the resulting predicate matches if both given predicates match)
"or는 둘중에 하나의 predicate가 일치하면 일치합니다.
RequestPredicates에서 발견된 predicates의 대부분은 조합체들입니다.
예를 들어 RequestPredicates.GET(String)은 RequestPredicates.method(HttpMethod) 와 RequestPredicates.path(String) 의 조합입니다.

Running a Server

이제 잃어버진 퍼즐의 한 조각만 남아있습니다.
HTTP 서버에서 router function을 구동하는 것입니다.
RouterFunctions.toHttpHandler(RouterFunction)을 사용하여 HttpHandler 안에 router function을 변환 할수 있습니다.
HttpHandler는 여러분에게 Reactor Netty, RxNetty, Servlet 3.1+ , Undertow과 같은 reactive runtimes의 넓고 다양하게 구동하도록 합니다.
여기서는 Reactive Netty에서 router function을 어떻게 구동하지는 나타냅니다.
예를 들면 : 
RouterFunction<ServerResponse> route = ...
HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create(HOST, PORT);
server.newHandler(adapter).block();
톰캣같은 경우는 아래와 같습니다.
RouterFunction<ServerResponse> route = ...
HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat server = new Tomcat();
Context rootContext = server.addContext("", System.getProperty("java.io.tmpdir"));
Tomcat.addServlet(rootContext, "servlet", servlet);
rootContext.addServletMapping("/", "servlet");
tomcatServer.start();
TODO : DispatcherHandler

으잉 아직도 정리가 안된 것이 있네... 헐..

HandlerFilterFunction

 router function에 의해 매핑된 Route는 ServerRequest 와 HandlerFunction 그리고 ServerResponse을 리턴하는 HandlerFilterFunction은 본질적으로 하나의 함수인 곳인 RouterFunction.filter(HandlerFilterFunction)을 호출함에 따라 필터링 할수 있습니다.
그 핸들러 함수 파라미터는  체인에서 다음 요소를 나타냅니다.
이것은 일반적으로 라우팅 하는 곳을 나타내는 HandlerFunction 입니다.
하지만 다수의 필터가 적용된 경우에는 또한 다른 FilterFunction일 수도 있습니다.
어노테이션과 함께 비슷한 기능은 @ControllerAdvice 그리고/또는 ServletFilter을 사용하여 얻을수 있습니다.
특정경로는 허가할지 말지 결정할 수 있는 SecurityManager을 가지고 있다고 가정하고, 
간단한 보안 필터를 우리 라우터에 적용해보도록 합시다.


import static org.springframework.http.HttpStatus.UNAUTHORIZED;

SecurityManager securityManager = ...
RouterFunction<ServerResponse> router = ...

RouterFunction<ServerResponse> filteredRoute = 
     route.filter(request, next) -> {
           if(securityManager.allowAccessTo(request.path()) {
                 return next.handle(request);
           } else {
                 return ServerResponse.status(UNAUTHORIZED).build();
           }
});
보다시피, 이 예제에서 next.handle(ServletRequest)을 발생시키는 것은 옵션입니다.
오직 handler function이 접근이 허용될때  실행됩니다.

2-2) Client Side 

WebFlux는 완전한 non-blocking 과 반응형 RestTemplate에 선택적인 반응형을 제공하는 
함수형, 반응형 WebClient 을 포함합니다.
request와 response의 body는  InputStream 과 OutputStream 이 아닌  
Flux<DataBuffer>인 반응형 ClientHttpRequest 와 ClientHttpResponse으로 네트워크 유입과 유출을 노출합니다.
추가적으로 같은 반응형 JSON, XML , SSE serialization mechanism을  서버상에서  지원합니다.
그래서 여러분은 typed objects을 가지고 작업 할수 있습니다.
아래는 Reactor Netty와 같은 특정 HTTP 클라이언에 플러그인 하기 위해 
 ClientHttpConnector 구현이 필요한 WebClient을 사용한 예제 입니다.
WebClient client = WebClient.create("http://example.com");

Mono<Account> account = client.get()
                            .url("/accounts/{id}", 1L)
                            .accept(APPLICATION_JSON)
                            .exchange(request)
                            .then(response -> response.bodyToMono(Account.class));
  • AsyncRestTemplate 또한 non-blocking interaction을 지원합니다.
    주요 차이점은  non-blocking streaming을 지원할수 없다는 점입니다.
    Twitter one(https://dev.twitter.com/streaming/overview) 가 같은 예제와 같습니다.
    근본적으로(fundamentally) 이건 아직까지 InputStream 과 OutputStream에 기반하고 의존하고 있습니다. 

2-3) Request and Response Body Conversion


spring-core 모듈은 typed Object 로의 바이트의 Flux의 직렬화가 가능하게 하는 
reactive Encoder 와 Decoder 계약(contract)을 제공합니다.
spring-web 모듈은 JSON(Jackson) 과 XML(JAXB) 구현체를 포함하고 있습니다.
웹 어플리케이션에서 사용을 위한 것 뿐만 아니라, SSE 스트리밍 과 zero-copy 파일 전송을 위한 다른것도 마찬가지입니다.

예를 들어, request body는 아래 방법을 따르는 것중에 하나가 될수 있고,
어노테이션 과 함수형 프로그래밍 모델 둘 모두에서 자동으로 디코딩 될것입니다.

- Account count : 컨트롤러가 발생되기 전에 블로킹없이 account는 비직렬화됩니다. 
- Mono<Account> account : 컨트롤러는 account가 비직렬화된 후에 실행될  로직을 선언하기 위해서,
      Mono을 사용할 수 있습니다. 
- Single<Account> account : Mono와 같지만, RxJava를 사용하는 것입니다.
- Flux<Account> accounts : input streaming scenario.
- Observable<Account> accounts : input streaming 입니다. RxJAva을 사용하죠

Response body는 아래 중에 가 하나가 될수 있습니다.

- Mono<Account> - Mono가 완성될때, 주어진 Account를 블로킹 하는 것 없이 직렬화합니다.
- Single<Account> - Mono 인데, RxJava 에서 사용합니다.
- Flux<Account> - 스트리밍 시나리오 입니다. 요청한 컨텐츠 타입에 따라 가능한 SSE 
- Observable<Account> : Flux와 같은데 RaJava Observable 타입 을 사용
- Flowable<Account> : Flux 와 같은데 RxJava 2 Flowable 타입을 사용
- Flux<ServerSentEvent> : SSE 스트리밍 
- Mono<Void> : request 핸들링은 Mono가 완성될때 완성합니다.
- Account - 주어진 Account 를 블로킹하는 것 없이 직렬화 합니다. 동기화, non-blocking 컨트롤러 메소드를 의미합니다.(imply). 
- void : 어노테이션 기반 프로그래밍 모델에 대한 명시, request 핸들링이 메소드가 리턴될때 완성합니다. 동기화, non-blocking 컨트롤러 메소드를 의미합니다.(imply).

FLux 나 Observable 과 같은 스트림 타입을 사용할 때, request/response에 명시되는 media 타입이나 mapping/routing 레벨에서는 어떻게 데이터가 직렬화 하고 flush되어야 하는지에 대한 결정을 하는데 사용됩니다.
예를 들면 REST 종단점, Flux<User>가 리턴되는 것은 아래와 같이 기본적으로 직렬화 될것입니다.

- application/json: Flux<User>는 비동기적인 컬렉션으로 다뤄지고, complete 이벤트가 발생될때, 명확한 출력을 갖는 JSON 배열로 직렬화됩니다.
- application/stream+json : Flux<User>는 개별적 JSON객체로 새로운 라인으로 구별되어 직렬화된 User 요소의 스트림으로 다뤄지고, 각각의 요소 뒤에 명확하게 출력됩니다.
WebClient은 JSON stream decoding을 지원합니다. 따라서 이것은 서버 to 서버 경우에 좋은 사용 케이스 입니다.
- text/event-steam : Flux<User> 또는 Flux<ServerSentEvent<User>>는 
 각각의 요소 뒤에, 데이터 인코딩과 명백한 출력을 위한 기본으로 JSON을 사용하여 개별적 SSE 요소들로 직렬화한 User 또는 ServerSentEvent 요소 의 스트림으로 다뤄질것입니다.
 이것은 브라우저 클라이언트 스트림을 노출하는 용으로 매우 적합합니다. 
WebClient은 마찬가지로, SSE 스트림들을 읽는 것을 지원합니다. 

2-4) Reactive WebSocket Support

WebFlux은 reactive WebSocket client 와 server 지원을 포함합니다.
서버와 클라이언트 모두다 Java WebSocket API(JSR-356), Jetty, Undertow, Reactor Nettt, RxNetty 상에서 지원합니다.

서버측에서, WebSocketHandlerAdapter을 선언하고, 그리고, 간단하게 WebSocketHandler 기반 종단점에 매핑하는 것을 추가합니다.
@Bean
public HandlerMapping webSocketMapping() { 
   Map<String, WebSocketHandler> map = new HashMap<>();
   map.put("/foo", new FooWebSocketHandler());
   map.put("/bar", new BarWebSocketHandler());

   SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
   mapping.setUrlMap(map);
   return mapping;
}

@Bean
public WebSocketHandlerAdapter handlerAdapter() {
   return new WebSocketHandlerAdapter();
}
클라이언트측에서는 위의 목록화된 라이브러리를 지원하는 것 중에 하나를 위한 WebSocketClient을 생성합니다. 
 WebSocketClient client = new ReactorNettyWebSocketClient();
client.execute("ws://localhost:8080/echo"). session -> { ... }).blockMillis(5000);
2-5) Testing

spring-test 모듈은 구동되는 서버가 있거나 없거나, WebFlux 서버 종단점을 테스트하는데 사용할수 있는 WebTestClient을 포함합니다.
구동하는 서버가 없는 테스트는 mock request 와 response는 소켓을 사용하여 네트워크상에 연결하는 것 대신 사용되는  Spring MVC에서의 MockMvc와 비교됩니다.
WebTestClient은 하지만 또한 구동하는 서버를 대신해해 테스트를 동작시킬수도 있습니다.

이 프레임워크에 대한 샘플 테스트는 아래에서 보시면 더 많은 정보를 캐치할수 있습니다.



3) Getting Started

3-1) Spring Boot Starter

http://start.spring.io을 통해 사용가능한 Spring Boot Web Reactive starter는  시작하는데 가장 빠른 방법입니다.
여기서 필수적인  모든 것을 하며, Spring MVC을 가지고 한것과 마찬가지로,  여러분은 @Controller 클래스들을 작성하는 것을 시작하시면 됩니다. 
간단하게 http://start.spring.io 에 가서, 2.0.0.BUILD-SNAPSHOT을 선택하고, dependecies 박스에서 reactive을 입력합니다.
기본적으로 starter는 Tomcat과 동작합니다. 하지만, 여느때처럼 Spring Boot을 가지고 의존관계는 변경할수 있습니다.
보다 상세하고, 구조를 보려면 starter 페이지를 보시길 바랍니다.

이 starter는 또한 함수형 web API을 지원하고, 자동으로 RouterFunction beans을 감지합니다.
여러분의 Spring Boot WebFlux 어플리케이션은 RouterFunction이나 RequestMapping 접근(approach)을 사용해야 합니다.
현재 그것들끼리 섞는 것이 가능하지 않습니다.

3-2) Manual Bootstrapping

이 섹션은 수동으로 설정하고 구동하는 단계에 대해 알려줍니다.
(This section outlines the steps to get up and running manually.)
의존관계는 spring-webflux 와 spring-context와 함께 시작합니다.
그다음 jackson-databind 와 io.netty:netty-buffer (임시로 SPR-14528을 보세요)를 JSON 지원을 위해 추가하세요.
마지막으로 지원되는 런타임중에 하나를 추가하시면 됩니다.

Tomcat : org.apache.tomcat.embed:tomcat-embed-core
Jetty : org.eclipse.jetty:jetty-server and org.eclipse.jetty:jetty-servlet
Reactor Netty : io.projectreactor.ipc:reactor-netty
RxNetty : io.reactivex:rxnetty-common and io.reactivex:rxnetty-http
Undertow : io.undertow:undertow-core

어노테이션 기반 프로그래밍 모델 부트스트랩은 아래와 같습니다.
ApplicationContext context = new AnnotationConfigApplicationContext(DelegatingWebFluxConfiguration.class);  // (1)
HttpHandler handler = DispatcherHandler.toHttpHandler(context);  // (2)
(1)은 기본 Spring Web Framework configuration을 로드 합니다.
그 다음에 DispatcherHandler를 생성합니다.
(2) request 처리하는 것을 운행하는 메인 클래스, 그리고 그것을 HttpHandler - 반응형 HTTP request 핸들링을 위한 가장 낮은 단계의 Spring 추상화 에 적용합니다.

함수형 프로그래밍 모델 부트스트랩은 아래와 같습니다.
ApplicationContext context = new AnnotationConfigApplicationContext(); // (1)
context.registerBean(FooBean.class, () -> new FooBeanImpl()); // (2)
context.registerBean(BarBean.class); // (3)
HttpHandler handler = WebHttpHandlerBuilder
        .webHandler(RouterFunctions.toHttpHandler(...))
        .applicationContext(context)
        .build(); // (4)
(1)새로운 함수형 빈즈 등록 API의 장점을 취할수 있는 AnnotationConfigApplicationContext 인스턴스를 생성합니다.
(2) Java 8의 Supplier을 사용하는 beans을 등록하기 위하거나, 또는 단지 그의 클래스를 명시하는 것 (3)
HttpHandler는 WebHtttpHandlerBuilder(4)를 사용하여 생성됩니다.

HttpHandler는 그 다음 지원하는  런타임 중에 하나에서 설치될 수 있습니다.
// Tomcat and Jetty (also see notes below)
HttpServlet servlet = new ServletHttpHandlerAdapter(handler);
...
// Reactor Netty
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create(host, port).newHandler(adapter).block();
// RxNetty
RxNettyHttpHandlerAdapter adapter = new RxNettyHttpHandlerAdapter(handler);
HttpServer server = HttpServer.newServer(new InetSocketAddress(host, port));
server.startAndAwait(adapter);
// Undertow
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
  • Servlet 컨테이너, 특별히 WAR 배포을 갖는 것은, 여러분이 
    AbstractAnnootationConfigDispatcherHandlerInitializer을 사용할수 있습니다.
    이건 WebApplicationInitializer로써 그리고Servlet 컨테이너에 의해  auto-detected 됩니다.
    위에서 보여진대로, ServletHttpHandlerAdapter을 등록하는 것을 처리합니다.
    여러분은 하나의 추상 메소드를 여러분의 Spring configuration에 가르키기위해 구현해야할 필요가 있을것입니다.

3-3) Example

Spring Boot Web Reactive Starter :
 sources of  the reactive starter available at http://start.spring.io

여기에서 Quickstart를 따라가면, Spring Boot version은 2.0.0(SNAPSHOT) 으로 하고,
Reactive Web starter를 추가하면 끝.
우선 gradle 로 선택해서 프로젝트를 열어서 dependencies을 auto-import 하면, 
build.gradle 은 아래와 같다.

buildscript {
ext {
springBootVersion = '2.0.0.BUILD-SNAPSHOT'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
mavenCentral()
}


dependencies {
compile('org.springframework.boot:spring-boot-starter-webflux')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('io.projectreactor:reactor-test')
}
그리고 Application.class 는 별반 다를것이 전혀 없다.

package com.example.webfluxexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebfluxexampleApplication {

public static void main(String[] args) {
SpringApplication.run(WebfluxexampleApplication.class, args);
}
}
동작시켜보면, 기본 port는 8080 으로 http://localhost:8080/
routing 해 놓은 것이 하나도 없으니, 404 가 리턴 될것이다.


그럼 따라하라고 하는 예제를 기반으로 확장을 해보도록 한다. 
WebFlux sample apps in the spring boot repository 
이 많은 예제 중에,

우선 이것 저것 다 확인 해봤는데, WelcomeController 부터 봅시다. 
package com.example.webfluxexample;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
    @GetMapping("/")
    public String welcome(){
        return "Hello world!!!!!!!!!!!!!!!!!!";
    }
}
으헉 ... 끝입니다.
get 방식으로 해당 매핑 localhost/ 하면 body 단에 Hello world!!!!!!!!!!!!! 가 나타나죠.

두번째,  post 방식에 여러가지 제한을 걸어 보는 것이 있습니다.
ExampleController 입니다.

package com.example.webfluxexample;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExampleController {
    @PostMapping(
            path = "/" ,
            consumes = { MediaType.APPLICATION_JSON_VALUE , "!application/xml" },
            produces = MediaType.TEXT_PLAIN_VALUE,
            headers = "X-Custom=Foo",
            params = "a!=alpha" )
    public String example(){
        return "Hello world";
    }
}
자, application/json 만 허용하며, header에 X-Custom=Foo 가 있어야 하구,
파라미터에는 a=alpha 를 제외한 모든 값이 post 로 매핑되었습니다.
return으로 hello world! 가 body값에 전달 되도록 합니다.

마지막으로는 Handler를 따로 빼서 처리하는 것입니다.
말그대로, handler가 요청에 대한 별도 처리를 할 경우 사용해야 하며, 제가 많이 사용할 것 같습니다.

package com.example.webfluxexample;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class EchoHandler {
    public Mono<ServerResponse> echo(ServerRequest request){
        System.out.println(request.toString());
        System.out.println(request.headers().toString());
        return ServerResponse.ok().body(request.bodyToMono(String.class), String.class);
    }
}
핸들러 컴포넌트를 생성합니다.
앞서 이야기 한 ServerRequest를 인자로 해서 값을 받고, 리턴은 Mono<ServerResponse> 을 리턴합니다.
ServerResponse.ok() 는 말그대로 return status가 200  인 값이며,
.body 에 request.bodyToMono(String.class) 값과 String.class 두개의 인자를 포함해서 리턴을 하면,
요청시에 받은 body를 그대로 response에 전달하게 됩니다.

그럼 이 handler를 어디에 적용할까요? application.class에 넣어두었네요.

package com.example.webfluxexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@SpringBootApplication
public class WebfluxexampleApplication {

public static void main(String[] args) {
SpringApplication.run(WebfluxexampleApplication.class, args);
}


@Bean
public RouterFunction<ServerResponse> monoRouterFunction(EchoHandler echoHandler) {
return route(POST("/echo"), echoHandler::echo);
}
}

@Bean 어노테이션으로 monoRouterFunction 함수가 작성되어 있습니다.
인자값으로는 앞서 만든 핸들러인 EchoHandler가 있습니다. 
리턴값은 RouterFunction<ServerResponse> 가 되며,
route(POST("/echo"), echoHandler::echo) 로 리턴됩니다.
뭐가 되었던 간에 구성해 놓은대로 실행하면 정상 동작하는 것을 볼수 있습니다. 

마지막으로 Test 입니다.

package com.example.webfluxexample;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebfluxexampleApplicationTests {

    @Autowired
    private WebTestClient webClient;

    @Test
    public void testWelcome() {
        this.webClient.get().uri("/").accept(MediaType.TEXT_PLAIN).exchange()
                .expectBody(String.class).isEqualTo("Hello world!!!!!!!!!!!!!!!!!!");
    }

    @Test
    public void testExample() {
        this.webClient.post().uri("/")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_PLAIN)
                .header("X-Custom","Foo")
                .exchange()
                .expectBody(String.class).isEqualTo("Hello world");
    }

    @Test
    public void testEcho() {
        this.webClient.post().uri("/echo")
                .contentType(MediaType.TEXT_PLAIN)
                .accept(MediaType.TEXT_PLAIN)
                .body(Mono.just("Hello WebFlux!"), String.class)
                .exchange()
                .expectBody(String.class).isEqualTo("Hello WebFlux!");
    }

    @Test
    public void testActuatorStatus(){
        this.webClient.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON)
                .exchange().expectStatus().isOk().expectBody()
                .json("{\"status\":\"UP\"}");
    }

}



Functional Programming model sample