기록

[책리뷰] 스프링으로 시작하는 리액티브 프로그래밍 본문

교육/책

[책리뷰] 스프링으로 시작하는 리액티브 프로그래밍

zyin 2026. 2. 2. 10:00

개요

2025년 4월부터 5월까지 진행한 스터디에서 《스프링으로 시작하는 리액티브 프로그래밍》을 교재로 사용했다. 리액티브 프로그래밍의 기본 개념과 Reactive Streams, Project Reactor, Spring WebFlux의 핵심 내용을 다루는 책이다.

 

https://product.kyobobook.co.kr/detail/S000201399476

 

스프링으로 시작하는 리액티브 프로그래밍 | 황정식 - 교보문고

스프링으로 시작하는 리액티브 프로그래밍 | *리액티브 프로그래밍의 기본기를 확실하게 다진다*리액티브 프로그래밍은 적은 컴퓨팅 파워로 대량의 요청 트래픽을 효과적으로 처리할 수 있는

product.kyobobook.co.kr

목차

Part 01 리액티브 프로그래밍(Reactive Programming)
Chapter 01 리액티브 시스템과 리액티브 프로그래밍
Chapter 02 리액티브 스트림즈(Reactive Streams)
Chapter 03 Blocking I/O와 Non-Blocking I/O
Chapter 04 리액티브 프로그래밍을 위한 사전 지식

Part 02 Project Reactor
Chapter 05 Reactor 개요
Chapter 06 마블 다이어그램(Marble Diagram)
Chapter 07 Cold Sequence와 Hot Sequence
Chapter 08 Backpressure
Chapter 09 Sinks
Chapter 10 Scheduler
Chapter 11 Context
Chapter 12 Debugging
Chapter 13 Testing
Chapter 14 Operators

Part 03 Spring WebFlux
Chapter 15 Spring WebFlux 개요
Chapter 16 애너테이션 기반 컨트롤러
Chapter 17 함수형 엔드포인트(Functional Endpoint)
Chapter 18 Spring Data R2DBC
Chapter 19 예외 처리
Chapter 20 WebClient
Chapter 21 Reactive Streaming 데이터 처리

주요개념

1. Blocking I/O vs Non-Blocking I/O

Blocking I/O의 특징은 호출 결과를 받을 때까지 호출한 스레드가 점유된다는 점이다.

@GetMapping("/blocking")
public String blocking() {
    // 요청을 처리한 스레드가 여기서 멈춘다
    String result = restTemplate
        .getForObject("http://external/api", String.class);
    return result;
}

이 코드에서 요청을 처리하는 스레드는 외부 API 응답이 돌아올 때까지 아무 작업도 하지 못한다. 동시 요청이 늘어나면, 동일한 수의 스레드가 필요해진다. 문제는 구현체가 아니라 스레드 점유 모델이다.

 

Non-Blocking I/O에서는 I/O 요청 이후 스레드가 즉시 반환된다.

@GetMapping("/non-blocking")
public Mono<String> nonBlocking() {
    return webClient.get()
        .uri("http://external/api")
        .retrieve()
        .bodyToMono(String.class);
}

여기서 컨트롤러 메서드는 값을 반환하지 않고 처리 흐름을 반환한다. 실제 I/O 응답 처리는 이벤트 기반으로 진행되며, 요청을 받은 스레드는 빠르게 반환된다. 이 차이로 인해 처리량은 스레드 수가 아니라 설계된 실행 모델에 의해 결정된다. 

2. Flux vs Mono

Mono는 최대 한 번의 onNext 이후 반드시 종료 신호(onComplete 또는 onError)가 발생한다. 반면 Flux는 여러 번의 onNext가 가능하다.

Mono<String> mono =
    Mono.just("data");

mono.subscribe(
    data -> System.out.println("onNext: " + data),
    error -> System.out.println("onError"),
    () -> System.out.println("onComplete")
);


Flux<Integer> flux =
    Flux.just(1, 2, 3);

flux.subscribe(
    data -> System.out.println("onNext: " + data),
    error -> System.out.println("onError"),
    () -> System.out.println("onComplete")
);

 

3. Reactive Streams

Reactive Streams는 비동기 처리를 위한 표준 규약이다. 

Publisher<Integer> publisher = subscriber -> {
    subscriber.onSubscribe(new Subscription() {
        @Override
        public void request(long n) {
            subscriber.onNext(1);
            subscriber.onNext(2);
            subscriber.onComplete();
        }

        @Override
        public void cancel() {}
    });
};

소비자는 request(n)을 통해 처리 가능한 데이터 양을 명시한다.

publisher.subscribe(new Subscriber<>() {
    @Override
    public void onSubscribe(Subscription s) {
        s.request(1); // 한 개만 요청
    }

    @Override
    public void onNext(Integer item) {
        System.out.println(item);
    }

    @Override public void onError(Throwable t) {}
    @Override public void onComplete() {}
});

이 구조를 통해 Backpressure가 구현된다. 

4. Operator

리액티브 코드에서 Operator는 실행 코드가 아니라 실행 계획을 구성하는 요소이다.

Flux<Integer> pipeline =
    Flux.range(1, 5)
        .map(i -> i * 2)
        .filter(i -> i > 5);

이 시점에서는 아무 일도 발생하지 않는다. 실제 실행은 subscribe()가 호출될 때 시작된다.

 

Comments