리액티브 스트림즈와 프로젝트 리액터
RxJava, Akka Streams, Reactor 같은 리액티브 라이브러리가 각각 따로 만들어졌다면, 이 라이브러리들끼리 데이터를 주고받을 때는 어떻게 호환을 보장할까요?
리액티브 스트림즈와 프로젝트 리액터
리액티브 스트림즈(Reactive Streams) 는 비동기 스트림을 논블로킹 방식으로 처리하면서, 생산자와 소비자 사이의 속도 차이를 제어하기 위한 ** 표준 사양(Specification)** 입니다. 핵심은 Publisher, Subscriber, Subscription, Processor 4가지 인터페이스와 백프레셔 규약 을 정의하여, JVM 생태계의 서로 다른 라이브러리끼리도 호환되도록 한 것입니다.
4대 인터페이스
리액티브 스트림즈가 정의하는 4가지 인터페이스와 각각의 역할입니다.
Publisher — 데이터를 발행합니다.
public interface Publisher<T> {
void subscribe(Subscriber<? super T> s);
}
Subscriber — 데이터를 받아 처리합니다.
public interface Subscriber<T> {
void onSubscribe(Subscription s);
void onNext(T t);
void onError(Throwable t);
void onComplete();
}
Subscription — Publisher와 Subscriber 사이의 연결을 나타내며, 데이터 요청량을 제어합니다.
public interface Subscription {
void request(long n);
void cancel();
}
Processor — Publisher와 Subscriber를 모두 구현하는 중개자입니다. (Reactor 3.5부터 deprecated, Sinks로 대체)
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> { }
데이터가 흐르는 과정
데이터가 Publisher에서 Subscriber까지 도달하는 과정을 단계별로 살펴보겠습니다.
| 단계 | 동작 | 호출 메서드 |
|---|---|---|
| 1 | Subscriber가 Publisher를 구독 | publisher.subscribe(subscriber) |
| 2 | Publisher가 Subscription을 생성해 Subscriber에게 전달 | subscriber.onSubscribe(subscription) |
| 3 | Subscriber가 처리 가능한 데이터 개수를 요청 | subscription.request(n) |
| 4 | Publisher가 요청된 개수만큼 데이터를 전달 | subscriber.onNext(data) |
| 5 | 데이터가 모두 전달되면 완료 신호 전송 | subscriber.onComplete() |
이 과정에서 핵심은 3단계 입니다. Subscriber가 자신이 처리할 수 있는 만큼만 데이터를 요청하기 때문에, Publisher가 일방적으로 데이터를 밀어 넣는 것을 방지합니다.
구독 시점에 데이터를 바로 요청하는 패턴을 코드로 표현하면 다음과 같습니다.
public class CustomSubscriber implements Subscriber<T> {
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(1); // ← 1개만 먼저 요청
}
}

백프레셔: 왜 Subscriber가 주도권을 가지는가
만약 Publisher가 데이터 흐름의 주도권을 갖는다면 어떻게 될까요?
- Publisher는 Subscriber의 처리 상태를 모른 채 계속 데이터를 발행 합니다.
- 처리하지 못한 데이터가 **큐에 끝없이 쌓입니다 **.
- 결국 ** 메모리 고갈(OOM)** 이나 ** 지연 폭증 **이 발생합니다.
이 문제를 해결하기 위해 리액티브 스트림즈는 ** 백프레셔(Backpressure)** 메커니즘을 정의합니다. Subscriber가 Subscription.request(n)으로 자신이 처리할 수 있는 개수를 명시적으로 알려 주고, Publisher는 이 요청 범위 안에서만 데이터를 발행합니다.
백프레셔는 빠른 생산자와 느린 소비자 사이의 속도 차이를 흡수하는 ** 풀(Pull) 기반 흐름 제어 전략 **입니다.
프로젝트 리액터(Project Reactor)
Project Reactor 는 리액티브 스트림즈 명세를 구현한 대표적인 라이브러리이며, Spring WebFlux의 기본 리액티브 엔진 입니다. RxJava, Akka Streams, Java 9 Flow API도 같은 명세를 구현하지만, Spring 생태계에서는 Reactor가 사실상 표준입니다.
Reactor는 Publisher 인터페이스를 구현한 두 가지 리액티브 타입 을 제공합니다.
Mono (0~1개)
Mono 는 0개 또는 1개의 데이터만 발행하는 Publisher입니다. HTTP 응답, DB 단건 조회처럼 결과가 하나이거나 없는 경우 에 사용합니다.
Mono<User> user = userRepository.findById(id);
// 결과: 1건이면 onNext → onComplete, 없으면 바로 onComplete
Flux (0~N개)
Flux 는 0개부터 N개(무한대 포함)의 데이터를 발행하는 Publisher입니다. 리스트 조회, 이벤트 스트림처럼 여러 개의 데이터가 지속적으로 들어오는 경우 에 사용합니다.
Flux<Order> orders = orderRepository.findByCustomerId(customerId);
// 결과: 여러 건의 onNext → onComplete
주의할 점
Processor는 deprecated — Sinks를 사용해야 한다
Reactor 3.5부터 Processor 계열 클래스는 deprecated 되었습니다. 외부에서 값을 주입해야 하는 경우에는 Sinks를 사용해야 합니다. Processor는 Publisher와 Subscriber 규약을 동시에 지켜야 해서 스레드 안전성 문제가 발생하기 쉬웠기 때문입니다.
subscribe() 없이는 아무 일도 일어나지 않는다
Mono나 Flux를 생성하고 연산자를 체이닝해도, **subscribe()가 호출되기 전까지는 데이터가 흐르지 않습니다 **. 이것은 리액티브 스트림즈가 "게으른(lazy)" 실행 모델을 따르기 때문입니다.
정리
| 항목 | 설명 |
|---|---|
| 리액티브 스트림즈 | 비동기 스트림 처리를 위한 표준 사양 (4대 인터페이스 + 백프레셔 규약) |
| Publisher | 데이터를 발행하는 인터페이스 |
| Subscriber | 데이터를 소비하는 인터페이스 |
| Subscription | 연결 관계 + 요청량 제어 (request(n), cancel()) |
| 백프레셔 | Subscriber가 처리 가능한 만큼만 요청하는 풀 기반 흐름 제어 |
| Mono | 0~1개 데이터 발행 (단건 조회, HTTP 응답) |
| Flux | 0~N개 데이터 발행 (리스트 조회, 이벤트 스트림) |