Spring Webflux는 과연 현재 프로젝트에 어울릴까

김형준·2025년 2월 17일
0

개요

spring에서 반응형 프로그래밍 지원을 위한 비동기 논블로킹 웹 프레임워크

reactive 스트림 사양을 기반으로 동작한다.

이벤트들을 비동기적으로 처리하며 이를 Publisher-Subscriber 패턴(Reactive Stream)으로 구현하며

한 스레드 내에서 요청에 대한 처리를 동기적으로 대기할 필요 없이 비동기적으로 다음 요청을 처리할 수 있다.

또한 확장성이 좋아 Spring MVC 및 다양한 서버(Netty, Undertow 등)과 통합 가능하다.

  • DB 작업들도 비동기적으로 처리 가능한가? R2DBC 드라이버를 사용하도록 설정하면 가능하며, MySQL의 경우 R2DBC를 사용하려면 MariaDB를 활용해야 한다.

주요 특징들

Reactor

reactive 프로그래밍 구현을 위한 데이터 스트림 기반의 pub-sub 구조를 갖는 Reactive 라이브러리

Event

Subscriber가 Publisher에 데이터를 요청, Publisher가 데이터를 생성 및 전송하는 과정

request(n) 메서드로 데이터를 요청할 수 있으며 onNext(data) 메서드로 Subscriber에 데이터를 전송한다.

BackPressure

subscriber가 처리 가능한 데이터의 양을 subscription을 통해 제어하는 것

이를 통해 publisher → subscriber 방향으로 subscriber가 처리 가능한 양의 데이터만 전송되도록 한다.

reactive 스트림 처리 과정

  1. subscriber를 publisher에 등록해 데이터 스트림 수신 준비되었다는 것을 알림
  2. publisher가 subscriber에 데이터 전달 전에 BackPressure로 전송받을 데이터의 양을 제어함
  3. subscriber가 request(n) 메서드로 publisher에게 n개의 데이터를 요청하거나 cancel() 메서드로 데이터 스트림을 취소함
  4. publisher가 데이터를 생성할 때마다 onNext(data)가 호출되어 데이터를 subscriber에 전달
  5. 전송할 데이터가 없다면 onComplete() 또는 onError()가 호출됨

Publisher 종류

Mono

하나의 작업을 비동기적으로 처리 가능한 Publisher 클래스

작업 성공/실패에 대한 대기가 필요한 경우 적합하다.

메서드 이름기능
just()주어진 데이터를 포함하는 Mono 생성
empty()데이터가 없는 Mono 생성
error()에러 상황을 나타내는 Mono 생성
fromCallable()Callable 객체를 이용해 Mono 생성
fromFuture()Future 객체를 이용해 Mono 생성
fromRunnable()Runnable 객체를 이용해 Mono 생성

Flux

여러 작업들을 동시에 처리 가능한 Publisher 클래스

데이터의 연속적 흐름, BackPressure가 필요한 경우 적합하다.

BackPressure를 지원하며, map(), filter(), flatMap() 등의 연산자로 데이터 스트림을 가공 가능

메소드설명
just()주어진 데이터를 포함하는 Flux 생성
fromIterable()Iterable 객체를 이용해 Flux 생성
fromArray()배열을 이용해 Flux 생성
fromStream()Stream을 이용해 Flux 생성
empty()데이터가 없는 Flux 생성
error()에러 상황을 나타내는 Flux 생성
range()지정된 범위의 정수를 포함하는 Flux 생성
interval()일정 시간 간격으로 값을 생성하는 Flux 생성
merge()여러 개의 Flux를 병합하여 하나의 Flux로 병합
concat()여러 개의 Flux를 순차적으로 이어붙여 하나의 Flux로 병합
zip()여러 개의 Flux를 조합하여 튜플 형태로 변환
collectList()Flux에 포함된 모든 데이터를 리스트로 수집
collectMap()Flux에 포함된 모든 데이터를 맵으로 수집

Multi Event Loop

단일 스레드로 동작하는 이벤트 루프를 여러 개 사용해 동시에 많은 연결, 이벤트를 처리하는 아키텍처

이벤트 루프 내 많은 I/O가 발생해도 다른 루프를 통해 애플리케이션이 계속 동작할 수 있게 해 준다.

예시

아래와 같이 이벤트 루프 그룹들에 요청 수락 담당, I/O 작업 담당과 같은 역할을 분배할 수 있다.

EventLoopGroup bossGroup = new NioEventLoopGroup(1);  // 연결 수락 전담
EventLoopGroup workerGroup = new NioEventLoopGroup();  // I/O 작업 처리 (CPU 코어 수 만큼)

MVC와의 차이점

Spring MVC는 요청을 처리하는 동안 스레드를 대기 상태로 있게 하는 블로킹 I/O 방식 → 요청 - 응답 및 각 작업들이 순차적으로 진행되도록 하며 요청과 스레드의 비율은 1:1 → 요청이 몰릴 경우 스레드를 많이 생성해야 하고 서버를 다운시킬 가능성이 높다.

WebFlux는 하나의 스레드 내에서 요청 처리 중인 경우에도 다음 요청을 바로 처리 가능 → 요청과 스레드의 비율을 1:1로 맞추지 않아도 된다.

기술적 의사결정

예약 대기열 구현 시에 고려할 우선순위가 낮다고 판단되었다.

  • 예약이 많이 몰릴 경우 non-blocking I/O 방식의 고성능 Web Application가 필요한 건 사실
  • 하지만 예약 서비스 로직에서는 동시성 관리가 더 중요한 편
  • 요청에 대한 단순 queueing은 redis Sorted Set, List 같은 방식으로도 구현 가능
  • Spring 상에서의 Reactive Programming 방식은 러닝 커브가 높을 수 있음
  • 성능 최적화를 진행할 때 도입을 고려해 보는 것이 좋을 것
  • select 수행 시 여러 가지 이유로 blocking이 발생해 webflux의 성능이 감소할 수 있음
    • 현재 예약 등록 로직에서는 여러 select가 수행됨
    • MySQL DB 엔진 종류에 따라 쿼리를 동기적으로 수행
    • 정말 도입하고 싶다면 PostgreSQL 같은 DB로 변경할 필요가 있음
  • java 21버전부터는 스레드 생성 비용을 크게 낮춰 많은 스레드를 동시에 생성 가능하면서도 많은 요청을 동시에 처리 가능한 경량 스레드가 등장함
    • 많은 요청도 경량 스레드를 활용해 처리하면 굳이 webflux를 도입하지 않아도 되며 더 직관적인 코드 작성이 가능
    • 다만 현재 프로젝트는 17버전을 사용하고 있으므로 17 → 21 마이그레이션 작업이 요구될 수 있음
    • Kotlin 러닝커브를 극복할 수 있다면 메서드 단위로 경량 스레드 적용이 가능한 Coroutine을 사용하는 것도 생각해 볼 것

참고

https://adjh54.tistory.com/232

https://okky.kr/questions/1412539

https://techblog.woowahan.com/15398/

0개의 댓글

관련 채용 정보