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 인스턴스를 만드는 메서드를 ** 생성 연산자 **라고 부릅니다.

JAVA
Mono.just(“Hello”);                         // 이미 값이 있을 때
Mono.justOrEmpty(Optional.of(“Hello”));     // 값이 있을 수도, 없을 수도 있을 때
Mono.empty();                               // 값 없이 완료
JAVA
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() 메서드를 호출해야 비로소 데이터가 흐르기 시작합니다.

JAVA
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) 입니다.

JAVA
Flux.just(“Hello”, “World”)
    .map(String::toUpperCase)         // ← 연산자: 대문자로 변환
    .filter(s -> s.startsWith(“H”))   // ← 연산자: “H”로 시작하는 것만 통과
    .subscribe(System.out::println);  // 출력: HELLO

** 연산자 **는 업스트림에서 들어온 데이터를 가공하여 다운스트림으로 전달하는 ** 중간 처리자 **입니다. Reactor 내부에서는 FluxMap, FluxFilter 같은 클래스가 이 역할을 맡아, “위에서 받고 → 가공해서 → 아래로 전달하는” 파이프라인을 구성합니다.

마블 다이어그램으로 연산자 이해하기

Reactor에는 수백 개의 연산자가 존재합니다. 이를 모두 외울 수는 없고, 필요할 때마다 찾아서 사용해야 합니다. 이때 가장 큰 도움이 되는 것이 ** 마블 다이어그램(Marble Diagram)** 입니다.

map 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()가 호출되는 순간 실제로 데이터가 흘러가며 소비됩니다.

댓글 로딩 중...