Spring WebFlux
if(Kakao) dev 2018 스프링5 웹플럭스와 테스트 전략 - 이일민(Toby)
스프링 5.0에 새로 등장한 웹 프레임워크 + 리액티브 스택
- spingMVC와 두 개의 웹 기술 트랙
- 초기 이름은 스프링 웹 리액티브, 현재는 웹 플럭스로 바뀜
스프링 웹 플럭스 VS MVC
SpringMVC : 서블릿 스택 기반 웹 프레임워크
Spring WebFlux : 리액티브 스택
스프링 웹 플럭스와 MVC가 공유하는 부분과 고유한 부분 (이 부분 때문에 혼란스러울 것이다)
예제
SpringMVC - Hello API
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "Hello " + name;
}
스프링 웹 플럭스
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "Hello " + name;
}
웹 플럭스의 @Controller 코드는 스프링 MVC의 코드와 동일한 방식으로 작성 가능하다
- 그렇지 않은 것도 있다.
스프링 웹 플럭스 도입이유
- 100% (I/O, 데이터 처리) 논블록킹 개발
- 확장성과 고효율성이 매우 중요
- 업, 다운 스트리밍과 Back pressure가 필요
- 고속 서비스 오케스트레이션 개발
- 유사한 프로그래밍 모델의 경험
- 유연하게 커스토마이징이 가능한 웹 프레임워크 구성
- 본격적인 함수형 프로그래밍 모델 사용
=> 확장성과 고효율성이 매우 중요한 경우에 이 프레임워크를 활용하라, (기업입장)확장성을 높여서 돈을 절약하라
스프링 웹 플럭스를 사용하지 않는게 좋은 이유
- 웹플럭스가 왜 필요한지 분명하게 모름
- 비동기/논블록킹 방식 도입한다 (써야할 이유 x)
- 리액티브 라이브러리(Reactor, RxJava) 사용한다 (써야할 이유 x)
- 블록킹이 서버, 코드, 라이브러리에 존재
- 블록킹 IO, 블록킹 서블릿 필터
- JPA (x), JDBC (x)
- ADBA (o), AoJ (?), R2DBC (?)
- SpringMVC로 개발했더니 아무 문제 없음 (써야할 이유 x)
스프링 웹 플럭스는 스프링 MVC로 시작해도된다
스프링5 MVC는 웹 플럭스에서 제공되는 다양한 기능과 프로그래밍 모델 제공
- 비동기/논블록킹 API 호출
- 비동기/논블록킹 데이터 액세스
- 리액티브 데이터 조회, 전송
- 비동기 웹 요청 처리
- 서버 스트리밍
- 클라이언트 스트리밍
- Reactor(Flux, Mono), RxJava, Flow 등을 이용하는 코드
MVC에서 WebClient
사용이 가장 좋은 출발점
- RestTemplate 대체
스프링 웹 플럭스를 꼭 쓸 이유도, 그렇지 않을 이유도 없는 중간이라면 한번 써보는게 좋다. WebClient
하나만 알아가도 좋다
리엑티브 프로그래밍, 비동기/논블록킹
리엑티브 (함수형) 프로그래밍
- 인터넷 시대에 복잡함을 해결하기 위해 등장
- 연속적으로 일어나는 이벤트를 다루는 프로그래밍 기법
특징
- UI 이벤트, 비동기적인 I/O 이벤트, 통제 불가능한 이벤트 스트림 처리
- 동시성, 비동기/논블록킹 호출을 다루는데 탁월
- 조합 가능한 비동기 로직을 다루는 함수형 프로그래밍
비동기/논블록킹 API 호출 - 가장 단순한 리엑티브
동기/블록킹 API 호출 - RestTemplate
- 장점 : 쉽고 간단함
- 단점 : IO 동안 블록킹, 시스템 특성에 따라 매우 비효율적이 될 수도(돈으로 해결)
비동기/논블록킹 API 호출
- 적은 서버 리소스만 사용해서 많은 요처을 처리하자는 것이 핵심
- AsyncRestTemplate(Spring4)
- Async/Await(Java8++)
- WebClient(Spring5)
- 장점 : 확장성이 뛰어나고 높은 처리율과 낮은 레이턴시를 가지는 효율적인 서버 구성이 가능하다.(돈 절약), 쉽고 직관적이고 간단하다
- 단점 : 장점을 얻을 만한 경우가 많지 않음, 자칫하면 코드가 복잡하고 이해하기 어려움
비동기/논블록킹 API 진화
- Future -> DeferedResult -> CompletableFuture
- 자바에서도 Async/Await 사용가능
- Reactor Flux/Mono를 이용할 수 있다.
Reactor Flux/Mono는 CompletableFuture와 유사해보이지만, 큰 장점이 있다.
- Mono : 어떤 이벤트가 비동기적으로 넘어올 때 딱 1개까지만 허용한다.
- Flux : 데이터를 Stream으로 지속적으로 받을 수 있다. 캐시에 담은 다음에 Mono에 한꺼번에 넣어서 넘겨도 된다.
CompletableFuture VS Reactor Flux/Mono
공통점
- 람다식으로 사용하는 함수형 스타일
- 비동기와 비동기 작업의 조화(compose, flatMap)
- 비동기와 동기 작업의 적용(apply, map)
- Exceptional 프로그래밍
- 작업별 쓰레드 풀 지정 가능
Flux/Mono 방식의 장점
- 데이터 스트림(Flux) <-> List/Collection
- 강력한 연산자 제공
- 지연 실행, 병합, 분산, 실행 제어
- 유연한 스케줄러
- ReactiveStreams, 자바 9+ 표준
- 다양한 지원 라이브러리, 서비스, 서버
리액티브 애플리케이션의 데이터 흐름
리액티브 API 데이터 시퀀스 검증
Mono나 Flux를 가지고 있으면 이는 Reactive API라고 부를 수 있다.
단위 테스트를 할 수도 있지만, 그 뒷부분까지 통합 테스트를 할 수도 있다.
주의! : 테스트 코드는 항상 동기
방식이어야 한다. 하지만 우리가 테스트할 대상은 비동기/논블로킹 특성을 가지고 있다.
subscribe() + assert
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(1)); // 테스트 성공
}
@Test
void mono2() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(2)); // 테스트 성공??????
}
Mono나 Flux는 데이터를 리턴 값으로 받는 것이 아니라, 일종의 콜백을 받는 것이다.
Exception이 발생하지 않기때문에 2를 넣어도 테스트가 성공한다.
Scheduler는 별도의 데몬 스레드에서 작동하는 것이기 때문에 subscribe() + assert 사용할때 조심해야 한다.
CountDownLatch
@Test
void mono3() throws InterruptedException {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
CountDownLatch latch = new CountDownLatch(1);
mono.subscribe(item -> {
assertThat(item).isEqualTo(2);
latch.countDOwn();
});
latch.await(); // 테스트가 끝나지 않음!
}
-> CountDownLatch
를 이용하여 테스트에서 동시성을 제어해야한다.
// 테스트에서 동시성을 제어해야 하는 번거로움!
@Test
void mono4() throws InterruptedException {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger item = new AtomicInteger();
mono.subscribe(
i -> item.set(i),
e -> latch.countDown(),
latch::countDown
);
latch.await();
assertThat(item.get()).isEqualTo(1);
}
-> 테스트 코드를 계속 이렇게 작성해야하면 고통스러울 것이다. block() 사용하자
block() + assert
@Test
void mono5() throws InterruptedException {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
Integer item = mono.block();
assertThat(item).isEqualTo(1);
}
StepVerifer
비동기/논블록킹 테스트에는 StepVerifier을 이용하자
- 비동기/논블로킹으로 동작하는 코드 테스트 툴
- 데이터 스트림의 검증
- 예외, 완료도 검증
- 가상시간을 이용해 오랜 시간의 이벤트 테스트
@Test
void stepVerifier() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(1) // 첫번째 데이터 아이템 값
.verifyComplete();
}
@Test
void stepVerifier2() {
var flux = Flux.just(1, 2, 3)
.concatWith(Mono.error(new RuntimeException()))
.subscribeOn(Schedulers.single())
StepVerifier.create(flux)
.expectNext(1)
.expectNext(2)
.expectNext(3)
.expectError(RuntimeException.class) // 에러나고 종료
.verify();
}
리액티브 HTTP API 호출 테스트
RestTemplate
- 동기/블록킹
AsyncRestTemplate
- 비동기/논블록킹
- Future 콜백, CompletionStage
WebClient
- 비동기/논블록킹
- Flux/Mono 요청, 응답
- Streaming 지원
- BackPressure 지원
WebClient
를 사용하면 RestTemplate를 대신해서 비동기/논블록킹을 함수형 프로그래밍 스타일로 쉽게 작성할 수 있다
WebClient
@GetMapping("/api")
public Mono<String> helloAPi() {
return client.get()
.uri("/api/hello")
.retrieve()
.onStatus(HttpStatus::is4xxClientError,
res -> Mono.error(new IllegalArgumentException()))
.onStatus(HttpStatus::is5xxServerError,
res -> Mono.error(new RuntimeException()))
.bodyToMono(String.class)
.map(body -> body.toUpperCase()) // 비지니스 로직 수행
.switchIfEmpty(Mono.just("Empty"));
}
원격 리액티브 API 호출 - 통합테스트(feat. MockServer)
원격 테스트를 하려면 서버를 따로 띄워야하는데, MockServer
를 사용하면 HTTP API 통합테스트를 할 수 있다.
MockServer
를 포함하면, 간단하게 HTTP Request를 테스트 할 수 있는 서버를 생성할 수 있다.
WebClientIntegrationTests
는 유용한 샘플이니 참고하면 좋다.
private MockWebServer server;
private WebClient webClient;
@Before
public void setup() {
var connector = new ReactorClientHttpConnector();
this.server = new MockWebServer();
this.webClient = WebClient
.builder()
.clientConnector(connector)
.baseUrl(this.server.url("/").toString())
.build();
}
@Test
public void shouldReceiveResponseHeaders() {
prepareResponse(response -> response
.setHeader("Content-Type", "text/plain")
.setBody("Hello Spring!"));
Mono<HttpHeaders> result = this.webClient.get()
.uri("/greeting?name=Spring").exchange()
.map(response -> response.headers().asHttpHeaders());
StepVerifier.create(result).consumeNextWIth(
httpHeaders -> {
assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType());
assertEquals(13L, httpHeaders.getContentLength()); })
.expectComplete()
.verify(Duration.olfSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT));
assertEquals("/greeting?name=Spring", request.getPath());
});
}
interface HelloService {
Mono<String> hello();
}
@Component
public class RemoteHelloService implements HelloService {
public Mono<String> Hello() {
return client.get()
.uri("/hello")
.retrieve()
.onStatus(HttpStatus::is4xxClientError,
res -> Mono.error(new IllegalArgumentException()))
.onStatus(HttpStatus::is5xxServerError,
res -> Mono.error(new RuntimeException()))
.bodyToMono(String.class);
}
}
@Autowired HelloService helloService; // Mock 서비스로 대체 가능
@GetMapping("/api")
public Mono<String> helloApi() {
return this.helloService.hello()
.map(body -> body.toUpperCase())
.switchIfEmpty(Mono.just("Empty"))
.doOnError(c -> c.printStackTrace());
}
웹 플럭스의 새로운 아키텍처
기존 MVC는 서블릿 스팩과 서버의 제약 위에 개발
- 프론트 컨트롤러 패턴, MVC 패턴, 전략 패턴
웹 플럭스는 독자적인 아키텍처를 가지는 프레임워크
- 서블릿(3.1+) 컨테이너를 사용할 수 있으나 의존적이지 않음
- Netty, Undertow 서버 지원
- 논블록킹 네트워크/논블록킹 API
- 논블록킹 데이터 스트림
- 서버/기술 의존적이지 않은 프레임워크 재구성 손쉬움
- 함수형 엔드포인트
- 뛰어난 테스트 편의성
EnableWebFlux
어노테이션을 추가하면
![이미지 : 웹플럭스의 새로운 아케텍처 - @EnableWebFlux]
TestClient - @WebFluxText
Spring boot 2 애플리케이션의 Mock Test
bindToApplicationContext
이용
@RunWith(SpringRunner.class)
@WebFluxTest
public class WebCLientBootTest {
@Autowired WebTestClient webTestClient;
@Test
public void hello() {
webTestClient.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
}
}
bindToRouterFunction
: 함수형 엔드포인트에 대한 테스트
Mono<ServerResponse> handler(ServerRequest request) {
return ServerResponse.ok().body(Mono.just("hello"), Spring.class);
}
@Test
void routerFunction() {
ReouterFunction<ServerResponse> route =route(GET("/rf"), this::handler);
WebTestClient client = WebTestClient.bindToRouterFunction(route).build();
client.get().uri("/rf")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.isEqualTo("hello");
}
bindToController
: 특정 컨트롤러/핸들러만으로 테스트 대상 구성. 예를 들어 컨트롤러가 100개 있는 서버를 테스트하고 싶다면, 기존의 테스트 코드를 재사용 할 수 있다.
WebTestClient client = WebTestClient.bindToController(
new MyController(), new HelloAPi()
).build();
client.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
bindToServer
: static 메서드를 사용해서 실제 동작하는 서버에 연결해서 테스트할 수 있다.
함수형 엔드포인트 테스트 방법
bindToRouterFunction
\@WebFLuxTest
- 함수 단위 테스트
- 본격적인 함수형 스타일 웹 프로그래밍
- 조합 가능한 마이크로 프레임워크
- 작고 가벼운 비동기 논블록킹 웹
Reference
'Spring' 카테고리의 다른 글
Filter, Interceptor, AOP (1) | 2023.11.23 |
---|---|
[에러] org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfig' defined in file (0) | 2022.03.27 |
예외처리 - @ExceptionHander, @ControllerAdvice, @RestConrollerAdvice (0) | 2022.01.11 |
Spring 이란 (0) | 2021.05.18 |