Spring Webflux 간단 개념 정리 및 실습
Spring WebFlux는 Spring 5에서 새롭게 추가된 모듈입니다.
WebFlux는 클라이언트, 서버에서 reactive 스타일의 어플리케이션 개발을 도와주는 모듈이며,
reactive-stack web framework이며 non-blocking에 reactive stream을 지원합니다.
장점 : 고성능, spring 과 완벽한 통합, netty 지원, 비동기 non-blocking 메세지 처리, Back Pressure
netty : 프로토콜 서버 및 클라이언트와 같은 네트워크 응용 프로그램을 빠르고 쉽게 개발할 수있는 NIO(Non-Blocking Input Ouput) 클라이언트 서버 프레임 워크
기존의 소켓 프로그래밍은 클라이언트가 접속하게 되면 스레드를 할당해야 하는데(1:1관계),정말 많은 클라이언트가 접속을 하게 될 경우 그 숫자만큼 스레드를 생성해야 해서 리소스의 낭비로 이루어지고,문맥 교환과 관련된 문제와 입력이나 출력 데이터에 관련한 무한 대기 현상이 발생하는 문제를 해결하기 위해서 등장
(상당히 방대한 내용이 있지만 주제에 벗어나는 듯하여 이는 다른 포스팅에서 정리하려고합니다.)
Back Pressure : 스프링 배치에서 다루었던 개념으로 빠른 Publisher - 느린 Subscriber 문제를 해결하는 원리이다. Publisher의 일방적 데이터 Push 가 아니라, Subscriber가 처리할 수 있을 만큼의 데이터만 Subscriber의 요청에 의해서 전달해주는 것이다.
스프링 부트 2를 보면 Reactive Stack과 Servlet Stack이 있는데 주요하게 봐야하는 부분으로
1. Spring WebFlux vs Spring MVC
스프링 MVC는 아마 스프링을 다뤄봤다 하면 경험이 있을탠데
1 request : 1 thread
sync + blocking
이글에서 다루고자하는 Spring WebFlux는
many request : 1 thread
async + nonblock
2. DB
WebFlux로 개발하고 DB는 blocking 이라면 ? WebFlux를 쓸 이유가 없다. 라는 개념아래서 reactive를 지원하는 DB를 사용해야하는데 우리가 일반적으로 알고있는 RDBMS는 지원하지않고 Redis, Mongo 등은 지원한다. 실습에서는 Redis를 이용하는 부분을 다뤄 볼려고합니다.
(R2DBC를 이용하면 MYSQL 등과 비동기 방식으로 연결이 가능한 듯합니다. )
중간 정리
1. 그래서 WebFlux 쓰는 이유
배민 기술 블로그를 보면 가게노출 시스템에 WebFlux를 도입하였는데 내용을 보면
가게노출 시스템은 배달의민족 앱 내에서 가장 많은 트래픽을 소화하는 시스템으로, 다음과 같은 리소스 최적화가 반드시 필요
- 하나의 사용자 요청을 처리하기 위해 수십여개의 외부 시스템에 대한 요청이 필요한 시스템에서, 어떻게 가장 빠른 응답을 줄 수 있을까?
- 수많은 요청을 처리해야할 때 어떻게 쓰레드 지옥을 벗어날 수 있을까?
앞서서도 설명하였듯 Spring MVC는 1:1로 요청을 처리하기 때문에 트래픽이 몰리면 많은 쓰레드가 생겨납니다. 쓰레드가 전환될때 context switching 비용이 발생하게 되는데 쓰레드가 많을 수록이 비용이 커지게 되기 때문에 적절한 쓰레드의 수를 유지해야하는 문제가 있습니다.
이에 반해서 WebFlux 는 Event-Driven 과 Asynchronous Non-blocking I/O 을 통해 리소스를 효율적으로 사용할 수 있도록 만들어 줍니다.
https://woowabros.github.io/experience/2020/02/19/introduce-shop-display.html
2. 이렇게 좋은데 왜 MVC를 쓰는 경우가 있나요?
Webflux 가 무조건 빠르고 좋은 것이 아니라 Webflux 프로젝트의 비지니스 로직들이 모두 Async + NonBlocking 으로 되어있다면 빠를 수 있습니다. 그런데 이런 코드를 작성하는 것은 꾀... 어렵죠. ㅎ 아래 주소는 NHN FORWARD 2020에서 WebFlux의 성능이 나오지 않는 부분에 대한 발표내용입니다.
https://www.youtube.com/watch?v=I0zMm6wIbRI
MVC는 못해도 본전이지만, Reative 는 못하면 MVC를 적용하느니만 못하다. 그리고 트래픽이 높은 서비스가 아니라면 MVC로도 감당 가능하다.
Mono와 Flux
Spring Webflux에서 사용하는 reactive library가 Reactor이고 Reactor가 Reactive Streams의 구현체입니다.
Flux와 Mono는 Reactor 객체이며, 차이점은 발행하는 데이터 갯수입니다.
- Flux : 0 ~ N 개의 데이터 전달
- Mono : 0 ~ 1 개의 데이터 전달
보통 여러 스트림을 하나의 결과를 모아줄 때 Mono를, 각각의 Mono를 합쳐서 하나의 여러 개의 값을 여러개의 값을 처리할 떄 Flux를 사용합니다.
그런데 Flux도 0~1개의 데이터 전달이 가능한데, 굳이 한개까지만 데이터를 처리할 수 있는 Mono라는 타입이 필요할까요? 데이터 설계를 할때 결과가 없거나 하나의 결과값만 받는 것이 명백한 경우, List나 배열을 사용하지 않는 것처럼, Multi Result가 아닌 하나의 결과셋만 받게 될 경우에는 불필요하게 Flux를 사용하지 않고 Mono를 사용하게 됩니다.
실습
1. 의존성
plugins {
id 'org.springframework.boot' version '2.5.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.webflux.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
test {
useJUnitPlatform()
}
2. handler 작성
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.BodyInserters.*;
import static org.springframework.web.reactive.function.server.ServerResponse.*;
@Component
public class HelloHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ok().contentType(MediaType.TEXT_PLAIN)
.body(fromValue("Hello " + request.pathVariable("name")));
}
/*
// 세미나 코드 (위와 동일한 내용)
HandlerFunction helloHandler = req ->
ok().body(fromValue("Hello" + req.pathVariable("name"))); // 세미나 코드에서 사용한 fromObject 가 @Deprecated -> fromValue
*/
}
3. router 등록
import com.webflux.study.handler.HelloHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.*;
@Configuration
@RequiredArgsConstructor
public class HelloRouter {
private final HelloHandler helloHandler;
@Bean
public RouterFunction<ServerResponse> helloRoute() {
return RouterFunctions
.route(RequestPredicates.GET("/hello/{name}")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), helloHandler::hello);
}
}
4. 테스트 코드 작성
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloWebClientTest {
@Autowired
private WebTestClient webTestClient;
@Test
public void testHello() {
webTestClient
// Create a GET request to test an endpoint
.get().uri("/hello/jong")
.accept(MediaType.TEXT_PLAIN)
.exchange()
// and use the dedicated DSL to test assertions against the response
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello jong");
}
}