Reactive Programming 입문 — 핵심 정리

2026-05-03AWS SAA-C03 스터디

Java Reactive Programming 핵심 정리 시리즈 첫 글. 비동기 논블로킹이 왜 필요한지부터 수도 파이프라인 비유로 풀어가며 — 프로세스/스레드 메모리 한계, I/O 모델 4가지, 통신 패턴 4가지, Reactive Streams 4대 인터페이스(Publisher·Subscriber·Subscription·Processor), Mono vs Flux, Lazy Execution, 배압 메커니즘까지 처음 보는 사람도 따라올 수 있게 친절하게 풀어쓴 1편.

📚 Java Reactive Programming 핵심 정리 · 1편 / 14편 — 핵심 정리

이 글은 Java Reactive Programming 핵심 정리 시리즈의 첫 번째 편입니다. 마이크로서비스·실시간 채팅·스트리밍 데이터 같은 단어가 자주 등장하는 영역에서 거의 빼놓을 수 없는 도구가 Reactive Programming이에요. Spring WebFlux를 쓴다면 그 기반이 Project Reactor고, RxJava로 안드로이드를 짠다면 같은 패러다임이 깔려 있습니다.

이 시리즈는 13편을 통해 Reactive Streams 명세부터 Mono/Flux·연산자·스레딩·배압·테스트까지 차근차근 쌓아 갑니다. 한 번에 다 외우려 하지 마시고, 이번 1편에서는 "왜 Reactive Programming이 만들어졌는가, 어떤 부품으로 구성되는가, Mono/Flux는 뭔가" — 이 세 가지 질문의 답만 머리에 들어와도 충분합니다.

본문 흐름은 수도 파이프라인 비유를 따라 풀어 가요. 데이터가 물처럼 흐르고, Publisher는 수도꼭지, Subscriber는 컵, Subscription은 그 사이의 밸브 — 이 한 그림만 잡고 가면 인터페이스 네 개가 한 번에 정리됩니다.

📚 학습 노트

이 시리즈는 Project Reactor 공식 문서, Reactive Streams 명세, 여러 비동기 프로그래밍 학습 자료 등 공개 자료를 참고해 한국어 학습 노트로 풀어쓴 자료입니다.

읽으면서 IDE에 reactor-core 의존성을 추가하고 Mono·Flux를 직접 호출해 보면 머리에 훨씬 잘 박혀요. 30분이면 첫 파이프라인을 만들 수 있습니다.

왜 Reactive Programming이 처음엔 어렵게 느껴질까요

이유는 네 가지예요.

첫째, 동기 사고에 너무 익숙합니다. 자바를 배울 때 거의 처음부터 "한 줄씩 차례로 실행"이라는 모델로 생각해 왔어요. Reactive Programming은 그 반대 — 데이터가 흘러갈 길만 깔아 두고 실제 실행은 한참 뒤에 일어나는 모델이라 처음엔 직관에 반합니다.

둘째, 인터페이스가 4개나 한 번에 등장합니다. Publisher·Subscriber·Subscription·Processor — 한 페이지에 이 단어들이 다 나오면 머리가 어지러워져요.

셋째, 람다·Stream과 비슷해 보이지만 다릅니다. map·filter·flatMap 같은 메서드 이름이 똑같아 처음엔 "Stream의 비동기 버전인가" 싶은데, 실행 모델·라이프사이클·스레드 처리가 모두 달라요.

넷째, "구독해야 실행된다"가 직관에 반합니다. Reactive Programming의 가장 큰 함정 — Mono.just("hello").map(...) 만 적어두면 아무것도 일어나지 않아요. 누군가 subscribe()를 호출해야 비로소 시작되는데, 이 Lazy 실행이 처음엔 정말 헷갈립니다.

