Spring WebFlux에 대해 알아보고, CRUD API를 구현해보자 ❕
먼저 Spring WebFlux는 Spring Framework 5.0에 도입된 비동기 및 반응형 웹 프레임워크이다. 기존 Spring MVC는 Servlet 스택 기반의 동기적 처리 모델을 사용했지만, 높은 트래픽과 서버 자원을 효율적으로 처리해야 하는 요구가 증가함에 따라, 비동기 논블로킹 및 반응형 프로그래밍 모델을 지원하는 Spring WebFlux가 등장하였다.
1. Spring Webflux
Spring 프레임워크에서 제공하는 비동기 논블로킹(non-blocking) 프로그래밍 모델을 기반으로한 모듈이며, 기존의 Spring MVC가 블로킹 I/O 기반이었던 것과 달리 WebFlux는 논블로킹 I/O를 사용하기 때문에 높은 처리량과 확장성을 지원한다.
주요 특징
Reactive Programming (반응형 프로그래밍)
리액티브는 특정 이벤트에 반응하는 비동기 프로그래밍 패러다임을 말한다. 주로 비동기 데이터 처리와 이벤트 기반 시스템에서 사용되는데, 데이터가 변경되었을 때 반응하여 연산을 실행하도록 설계된 것을 말한다.
WebFlux는 리액티브 프로그래밍을 구현하기 위해 Reactor이라는 라이브러리를 이용하고, Reactor는 Netty 서버를 통해 이벤트 기반의 반응형 스트림으로 데이터를 주고받는다.
Reactor 라이브러리
반응형 프로그래밍을 구현하기 위한 Reactive Streams의 구현체 중 하나이다. Reactive Streams는 일종의 스펙이고, 여러 구현체가 존재하는데 그 중 Spring WebFlux가 사용하는 것이 Reactor인 것이다. Publisher-Subscriber 패턴으로 동작하며 데이터를 생성 및 가공 후 구독자에게 전달하는 역할을 한다.
함수형 엔드포인트
WebFlux는 기존의 컨트롤러 대신 함수형 엔드포인트를 도입했다. 이를 통해 람다 표현식을 사용하여 엔드포인트를 정의할 수 있다.
백프레셔너블
데이터 흐름의 속도를 제어하기 위해 백프레셔를 지원한다. Publisher에 의해 생산되는 데이터를 Subscriber가 충분히 처리하지 못할 때, 처리속도 불균형 문제를 해결할 수 있는 장치이다.
백프레셔는 Subscriber가 처리할 데이터 양을 Subscription을 통해 제어하며, Publisher는 Subscriber의 처리 속도에 맞춰 데이터를 생성한다. 이를 통해 Subscriber가 처리할 수 있는 양 이상의 데이터가 생성되는 상황을 방지한다.
논블로킹 I/O
WebFlux는 논블로킹 I/O를 사용하여 요청을 비동기적으로 처리하므로 스레드 풀을 효율적으로 활용하고 높은 동시성을 지원한다.
Netty
비동기 논블로킹 환경의 서버로, 내부적으로 논블로킹과 더불어 *I/O Multiplexing 기반 *Event Driven 방식으로 높은 성능을 보장한다.
Netty는 두 개의 큰 이벤트 루프 그룹을 사용한다. 각 이벤트 루프 그룹은 싱글 스레드기반의 이벤트 루프를 1개 ~ N개 여러개 운영이 가능한 구조이다. 즉, Boss Event 루프 그룹은 1개, Worker Event 루프 그룹은 다수의 이벤트 루프를 운영한다.
* I/O Multiplexing ?
Non-Block방식의 불필요한 리소스 낭비를 없앨 수 있는 방법이 I/O Multiplexing이다.
- 이벤트를 등록하고 대기하면, 연결이나 데이터 수신 상황이 발생했을 때 커널로부터 이벤트를 전달받는다.
- 다수의 소켓에 대해 이벤트 등록이 가능하기 때문에, 하나의 스레드로 다수의 I/O를 다룰 수 있다.
* Event Driven 방식?
- 클라이언트 요청은 이벤트 루프가 관리하는 큐에 추가된다.
- 이벤트 루프는 준비된 작업을 순차적으로 처리하며, 데이터가 준비되지 않은 경우 대기하지 않고 다른 작업을 진행한다.
- 작업 완료 시 콜백(Callback)이 실행되어 요청에 대한 결과를 반환하거나 다음 작업을 이어간다.
2. Spring MVC과 비교
공통점
- 어노테이션 기반 : @Controller, @RestController, @RequestBody와 같은 어노테이션을 사용할 수 있다.
- Reactive 클라이언트 : 즉, Spring MVC에서도 Reactor를 사용할 수 있다
- 실행 가능한 서버 : Tomcat, Jetty, Undertow
차이점
특징 | Spring MVC | Spring WebFlux |
처리 방식 | 블로킹 | 논블로킹 |
I/O 모델 | 스레드 기반 | 이벤트 루프 기반 |
스레드 처리 | 요청당 하나의 스레드 사용 | 요청당 이벤트 루프에서 처리 |
확장성 | 스레드 풀 제한으로 확장성 제약 있음 | 높은 동시성 처리 가능 |
사용 서버 | Tomcat, Jetty | Netty, Undertow, Tomcat |
실제로 Spring Framework 5 이상(Spring Boot 2 이상)에서 애플리케이션 구성 시, Reactive Stack과 Servlet Stack 중 선택할 수 있다.
Spring MVC에서도 Reactor를 사용할 수 있지만, Servlet Stack 자체가 Blocking I/O 방식이기 때문에 완벽한 비동기 논블로킹이 지원되지 않을 수 있다.
스레드 할당 방식의 차이
- 기존 사용했던 Servlet Stack은 블로킹 방식으로, 클라이언트 요청마다 스레드가 사용되고 많은 요청이 온 경우 스레드 풀 개수만큼 요청을 처리할 수 있다. 따라서 가용할 수 있는 스레드가 소진되면 대기열이 가득차게 되고 대기 시간이 길어지게 된다.
- 하지만 Reactive Stack은 논블로킹 방식으로, 클라이언트 요청이 오면 이벤트 핸들러와 콜백을 포함하여 스레드 풀에 요청을 위임한다. 이런 방식을 택한다면 제한된 스레드 개수에서 효율적으로 요청과 응답을 받을 수 있다.
Q. 그럼 WebFlux가 Spring MVC보다 무조건 빠를까?
그건 아니다. 아래 공식문서를 확인해보면 알 수 있듯이, 빠른 처리보다는 적은 수의 스레드로 메모리를 효율적으로 사용해 많은 트래픽을 처리할 수 있는 것이 WebFlux의 핵심이다!
3. Reactive Streams & Reactor
3-1. Reactive Streams
Reactive Streams는 비동기 데이터 스트림 처리르 위한 표준이다. 이는 대규모 데이터 처리나 비동기 통신에서 발생할 수 있는 데이터 불균형 문제를 해결하기 위해 만들어진 사양이다.
구성 요소
- Publisher
- 데이터 생산자
- Subscriber가 구독하면 데이터 스트림을 생성한다.
- Subscriber
- 데이터 소비자
- Publisher로부터 데이터를 받아 처리한다.
- Subscription
- 구독 정보를 관리
- Subscriber와 Publisher 간의 데이터 요청과 데이터 흐름을 관리한다.
- Processor
- Publisher와 Subscriber의 역할을 동시에 수행
- 데이터를 변환하거나 중개하는 역할
3-2. Reactor
Spring WebFlux는 Reactive Streams의 구현체로 Reactor 라이브러리를 사용하며, 위 4개의 구현체를 모두 대응하는 역할을 제공한다. 이번 시간에는 실습에서 필요한 Publisher와 이와 관련한 연산자에 대해서 알아보려고 한다.
Reactor Publisher
Mono와 Flux라는 클래스를 기반으로 비동기 스트림 처리를 제공한다.
Mono
- 0 또는 1개의 이벤트를 발생시키는 데이터 스트림
Mono<String> mono = Mono.just("hello!");
mono.subscribe(System.out::println);
Flux
- 0 또는 N개의 이벤트를 발생시키는 데이터 스트림
Flux<String> flux = Flux.just("hello", "world!");
flux.subscribe(System.out::println);
Reactor Operators
- map : 각 데이터를 변환
- flatMap : 각 데이터를 1~N개로 변환
- filter : 조건에 따라 필터링
- zip : 두 스트림을 결합
- swtichIfEmpty : 원래 스트림이 완전히 비어 있을 때, 다른 Publisher(Mono, Flux)로 대체
- defaultIfEmpty : 스트림이 실제 값으로 비어 있을 때, default 고정값 설정
- limitRate : 한 번 동작할 때, 특정 개수만큼 실행
…
4. 실습
5-1. Spring Initializing 생성 및 의존성 추가
spring-boot-starter-webflux
spring-boot-starter-data-r2dbc
r2dbc-mysql
(io.asyncer)lombok
데이터베이스에 맞는 R2DBC driver와 그 구현체의 의존성을 추가해줘야 한다.
GitHub - asyncer-io/r2dbc-mysql: Reactive Relational Database Connectivity for MySQL. The official successor to mirromutth/r2dbc
Reactive Relational Database Connectivity for MySQL. The official successor to mirromutth/r2dbc-mysql(dev.miku:r2dbc-mysql). - asyncer-io/r2dbc-mysql
github.com
나는 spring-boot-starter-data-r2dbc 버전 3.*를 사용했기 때문에, r2dbc-mysql:1.2.0을 사용했다.
yml 설정
spring:
r2dbc:
url: r2dbc:pool:mysql://localhost:3306/webflux
username: name
password: pwd
logging:
level:
org.springframework.r2dbc.core: debug
5-2. Entity, Repository 설정
DB 테이블을 생성해주고, Entity와 Repository 클래스들을 생성한다.
(주의)
1. *^@Id 어노테이션 설정 필드는 엔티티를 생성할 때, 값을 할당하느냐 안하느냐로 SQL 쿼리가 달라진다.^
- @Id 필드가 null인 상태로 save()를 실행하면, Insert가 실행되고
- @Id 필드가 값이 설정된 상태로 save()를 실행하면, Update가 실행된다.
2. ^spring data r2dbc는 연관관계 매핑을 지원하지 않는다.^
- 캐싱, 지연로딩, 쓰기 지연등 ORM의 주요 기능을 제공하지 않는다는 의미이다. 따라서 join이 필요한 상황이라면 직접 조인 쿼리를 작성해야 한다.
5-3. API 구현
카테고리 생성 API 실행
server가 Netty로 동작되며, 데이터베이스에도 정상적으로 insert된다.
수정 및 삭제 API
앞서 flatMap은 1개의 데이터를 1개 이상의 데이터로 변환할 때 사용한다고 했는데, I/O에서 비동기로 동작하기 때문에 외부 DB요청이나 API서버 요청에 사용하기 좋다.
5-4. 동적 쿼리 작성
기존 Spring Data JPA에서 사용하는 것처럼, Repository에서 @Query 어노테이션을 활용한다. @Query 어노테이션을 사용하면 네이티브 쿼리를 사용할 수 있다.
5. 마무리
간단하게 Spring WebFlux에 대해 알아보고, 실습을 해보았다. 최근 기업들의 기술 스택을 살펴보니 WebFlux를 사용하는 곳이 많아서 궁금했는데, 논블로킹 비동기 방식으로 빠른 속도를 제공하기 때문에 사용한다고 생각했다. 학습해보니, 빠른 속도가 핵심이라기보다는 적은 수의 스레드로 메모리를 효율적으로 사용해 많은 트래픽을 처리할 수 있다는 점이 더 중요한 이유였다.
특히, 사용자 요청 수가 많아지는 환경에서 스레드 풀의 크기 제한으로 인해 발생할 수 있는 병목 현상을 완화할 수 있다는 점이 시스템 확장성에 좋아 보였다. 이번 학습을 통해 단순히 기술을 사용하는 것을 넘어, 기술이 해결하려는 문제를 이해하는 것이 중요하다는 것을 느꼈다. 앞으로 프로젝트에서도 비슷한 제약이 발생하면 WebFlux와 같은 기술을 도입해 효율성을 높여보고 싶다.
참고자료 😃
https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html
https://docs.spring.io/spring-boot/reference/web/reactive.html
https://jh-labs.tistory.com/776
https://adjh54.tistory.com/232
https://ckddn9496.tistory.com/158
'Spring' 카테고리의 다른 글
[Spring] ResponseBodyAdvice를 활용한 공통 응답 처리 (0) | 2024.12.16 |
---|---|
[Spring] Apache Poi 엑셀 다운로드 개선 및 성능 확인 (+ SXSSF) (1) | 2024.12.06 |
[Spring] Log4J, Logback, Log4J2 개념 (+Logback실습) (1) | 2024.11.21 |
[Spring] SpringBoot에서 Redis 적용하기 (+부하 테스트) (8) | 2024.11.08 |
[Spring] Spring에서 동시성 이슈를 해결하는 방법 (1) | 2024.10.02 |