spring camp 2017 about spring web flux
2022, Oct 03
spring webflux async
강의
https://www.youtube.com/watch?v=2E_1yb8iLKk (발표자: 이일민)
스프링 5.0 webflux소개와 개발방법
- 리액티브 프로그래밍이란 무엇인가
- 클라우드에서 리액티브 시스템 구축
- reactor, rxjava 사용법
용도
- 비동기-논블로킹 리액티브 개발에 사용
- 고성능 웹 애플리케이션 개발
- 서비스간 호출이 많은 마이크로 서비스 아키텍처에 적합
비동기-논블록킹 리액티브 웹 어플리케이션의 효과를 얻으려면
- WebFlux + 리액티브 리포지토리 + 리액티브 원격API호출 + 리액티브 지원 외부 서비스 + @Async 블로킹 IO
- 코드에서 중간 코드에 블록킹 작업이 발생하지 않도록 Flux 스트림 또는 Mono에 데이터를 넣어서 전달
리액티브 함수형은 꼭 성능 때문만?
- 함수형 스타일코드는 편한 코드 작성
- 데이터 흐름에 다양한 오퍼레이터 적용
- 연산을 조합해서 만든 동시성 정보가 노출되지 않는 추상화 코드 작성
- 동기, 비동기, 블록킹, 논블록킹 등을 유연하게 적용
- 데이터의 흐름의 속도를 제어할 수 있는 메커니즘 제공
장점 및 단점
- 장점
- 함수형 스타일의 장점
- 모든 웹 요청 처리 작업을 명시적인 코드로 작성
- 메소드 시그니처 관례와 타입 체크가 불가능한 어노테이션에 의존하는 MVC스타일보다 명확
- 정확한 타입 체크가능
- 함수 조합을 통한 편리한 구성, 추상화 유리
- 테스트 코드 작성의 편리함
- 핸들러 로직 및 요청 미팽과 리턴값 처리까지 단위테스트로 작성 가능
- 모든 웹 요청 처리 작업을 명시적인 코드로 작성
- 논블록킹 IO에만 효과적인것이 아니라 시스템 외부에서 발생하는 이벤트 및 클라이언트로부터의 이벤트에도 유용하다.
- 함수형 스타일의 장점
- 단점
- 함수형 스타일의 코드 작성이 편하지 않으면 코드 작성과 이해 모두 어려움
webflux를 개발하는 방식 2가지
- 기존의 @MVC 방식 - @Controller, @RestController, @RequestMapping
- 새로운 함수형 모델 - annotation을 사용하지 않고, routerfunction, handlerFunction사용
새로운 요청-응답 모델
- 서블릿 스택과 API에서 탈피
- 서블릿API는 리액티브 함수형 스타일에 적합하지 않음
- HttpServletRequest, HttpServletResponse → ServerRequest, ServerResponse인 추사황 모델로 대체
지원 웹 서버/컨테이너
- Servlet 3.1+ (Tomcat, Jetty, …) - 서블릿 3.1+의 비동기-논블로킹 요청 처리가능
- netty
- undertow
함수형 스타일 webflux(RouterFunction + HandlerFunction)이란
함수형 webflux가 웹 요청을 처리하는 방식 예제
- 요청 매핑 → RouterFunction이 담당
// HandlerFunction 구현
HandlerFunction helloHandler = req -> {
String name = req.pathVariable("name");
Mono<String> result = Mono.just("Hello " + name);
Mono<ServerResponse> res = ServerResponse.ok().body(result, String.class); // ServerResponse의 빌더를 활용
return res; // Mono에 담긴 ServerResponse타입으로 리턴
}
// 또는
HandlerFunction helloHandler = req ->
ok().body(fromObject("Hello " + req.pathVariable("name")));
----
// RouterFunction 구현
RouterFunction router = req ->
RequestPredicates.path("/hell/{name}").test(req) ? // url경로 패턴 검사
Mono.just(helloHandler) : Mono.empty();
(1) RouterFunction
함수형 스타일의 요청 매핑
@FuctionalInterface public interface RouterFunction<T extends ServerResponse> { Mono<HandlerFunction<T>> route(ServerRequest request); }
- webflux버전의 웹 응답인 ServerResponse이나 그 서브타입의 Mono퍼블리셔를 리턴하는 HandlerFunction의 Mono타입
(2) HandlerFunction
- 함수형 스타일의 웹 핸들러(컨트롤러 메소드)
- 웹 요청을 받아 웹 응답을 돌려주는 함수
@FuctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}
(3) HandlerFunction과 RouterFunction 조합을 통해 코드를 좀더 간결하게 가능하다
RouterFunctions.route(predicate, handler)
RouterFunction router = RouterFunctions.route(RequestPredicates.path("/hello/{name}"), // test()처럼 실행시키지 않고 검사만 req -> ServerResponse.ok().body(fromObject("Hello " + req,pathVariable("name")))));
RouterFunction의 등록
- RouterFunction타입의 @Bean으로 만든다. 람다식 오브젝트를 하나 만들어서
- 핸들러 내부 로직이 복잡하다면, 분리한다
- 핸들러 코드만 람다식으로 따로 선언하거나
- 메소드를 정의하고 메소드 참조로 가져온다.
HandlerFunction handler = req -> {
String res = myService.hello(req.pathVariable("name"));
return ok().body(fromObject(res));
};
@Bean
RouterFunction helloPathVarRouter() {
//return route(RequestPredicates.path("/hello/{name}"),
//req -> ok().body(fromObject("Hello " + req.pathVariable("name"))));
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); // 빈의 핸들러 메서드 레퍼런스
}
- 좀더 나은 방향
- RouterFunction의 조합으로 개선
- 핸들러마다 Bean을 생성할 순 없으니까
- → RouterFunction의 and(), andRoute() 등으로 하나의 Bean에 n개의 RouterFunction을 체인으로 선언한다.
- 핸들러마다 Bean을 생성할 순 없으니까
- RouterFunction의 매핑 조건의 중복
- 타입레벨 - 메소드 레벨의 @RequestMapping처럼 공통의 조건을 정의하는 것 가능
- → RouterFunction.nest()을 사용
public RouterFunction<?> routingFunction() { return nest(pathPrefix("/person"), // /person로 시작하는 api들을 연결 nest(accept(APPLICATION_JSON), route(GET("/{id}"), handler::getPerson) .andRoute(method(HttpMethod.GET), handler::listPeople) ).andRoute(POST("/").and(contentType(APPLICATION_JSON)), handler::createPerson)); }
- 타입레벨 - 메소드 레벨의 @RequestMapping처럼 공통의 조건을 정의하는 것 가능
- RouterFunction의 조합으로 개선
@MVC WebFlux (@Controller + @RequestMapping) 개발방식
- 비동기 + 논블로킹 리액티브 스타일로 코드 작성 가능
- 매핑만 어노테이션 방식을 이용
@RestController
public static class MyController {
@RequestMapping("/hello/{name}")
Mono<ServerResponse> hello(ServerRequest req) {
return ok().body(fromObject(req.pathVariable("name");
}
}
- @MVC 요청 바인딩과 Mono/Flux 리턴 값
- 가장 대표적인 @MVC WebFlux 작성 방식
- 파라미터 바인딩은 @MVC 방식 그대로
- 핸들러 로직 코드의 결과를 Mono /Flux 타입으로 리턴
@GetMapping("/hello/{name}") Mono<String> hello(@PathVariable String name) { return Mono.just("Hello " + name); }
- @RequestBody 바인딩(json, xml)
- T
- Mono
- Flux
@RequestMapping("/hello/{name}") Mono<String> hello(@PathVariable Mono<User> user) { //변환된 오브젝트를 Mono에 담아서 전달 return user.map(u -> "Hello" + u.getName()); // Mono의 연산자를 사용해서 로직을 수행하고 Mono로 리턴 } // 또는 스트림으로 표현할 수 있다 @PostMapping("/hello") Flux<String> hello(@PathVariable Flux<User> user) { //변환된 오브젝트를 Mono에 담아서 전달 return user.map(u -> "Hello" + u.getName()); // User의 스트림 형태로 로직 수행 }
- @ResponseBody 리턴 값 타입
- T
- Mono
- Flux
- Flux
: Httpstream으로 리턴하고 싶을 때 - void
- Mono
WebFlux와 리액티브 기술(WebClient + Reactive Data)
Q. WebFlux만으로 성능이 좋아질까?
- 비동기-논블로킹 구조의 장점은 블록킹 IO를 제거하는데서 나온다
- HTTP서버에서 논블로킹 IO는 오래 전부터 지원
- 이것만으로는 성능이 나아지지 않는다. 다른 것도 개선해야하는데
개선할 블로킹 IO
- 데이터 액세스 리포지토리
- JPA - JDBC기반 RDB연결
- 현재는 답이 없다
- 일부 DB는 논블로킹 드라이버 존재
- @Async 비동기 호출과 CFuture를 리액티브로 연결하고 쓰레드풀 관리를 통해서 웹 연결 자원을 효율적으로 사용하도록 만드는 정도
- 물론 db접속은 블록킹이지만 아래처럼하면 db로 가져오는 것 외에는 논블록킹으로 구현할 수 있다.
@Async CompletableFuture<User> findOneByFirstname(String firstname); // 이렇게 만들고 이걸 여기서 Mono타입으로 호출 @GetMapping Mono<User> findUser(String name) { // fromCompletionStage로 리턴시키면 비동기적 return Mono.fromCompletionStage(myRepository.findOneByFirstName(name)); }
- Async JDBC가 등장한다면 해결
- JPA - JDBC기반 RDB연결
- HTTP API 호출
- 기타 네트워크를 이용하는 서비스
- 본격 리액티브 데이터 액세스 기술
- 스프링 데이터의 리액티브 리포지토리 이용
- MongoDB
- Cassandra
- Redis
ReactiveCrudRepository 확장
public interface ReactivePersonReposityro extends ReactiveCrudRepository<Person, String> { Flux<Person> findByLastname(Mono<String> lastname); @QUery("{'firstname':?0, 'lastname;:?1}") Mono<Person> findByFirstnameAnd Lastname(String firstname, String lastname); // 0 또는 1개의 결과를 비동기-논블로킹 리액티브로 조회하도록 결과를 Mono<T>로 리턴 }
- 스프링 데이터의 리액티브 리포지토리 이용
- 논블로킹 API호출은 WebClient
- AsyncRestTemplate의 리액티브 버전
- 요청을 Mono/Flux로 전달할 수 있고
- 응답을 Mono/Flux형태로 가져온다.
@GetMapping("/webclient") Mono<String> webclient() { return WebClient.create("http://localhost:8080") .get() .uri("/hello/{name}", "Spring") .accept(MediaType.TEXT_PLAIN) .exchange() .flatMap(r -> r.bodyToMono(String.class)) // 요청 바디를 String타입으로 변환해서 Mono에 //담아 리턴하는 함수로 Mono데이터에 적용한 함수의 결과가 Mono타입이기 때문에 flatMap을 적용해야한다. 아니면 // Mono<Mono<String>>이 됨 .map(d -> d.toUpeerCase()) .flatMap(d -> helloRepository.save(d)); }
공부해야할 것
- 함수형 프로그래밍
- CompletableFuture와 같이 비동기 작업의 조합, 결합에 뛰어난 툴의 사용법 익힐 것
- ReactCore 학습
- Mono/Flux, 오퍼레이터, 스케쥴러
- WebFlux와 스프링의 리액티브 스택 공부
- 비동기 논블로킹 성능과 관련된 벤치마킹, 모니터링, 디버깅 연구
- 테스트