해결법은 한 가지예요. Reactive Programming을 "수도 파이프라인" 으로 잡고 풀면 갑자기 명확해집니다. 데이터가 물이고, Publisher는 수도꼭지, Subscriber는 컵, Subscription은 그 사이 밸브, Backpressure는 "컵이 차면 잠가 달라"는 신호 — 이 그림 하나가 모든 개념을 묶어 줘요. 이 글은 그 비유를 따라 처음부터 풀어 갑니다.

왜 비동기 논블로킹이 필요한가 — 스레드의 한계

먼저 Reactive Programming이 풀려고 하는 문제부터 짚고 가요. 핵심은 스레드의 메모리 한계입니다.

회사 비유로 풀면 — 자바 스레드는 OS 커널 스레드를 1:1로 래핑해요. 각 스레드가 약 1MB의 스택 메모리를 점유합니다. 1만 개 동시 요청을 처리하려고 1만 개 스레드를 띄우면? 메모리 10GB를 스택만으로 잡아먹어요. 그것도 대부분 시간을 I/O 대기로 멍하니 흘려보내면서요.

개념설명메모리 특성
프로세스(Process)독립된 메모리 공간을 가진 프로그램 실행 인스턴스힙·스택·메타데이터
스레드(Thread)프로세스 내에서 실제 코드를 실행하는 단위힙 공유, 스택은 각자 독립
컨텍스트 스위칭OS 스케줄러가 여러 스레드 간 CPU 실행을 빠르게 전환CPU 레지스터 저장·복원 비용
Java 스레드OS 커널 스레드를 1:1 래핑OS 한계를 그대로 가짐

핵심 문제 정리:

  • 각 스레드는 약 1MB 스택을 점유
  • I/O 대기 중에도 스택을 계속 잡고 있어 CPU 낭비
  • 스레드를 무작정 늘리면 메모리 고갈
  • 잦은 컨텍스트 스위칭은 CPU 오버헤드

여기서 시험 함정이 하나 있어요. 힙(Heap)은 모든 스레드가 공유, 스택(Stack)은 각자 독립입니다. new ArrayList() 같은 객체는 힙에, 메서드 호출 정보·지역 변수는 스택에. 스레드를 늘리면 늘어나는 건 스택만이에요.

> 한 줄 정리 — Reactive Programming은 "적은 스레드로 많은 요청을 처리" 하는 게 목표. 물리적 스레드 한계를 우회하는 패러다임.

I/O 모델 4가지 — 보험사 전화 비유

네트워크 통신을 처리하는 방식은 크게 4가지예요. 보험사 전화 비유로 한 번에 잡힙니다.

모델비유 (보험사 전화)호출 스레드작업 스레드성능
동기 블로킹전화 걸고 상담원 받을 때까지 기다리기차단낮음
비동기 블로킹친구한테 "내 대신 기다려줘" 부탁비차단차단중간
비블로킹번호 남기고 상담원이 다시 걸어주길 기다림비차단 (콜백 대기)높음
비동기 비블로킹번호만 남기고 다른 일 하다가 다른 담당자가 처리비차단비차단매우 높음

여기서 시험 함정이 하나 있어요. 비동기 비블로킹이 가장 뛰어나지만 구현이 매우 복잡합니다. Reactive Programming은 그 복잡성을 추상화해 개발자가 선언적으로 비동기 비블로킹 코드를 짤 수 있게 해 주는 도구예요.

통신 패턴 4가지

Reactive Programming이 지원하는 통신 패턴도 4가지로 정리됩니다.

패턴설명실제 예시
요청-응답단일 요청 → 단일 응답REST API 호출
요청-스트림단일 요청 → 다수 응답주문 후 배달 상태 실시간 추적
스트림-응답다수 요청 → 단일 응답웨어러블 심박수 데이터 누적 전송
양방향 스트리밍다수 ↔ 다수실시간 채팅

