리액티브 스트림즈 구현체, 리액터 한 번 써보기
Mono.just(“Hello”)를 호출하면 바로 “Hello”가 출력될까요? 리액터에서 데이터는 정확히 언제, 어떻게 흘러가기 시작할까요?
리액터 실습: 생성, 소비, 가공
리액티브 스트림즈의 4대 인터페이스(Publisher, Subscriber, Subscription, Processor)를 Reactor에서는 Mono/Flux(Publisher), subscribe()(Subscriber), ** 연산자(Operator)** 라는 구체적인 클래스와 메서드로 구현합니다.
이 글에서는 이 세 가지를 실제 코드로 다루면서 ”생성 → 가공 → 소비” 흐름을 익히는 것이 목표입니다.
Publisher: 데이터 생성
Reactor에서 Publisher를 구현한 대표 클래스는 Mono(01개)와 Flux(0N개)입니다. 이들은 컬렉션처럼 메모리에 값이 다 올라와 있는 것이 아니라, ** 나중에 도착할 수도 있고, 여러 번 나눠서 도착할 수도 있는 데이터 흐름 **을 표현합니다.
- Mono — 외부 API 호출 결과처럼 ”언젠가 한 번 올 수도, 안 올 수도 있는 결과” 를
Mono<Response>로 표현합니다. - Flux — SSE, WebSocket처럼 ”시간을 두고 0~N개의 값이 흘러오는 흐름” 을
Flux<Event>로 표현합니다.
생성 연산자
Mono/Flux 인스턴스를 만드는 메서드를 ** 생성 연산자 **라고 부릅니다.
Mono.just(“Hello”); // 이미 값이 있을 때
Mono.justOrEmpty(Optional.of(“Hello”)); // 값이 있을 수도, 없을 수도 있을 때
Mono.empty(); // 값 없이 완료
Flux.just(“Hello”, “World”); // 이미 값이 여러 개 있을 때
Flux.fromIterable(List.of(“A”, “B”, “C”)); // 컬렉션을 Flux로 변환
Flux.empty(); // 값 없이 완료
이 외에도 상황별로 다양한 생성 연산자가 있습니다.
| 연산자 | 용도 |
|---|---|
just(data) | 이미 값이 존재할 때 |
justOrEmpty(optional) | 값이 있을 수도, 없을 수도 있을 때 |
empty() | 값 없이 스트림을 끝낼 때 |
fromIterable(iterable) | List, Set 같은 컬렉션을 Flux로 변환 |
fromCallable(supplier) | 동기 함수 결과를 Mono로 감쌀 때 |
defer(supplier) | 구독할 때마다 새로 생성할 때 |
생성만으로는 아무 일도 일어나지 않는다
여기서 핵심적인 사실이 있습니다. Mono.just(value)는 값을 Mono 안에 ** 담아 두기만** 합니다. 아직 Subscriber에게 값을 전달하지 않습니다. 생성된 Mono/Flux는 연산자를 체이닝한 뒤, 마지막으로 subscribe()가 호출되는 시점에 비로소 값을 발행 하기 시작합니다.
subscribe()없이는 아무 일도 일어나지 않습니다. 이것이 리액터의 “게으른(lazy) 실행” 모델입니다.
Subscriber: 데이터 소비
subscribe() 메서드를 호출해야 비로소 데이터가 흐르기 시작합니다.
Mono.just(“Hello World”)
.subscribe(System.out::println); // 출력: Hello World
Flux.just(“Hello”, “World”)
.subscribe(System.out::println); // 출력: Hello \n World
subscribe()는 언제 호출할까
| 상황 | 누가 호출하나 |
|---|---|
| WebFlux 컨트롤러에서 Mono/Flux 반환 | 프레임워크 가 내부에서 호출 |
| 스케줄러, 배치, 백그라운드 작업 | 개발자 가 직접 호출 |
Spring WebFlux에서는 프레임워크가 subscribe()를 대신 호출해 주기 때문에, 비즈니스 코드에서 직접 호출할 일은 많지 않습니다. 다만 반환값 없이 메서드 내부에서 스트림을 끝내야 하는 경우에는 직접 호출해야 합니다.
Operator: 데이터 가공
생성과 소비 사이에서 데이터를 변환·필터링하는 역할을 하는 것이 연산자(Operator) 입니다.
Flux.just(“Hello”, “World”)
.map(String::toUpperCase) // ← 연산자: 대문자로 변환
.filter(s -> s.startsWith(“H”)) // ← 연산자: “H”로 시작하는 것만 통과
.subscribe(System.out::println); // 출력: HELLO
** 연산자 **는 업스트림에서 들어온 데이터를 가공하여 다운스트림으로 전달하는 ** 중간 처리자 **입니다. Reactor 내부에서는 FluxMap, FluxFilter 같은 클래스가 이 역할을 맡아, “위에서 받고 → 가공해서 → 아래로 전달하는” 파이프라인을 구성합니다.
마블 다이어그램으로 연산자 이해하기
Reactor에는 수백 개의 연산자가 존재합니다. 이를 모두 외울 수는 없고, 필요할 때마다 찾아서 사용해야 합니다. 이때 가장 큰 도움이 되는 것이 ** 마블 다이어그램(Marble Diagram)** 입니다.

마블 다이어그램은 연산자가 데이터 스트림을 어떻게 변형시키는지 ** 시간 흐름에 따라 시각적으로** 보여줍니다. 위쪽 타임라인이 입력, 가운데가 연산자, 아래쪽 타임라인이 출력입니다.
새로운 연산자를 처음 접했을 때는 Javadoc이나 공식 문서의 마블 다이어그램을 먼저 훑어보면, 코드를 읽기 전에 동작을 직관적으로 파악할 수 있습니다.
주의할 점
just()에 무거운 연산을 넣으면 안 된다
Mono.just(heavyComputation())은 just() 호출 시점에 이미 heavyComputation()이 ** 즉시 실행 **됩니다. 구독 시점까지 실행을 미루려면 Mono.fromCallable(() -> heavyComputation()) 또는 Mono.defer(() -> Mono.just(heavyComputation()))를 사용해야 합니다.
WebFlux에서 subscribe()를 직접 호출하면 위험하다
컨트롤러에서 Mono.just(...).subscribe()를 직접 호출하고 결과를 무시하면, ** 프레임워크가 관리하는 리액티브 체인과 분리된 별도의 실행 흐름 **이 생깁니다. 에러 처리가 누락되고, 트랜잭션 컨텍스트가 전파되지 않는 등의 문제가 발생할 수 있습니다.
정리
| 단계 | Reactor 구현 | 역할 |
|---|---|---|
| 생성 | Mono.just(), Flux.just(), fromIterable() 등 | 데이터 스트림을 만든다 |
| 가공 | map(), filter(), flatMap() 등 연산자 | 데이터를 변환·필터링한다 |
| 소비 | subscribe() | 데이터 흐름을 시작시킨다 |
핵심 흐름:
Mono/Flux로 스트림을 만들고, 연산자를 체이닝해 값을 가공한 다음,subscribe()가 호출되는 순간 실제로 데이터가 흘러가며 소비됩니다.