스프링캠프 2017[Day1 A3]: Spring WebFlux 발표 영상을 사실상 받아쓰기 함.
- 구 Spring-Web-Reactive
- 스프링5의 메인 테마는 원래 JDK9이었는데 이제는 WebFlux로 바뀜
- 스프링 리액티브 스택의 웹 파트 담당
- 비동기-논블록킹 리액티브 개발에 사용
- 효율적으로 동작하는 고성능 웹 어플리케이션 개발
- 서비스간 호출이 많은 마이크로서비스 아키텍처에 적합
2가지 개발 방식
- 기존의 어노테이션을 활용한 @MVC방식
- @Controller, @restController ... 와 유사(똑같진 않음)
- 새로운 함수형 모델
- RouterFunction
- HandlerFunction
새로운 요청-응답 모델
- 서블릿 스택과 API에서 탈피
- 서블릿 API는 리액티브 함수형 스타일에 적합하지 않음
- ServerRequest와 ServerResponse라는 새로운 모델
지원 웹 서버/컨테이너
- Servlet 3.1+ (Tomcat, Jetty, ...)
- 서블릿 3.1+의 비동기-논블록킹 요청 처리 기능 사용
- Netty
- Undertow
- 요청 매핑
- 웹 요청을 어느 핸들러로 보낼지 결정
- URL, 헤더
- @RequestMapping (기존의 MVC)
- 요청 바인딩
- 핸들러에 전달된 웹 요청 준비
- 웹 URL, 헤더, 쿠키, 바디
- 핸들러 실행
- 전달 받은 요청 정보를 이용해 로직을 수행하고 결과를 리턴
- 핸들러 결과 처리(응답 생성)
- 눈에 보이지 않지만 핸들러의 리턴 값으로 웹 응답 생성
@RestContoller
public class MyController {
@GetMapping("/hello/{name}")
// 요청 매핑
String hello(@PathVariable String name) { // 요청 바인딩
return "Hello" + name; // 핸들러 결과 처리 (응답 생성)
}
}
- 함수형 스타일 요청 매핑
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
Mono<HandlerFunction<T>> route(ServerRequest request);
}
ServerRequest request: 웹 플럭스 버전의 웹 요청
Mono<HandlerFunction>: 웹 플럭스 버전의 웹 응답인 ServerResponse나 그 서브타입의 Mono 퍼블리셔를 리턴하는 HandlerFunction의 Mono 타입...
- 함수형 스타일의 웹 핸들러(컨트롤러 메소드)
- 웹 요청을 받아 웹 응답을 돌려주는 함수
- 요청 매핑 - RouterFunction
- 요청 바인딩 - HandlerFunction
- 핸들러 실행 - HandlerFunction
- 핸들러 결과 처리(응답 생성) - HandlerFunction
- HandlerFunction을 먼저 만들고
- RouterFunction에서 path기준 매핑을 함
HandlerFunction helloHandler = req -> {
String name = req.pathVariable("name"); // ServerRequest.pathVariable()로 {name} 추출
Mono<String> result = Mono.just("Hello"+name); // 로직 적용 후 결과 값을 Mono에 담는다
Mono<serverResponse> res = ServerResponse.ok().body(result, String.class);
// 웹 응답을 ServerResponse로 만든다
// Http 응답에는 응답코드, 헤더, 바디가 필요
// ServerResponse의 빌더를 활용
// Mono에 담긴 ServerResponse 타입으로 리턴
return res;
}
위는 HandlerFunction.handle()의 람다식
Mono<T> handle(ServerRequest request);
HandlerFunction helloHander = req ->
ok().body(fromObject("Hello" + req.pathVariable("name")));
함수형 스타일의 코드는 간결하게 작성할 수도 있다 함수형 방식에서는 명시적으로 path variable을 가져옴 content-type이나 기타 헤더는 스프링에서 알아서 넣어줌.
RouterFunction router = req ->
RequestPredicates.path("/hello/{name}").test(req)? // 웹 요청 정보 중에서 URL경로 패턴 검사
Mono.just(helloHandler) // 조건에 맞으면 핸들러 함수를 Mono에 담아서 반환
: Mono.empty(); // 조건에 맞지 않으면 Mono반환 함수니까 뭐라도 반환해야 함
- RouterFunctions.route(predicate, handler)
RouterFunction router =
RouterFunctions.route(RequestPredicates.path("/hello/{name}"), // RouterFunction의 매핑 조건을 체크하는 로직만 발췌
req -> ServerResponse.ok().body(fromObject("Hello" + // HandlerFunction은 그대로
req.pathVariable("name"))));
- RouterFunction 타입의 @Bean으로 만든다
@Bean
RouterFunction helloPathVarRouter(){
return route(RequestPredicates.path("/hello/{name}"),
req->ServerResponse.ok().body(fromObject("Hello"+
req.pathVariable("name"))));
}
- 핸들러 코드만 람다 식을 따로 선언하거나
- 메소드를 정의하고 메소드 참조로 가져온다
HandlerFunction handler = req -> {
String res = myService.hello(req.pathVariable("name"));
return ok().body(fromObject(res));
}; // 다른 bean 호출을 포함한 복잡한 로직을 담은 람다식
return route(path("/hello/{name}"), handler);
@Component
public class HelloHandler {
@Autowired MyService myService;
Mono<ServerResponse> hello(ServerRequest req){
String res = myService.hello(req.pathVariable("name"));
return ok().body(fromObject(res));
}
}
@Bean
RouterFunction helloRouter(@Autowired HelloHandler helloHandler){
return route(path("/hello/{name}"), helloHandler::hello);
}
handlerFunction은 앞서 말했듯이 람다식으로 사용가능. 람다식은 메소드 타입이 일치하면 일반 메소드와 호환해서 사용가능.
그래서 앞의 람다식과 동일한 메소드 타입을 가진 메소드를 만들어 주입할 수 있음.
위의 방법의 단점은 bean 메소드 하나에 handlerFunction을 딱 하나만 연결 해 줄 수 있음.
- 핸들러 하나에 @Bean하나씩 만들어야 하나?
- RouterFunction의 and(), andRoute()등으로 하나의 @Bean에 n개의 RouterFunction을 선언할 수 있다.
- 타입 레벨 ~ 메소드 레벨의 @RequestMapping 처럼 공통의 조건을 정의하는 것 가능
- RouterFunction.nest()
public RouterFunction<?> routingFunction(){
return nest(pathPrefix("/person"), // URL이 /person으로 시작하는 조건을 공통으로
nest(accept(APPLICATION_JSON), // APPLICATION_JSON을 accept하는 공통조건 중첩
route(GET("/{id}"),handler::getPerson)
.andRoute(method(HttpMethod.GET), handler::listPeople)
).andRoute(POST("/").and(contentType(APPLICATION_JSON)),
handler::createPerson));
}
- 모든 웹 요청 처리 작업을 명시적인 코드로 작성
- 메소드 시그니처 관례와 타입체크가 불가능한 애노테이션에 의존하는 @MVC스타일보다 명확
- 정확한 타입 체크 가능
- 함수 조합을 통한 편리한 구성, 추상화에 유리
- 테스트 작성의 편리함
- 핸들러 로직은 물론이고 요청 매핑과 리턴 값 처리까지 단위테스트로 작성 가능
- 함수형 스타일의 코드 작성이 편하지 않으면 코드 작성과 이해 모두 어려움
- 익숙한 방식으로도 가능한데 뭐하러~
- 애노테이션과 메소드 현식의 관례를 이용하는 @MVC방식과 유사
- 비동기 + 논븐로킹 리액티브 스타일로 작성
- Webflux의 기본 요청, 응답 인터페이스 사용
- 함수형 WebFlux의 HandlerFunction을 메소드로 만들었을 때와 유사
- 매핑만 애노테이션 방식을 이용
예시 코드
@RestController
public static class MyController{
@RequestMapping("/hello/{name}")
Mono<ServerResponse> hello(ServerRequest req){
return ok().body(fromObject(req.pathVariable("name"))); // 메소드로 재정의된 HandlerFunction
}
}
- 가장 대표적인 @MVC WebFlux 작성 방식
- 파라미터 바인딩은 @MVC 방식 그대로
- 핸들러 로직 코드의 결과를 Mono/Flux 타입으로 리턴
@GetMapping("/hello/{name}")
Mono<String> hello(@PathVariable String name){
return Mono.just("Hello" + name);
}
@RequestMapping("/hello")
Mono<String> hello(User user){ // 커맨드 오브젝트, 모델 오브젝트 바인딩 / URL 파라미터 또는 form-data
return Mono.just("Hello" + user.getName());
}
- T
- Mono
- Flux
@RequestMapping("/hello")
Mono<String> hello(@RequestBody User user){ //웹 요청의 body를 MessageConverter에서 바인딩(@MVC와 동일)
return Mono.just("Hello" + user.getName());
}
@RequestMapping("/hello")
Mono<String> hello(@RequestBody Mono<User> user){ //반환된 오브젝트를 Mono에 담아서 전달
return user.map(u->"Hello" + u.getName()); //Mono의 연산자를 사용해서 로직을 수행하고 Mono로 리턴
}
@RequestMapping("/hello")
Flux<String> hello(@RequestBody Flux<User> user){ //Flux같은 경우는 오브젝트 하나로 받기 보다는 데이터 스트림을 클라이언트로 받을 때 사용
return user.map(u->"Hello" + u.getName()); //User의 스트림 형태로 로직 실행
}
- T
- Mono
- Flux
- Flux
- void
- Mono
- 비동기-논블록킹 구조의 장점은 블록킹IO를 제거하는 데서 나온다
- HTTP서버가 클라이언트로 부터 응답을 받는 것은 예전부터 논블럭킹 방식이었음. (거의 톰캣 6, 서블릿2시절부터도)
- 데이터 액세스 리포지토리
- HTTP API 호출
- 기타 네트워크 사용 서비스
- 현재는 답이 없다.
- 블로킹 메소드로 점철된 JDBC API
- 일부 DB는 논블록킹 드라이버가 존재하지만...
- @Async 비동기 호출과 CFuture를 리액티브로 연결하고 쓰레드풀 관리를 통해서 웹 연결 자원을 효율화하는 정도
- 리포지토리 소드의 리턴 값을 @Async 메소드 처럼 작성
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
// 리포지토리 메소드를 비동기로 실행하고 결과를 CompletableFuture로 돌려받는다.
// @Async이므로 비동기 실행과 동시에 메소드 리턴
@GetMapping
Mono<User> findUser(String name){
return Mono.fromCompletionStage(myRepository.findOneByFirstName(name));
}
// DB를 액세스 하는 구간 동안은 블로킹이 일어나지만 서블릿 쓰레드를 점유하지 않은채로 동작 가능
- 스프링 데이터의 리액티브 리포지토리 이용
- MongoDB
- Cassandra
- Redis
- ReactiveCrudRepository 확장
- AsyncRestTemplat의 리액티브 버전
- 요청을 Mono/Flux로 전달할 수 있고
- 응답을 Mono/Flux 형태로 가져옴
- 각 단계의 타입이 보이지 않기 때문이다.
- 타입이 보이도록 코드를 재구성하고 익숙해지도록 연습이 필요함
- Webflux
-
- 리액티브 리포지토리
-
- 리액티브 원격 API 호출
-
- 리액티브 지원 외부 서비스 (메시지 큐 등등)
-
- @Async 블록킹 IO
-
- 코드에서 블록킹 작업이 발생하지 않도록 Flux스트림 또는 Mono에 데이터를 넣어서 전달
- 함수형 스타일 코드를 이용해 간결하고 읽기 좋고 조합하기 편한 코드 작성
- 데이터 흐름에 다양한 오퍼레이터 적용
- 연산을 조합해서 만든 동시성 정보가 노출되지 않는 추상화된 코드 작성
- 동기, 비동기, 블록킹, 논블록킹 등을 유연하게 적용
- 데이터의 흐름의 속도를 제어할 수 있는 메커니즘 제공
- 시스템 외부에서 발생하는 이벤트에도 유용
- 클라이언트로부터의 이벤트에도 활용 가능
- WebFlux가 사용하는 Reactor외에 RxJava2를 비롯한 다양한 리액티브 기술에 적용된 표준 인터페이스
- 다양한 기술, 서비스 간의 상호 호환성에 유리
- 자바9에 Flow API로 포함