여기서 시험 함정이 하나 있어요. 가상 스레드(Virtual Threads, Java 21+)는 요청-응답 패턴에 효율적이지만 스트리밍 패턴은 여전히 Reactive가 더 적합합니다. 두 기술은 서로 다른 문제를 풀고, 함께 쓸 수도 있어요. "가상 스레드가 나오니까 Reactive는 끝났다"는 단정은 부정확합니다.

Reactive Programming의 4가지 정의 키워드

이 4개를 머리에 박아두면 모든 후속 개념이 이걸 변형한 거예요.

  • 비동기(Asynchronous) — 작업 완료를 기다리지 않고 다른 작업 처리
  • 논블로킹(Non-blocking) — 스레드가 대기하지 않고 즉시 제어권 반환
  • 배압(Backpressure) — 생산자 속도를 소비자 처리 속도에 맞춰 조절
  • 관찰자 패턴 기반 — 데이터 변화 시 구독자에게 자동 알림

Reactive Streams 명세 — JPA처럼 표준이 먼저

여기서 정말 중요한 시험 함정이 하나 있어요. Reactive Streams라이브러리가 아니라 명세(Specification) 입니다. 2014년에 정립된 비동기 스트림 처리 표준이고, 자바 9부터 java.util.concurrent.Flow 패키지에 표준으로 들어왔어요.

회사 비유로 — JPA가 명세고 Hibernate가 구현체인 것처럼, Reactive Streams 명세를 구현한 라이브러리가 Project Reactor·RxJava 입니다. 우리가 직접 사용하는 건 Project Reactor 같은 구현체지만, 그 뼈대는 Reactive Streams 명세에 이미 정해져 있어요. 자세한 사양은 Reactive Streams 공식 문서에서 확인할 수 있습니다.

4대 인터페이스 — 수도 파이프라인의 부품

Reactive Streams 명세는 단 4개의 인터페이스로 정의돼요.

// 1. Publisher — 데이터를 발행하는 생산자 (수도꼭지)
public interface Publisher<T> {
    void subscribe(Subscriber<? super T> subscriber);
}

// 2. Subscriber — 데이터를 소비하는 구독자 (컵)
public interface Subscriber<T> {
    void onSubscribe(Subscription subscription);  // 구독 시작
    void onNext(T item);                           // 데이터 수신
    void onError(Throwable throwable);             // 에러 발생
    void onComplete();                             // 모든 데이터 완료
}

// 3. Subscription — Publisher와 Subscriber 간 통신 채널 (밸브)
public interface Subscription {
    void request(long n);  // 구독자가 n개 데이터 요청 — 배압의 핵심!
    void cancel();         // 구독 취소
}

// 4. Processor — Publisher + Subscriber 동시 (중간 가공기)
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

회사 비유로 한 번 더 정리:

인터페이스비유역할
Publisher수도꼭지데이터를 흘려보냄
Subscriber데이터를 받아 처리
Subscription밸브흐름 양 조절·차단
Processor중간 정수기흘러가는 물을 가공

Publisher-Subscriber 통신 흐름

이 흐름이 머리에 잡히면 Reactive 코드를 디버깅할 때 어디서 무슨 일이 일어나는지 명확해집니다.

[1단계] 구독 요청
  Subscriber → Publisher.subscribe(subscriber)

[2단계] 구독 수락 + Subscription 전달
  Publisher → Subscriber.onSubscribe(subscription)

[3단계] 데이터 요청 (배압의 핵심!)
  Subscriber → subscription.request(n)  // n개만 요청

[4단계] 데이터 전달
  Publisher → Subscriber.onNext(item1)
  Publisher → Subscriber.onNext(item2)
  ... (최대 n개까지)

[5단계] 스트림 종료 (둘 중 하나)
  성공: Publisher → Subscriber.onComplete()
  실패: Publisher → Subscriber.onError(throwable)

[선택] 구독 취소
  Subscriber → subscription.cancel()
    → Publisher가 데이터 생성 즉시 중단

여기서 시험 함정이 하나 있어요. onComplete()onError()는 상호 배타예요. 둘 중 하나가 호출되면 스트림 완전 종료. 그리고 생산자는 구독자가 요청한 개수를 초과해서 데이터를 보낼 수 없습니다. 적게 보낼 수는 있어요(데이터가 그만큼 없으면 onComplete).

용어 혼용 — 같은 개념을 다양한 용어로 부릅니다.

> Publisher = Producer = Upstream = Observable > Subscriber = Consumer = Downstream = Observer

> 한 줄 정리 — Reactive Streams는 4개 인터페이스 + 통신 흐름 5단계로 끝. 외워두면 평생 갑니다.

Project Reactor — Spring 팀의 Java Reactive 구현체

Project Reactor는 Reactive Streams 명세를 구현한 Java 라이브러리입니다. Spring 팀이 개발하고, Spring WebFlux의 기반이에요. 자바 진영에서 사실상 표준 위치를 차지하고 있습니다. 자세한 동작은 Project Reactor 공식 문서에서 볼 수 있어요.

핵심 타입 두 가지:

타입아이템 수Java 비유주 용도
Mono\0~1개Optional (비동기)DB 단건 조회·API 단일 응답
Flux\0~N개 (무한 가능)Stream (비동기)목록 조회·이벤트 스트림

Project Reactor의 핵심 특성 3가지:

  • Lazy Execution — 구독자가 subscribe()를 호출하기 전까지 파이프라인은 아무것도 실행하지 않음
  • 기본 스레딩 — 모든 스트림 처리가 현재 스레드에서 실행. 다른 스레드 쓰려면 Schedulers 명시
  • 불변성 — 각 연산자는 기존 스트림을 변경하지 않고 새로운 Publisher 인스턴스를 반환

여기서 정말 중요한 시험 함정 — subscribe() 호출 없이는 아무것도 실행되지 않습니다. 이게 Reactive에서 가장 자주 만나는 함정이에요.

// 잘못된 예 — subscribe 없으면 아무것도 일어나지 않음
Mono<String> mono = Mono.just("Hello")
    .map(s -> s + " World")
    .doOnNext(System.out::println);  // 출력 안 됨!

// 올바른 예
mono.subscribe();  // 또는 mono.subscribe(System.out::println);

직접 구현해 보기 — 학습용 Publisher/Subscriber

실무에서는 Mono/Flux를 그대로 쓰지만, 원리 이해를 위해 한 번 직접 구현해 보면 머리에 잘 박혀요. 이메일 리스트를 발행하는 Publisher 예시.

// Subscription 구현
public class EmailSubscription implements Subscription {
    private final Subscriber<? super String> subscriber;
    private final List<String> emails;
    private int index = 0;
    private boolean cancelled = false;

    public EmailSubscription(Subscriber<? super String> subscriber, List<String> emails) {
        this.subscriber = subscriber;
        this.emails = emails;
    }

    @Override
    public void request(long n) {
        // 구독자가 요청한 만큼만 데이터 전송 — 배압 구현
        long sent = 0;
        while (!cancelled && index < emails.size() && sent < n) {
            subscriber.onNext(emails.get(index++));
            sent++;
        }
        // 더 이상 보낼 데이터가 없으면 완료 신호
        if (!cancelled && index >= emails.size()) {
            subscriber.onComplete();
        }
    }

    @Override
    public void cancel() {
        this.cancelled = true;
    }
}

// Publisher 구현
public class EmailPublisher implements Publisher<String> {
    private final List<String> emails = List.of(
        "user1@example.com", "user2@example.com", "user3@example.com"
    );

    @Override
    public void subscribe(Subscriber<? super String> subscriber) {
        subscriber.onSubscribe(new EmailSubscription(subscriber, emails));
    }
}

// Subscriber 구현
public class EmailSubscriber implements Subscriber<String> {
    private Subscription subscription;
    private final int batchSize;

    public EmailSubscriber(int batchSize) {
        this.batchSize = batchSize;
    }

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(batchSize);  // 처음 batchSize개 요청
    }

    @Override
    public void onNext(String email) {
        System.out.println("수신: " + email);
        // 추가 데이터 요청 가능 (스트리밍 패턴)
        // subscription.request(1);
    }

    @Override
    public void onError(Throwable t) {
        System.err.println("오류: " + t.getMessage());
    }

    @Override
    public void onComplete() {
        System.out.println("모든 이메일 수신 완료");
    }
}

이 코드를 직접 돌려 보면 request(n) 한 번에 onNext()가 n번까지 일어나고 마지막에 onComplete() 한 번 패턴이 손에 익습니다.

Project Reactor 실전 사용 — 같은 일을 짧게

위 직접 구현은 학습용이고, 실무에서는 Mono/Flux로 같은 걸 한 줄로 끝낼 수 있어요.

import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

// Mono 기본 사용
Mono<String> mono = Mono.just("Hello Reactive");
mono.subscribe(
    value -> System.out.println("수신: " + value),    // onNext
    error -> System.err.println("에러: " + error),    // onError
    () -> System.out.println("완료!")                  // onComplete
);

// Flux 기본 사용
Flux<Integer> flux = Flux.just(1, 2, 3, 4, 5);
flux.subscribe(
    value -> System.out.println("수신: " + value),
    error -> System.err.println("에러: " + error),
    () -> System.out.println("완료!")
);

Mono vs Flux — 자기 자리

다음 시리즈 글에서 자세히 풀지만 큰 그림은 1편에서 잡고 갑니다.

속성MonoFlux
아이템 수0~1개0~N개 (무한 가능)
Java 비유Optional (비동기)Stream (비동기)
주 용도단건 조회·단일 응답·단일 계산목록·이벤트 스트림·무한 스트림
변환mono.flux() → Fluxflux.next() → 첫 아이템만 Mono

여기서 시험 함정이 하나 있어요. 단일 결과지만 비동기인 케이스도 모두 Mono입니다. "DB에서 ID로 한 건 조회"는 결과가 0건일 수도 1건일 수도 있는데, 두 경우 모두 Mono로 표현해요.

자주 만나는 함정 10가지 — 시험 직전 압축 노트

여기까지가 Reactive Programming 1편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Reactive Programming = 비동기 데이터 스트림 + 배압 + 관찰자 패턴
  • 비동기 + 논블로킹 + 배압 + 관찰자 — 4가지 정의 키워드
  • 자바 스레드 1MB 스택 한계 → Reactive로 적은 스레드로 많은 요청 처리
  • I/O 모델 4가지 — 동기 블로킹 / 비동기 블로킹 / 비블로킹 / 비동기 비블로킹(가장 빠름)
  • 가상 스레드(Virtual Threads)는 요청-응답에 강함, 스트리밍은 여전히 Reactive
  • Reactive Streams는 명세, 구현체는 Project Reactor·RxJava (JPA-Hibernate 관계)
  • 4대 인터페이스 — Publisher · Subscriber · Subscription · Processor
  • 통신 흐름 5단계 — subscribe → onSubscribe → request(n) → onNext × n → onComplete/onError
  • 구독자가 request(n)을 호출해야 데이터 흐름 (배압의 기본)
  • onCompleteonError는 상호 배타 — 둘 중 하나만 호출
  • 생산자는 요청 개수 초과 X, 미달 OK
  • Project Reactor — Spring 팀 개발, Spring WebFlux 기반
  • Mono = 0~1개 (Optional 비동기), Flux = 0~N개 (Stream 비동기)
  • Lazy Executionsubscribe() 안 부르면 아무것도 안 일어남
  • 기본 스레딩 — 현재 스레드 실행 (Schedulers로 변경)
  • 불변성 — 각 연산자는 새 Publisher 반환

시리즈 다른 편

같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!