콘텐츠로 바로가기

Reactive Programming & Backpressure

비동기 데이터 흐름을 선언적으로 다루는 프로그래밍 사상과, 소비자가 생산자의 속도를 제어하는 백프레셔(Backpressure) 물리학을 다루는 학습 노드입니다.

Article
M

Me

hyunyoun's Blog

system-architecture-distributed-systemssystem-architecturedistributed-systemsevent-drivenreactive-systemsreactive-programmingbackpressurecqrs9 min read

1. Overview

리액티브 프로그래밍과 백프레셔(Reactive Programming & Backpressure)는 트래픽 폭풍이 몰아칠 때 스레드(Thread)를 늘려 버티던 낡은 블로킹(Blocking) 방식의 종말을 선언하고, 적은 수의 스레드로 엄청난 양의 데이터를 물 흐르듯 밀어내는 이벤트 루프(Event Loop)의 극한 최적화를 해부합니다.

학습자는 스레드가 I/O 작업(DB/네트워크)을 기다리며 멍때리는 시간을 0으로 수렴시키는 비동기 논블로킹(Asynchronous Non-blocking) 철학과, 이를 데이터 스트림(Stream) 관점에서 우아하게 조작하는 **리액티브 스트림즈(Reactive Streams, RxJava, Project Reactor)**의 수학적 뼈대를 뜯어봅니다. 나아가 댐이 터지듯 폭발하는 데이터를 하위 서버가 감당하지 못할 때, "내가 처리할 수 있는 만큼만 보내!"라고 역으로 목을 조르는 궁극의 생존 기제인 백프레셔(Backpressure) 메커니즘을 장악합니다. 마지막으로, 스프링 웹플럭스(Spring WebFlux) 같은 리액티브 프레임워크가 모놀리식 톰캣(Tomcat)을 어떻게 무너뜨리는지 아키텍처 관점으로 튜닝하는 역량을 확보합니다.

2. Scope & Boundaries

In-Scope

  • Reactive Manifesto (리액티브 선언문): Responsive(응답성), Resilient(탄력성), Elastic(유연성), Message-Driven(메시지 구동).
  • Asynchronous Non-blocking: Thread Pool의 한계(Context Switching 오버헤드)와 Event Loop 메커니즘.
  • Reactive Streams API: Publisher(발행자), Subscriber(구독자), Subscription, Processor.
  • Backpressure (배압): Pull 기반의 스트림 흐름 제어, 버퍼 터짐(OOM) 방지.

Out-of-Scope

  • JavaScript Event Loop 구현체: 자바스크립트 엔진 내부 구조 \rightarrow 05-04 Language Platforms 영역으로 위임.
  • 메시지 브로커 (Kafka/RabbitMQ): 인프라 레벨의 비동기 큐 \rightarrow 07-04-01 Message Brokers 영역 (본 문서는 애플리케이션 '코드 레벨'의 리액티브 처리에 집중).

Boundaries

  • Thread-per-request vs Event Loop: 전통적인 Spring Boot(MVC)는 유저 요청 1개당 스레드 1개를 통째로 할당합니다. DB 쿼리 응답이 2초 걸리면 그 스레드는 2초 동안 기절(Block)합니다. 트래픽 1만 개가 들어오면 스레드 1만 개가 기절하고 서버는 메모리 부족(OOM)으로 죽습니다. 반면 Reactive(WebFlux/Node.js)는 스레드를 CPU 코어 수(예: 4개)만큼만 두고 절대 기절시키지 않습니다. DB에 쿼리를 던져두고 응답을 기다리지 않은 채 바로 다음 유저 요청을 받는 '콜백(Callback)' 아키텍처입니다. 그러나, DB 드라이버(JDBC) 자체가 블로킹(Blocking) 방식이라면 리액티브 프레임워크를 도입해봤자 병목이 해결되지 않는 '가짜 리액티브(Fake Reactive)'의 맹점을 명확히 경계 짓습니다.

3. Counterexample

  • 백프레셔 없는 폭포수(OOM): 카프카에서 초당 10만 건의 결제 로그가 들어옵니다. 백엔드 시스템은 DB에 초당 1,000건밖에 저장하지 못합니다. 데이터를 밀어 넣는 속도(Push)를 제어할 백프레셔(Backpressure) 장치가 없으면, 남은 99,000건의 데이터는 서버의 RAM(Buffer)에 고스란히 쌓이다가 1분 만에 OutOfMemory(OOM) 에러를 뿜으며 서버를 죽여버립니다. 하위 시스템을 배려하지 않은 맹목적인 Push 아키텍처의 파국입니다.
  • 리액티브 안에서의 블로킹 (The Hidden Blocker): 최고급 리액티브 프레임워크(Spring WebFlux)를 도입해 놓고, 그 내부 로직에서 Thread.sleep()을 호출하거나 구형 RDBMS 드라이버(JDBC)를 썼습니다. 단 4개뿐인 이벤트 루프 스레드 중 하나가 거기서 멈춰(Block)버려 전체 서버 트래픽 처리량의 25%가 증발해버립니다. 논블로킹 생태계에 단 하나의 블로킹 코드라도 섞이면 전체 시스템이 썩어 들어가는 안티 패턴입니다.

4. Prerequisites

  • 동기 vs 비동기 (Basic): Blocking과 Non-blocking의 차이. (04-06 Functional & Async)
  • 운영체제 스레드 (Basic): 컨텍스트 스위칭 오버헤드. (03-01 OS Core)

5. Learning Map

Sequence Core Cluster Objective & Description Evidence (BoK)
1 The Thread Illusion 스레드를 무한히 늘리면 빨라질 거라는 착각을 박살 내고, 4개의 스레드로 1만 명을 상대하는 논블로킹 이벤트 루프를 쥡니다. P1
2 Reactive Streams 콜백 지옥(Callback Hell)을 수학적이고 체인(Chain) 형태의 스트림 연산(Map, Filter, FlatMap)으로 우아하게 제어하는 뼈대를 해부합니다. P5
3 Backpressure (배압) 폭포수처럼 쏟아지는 데이터를 댐(Buffer)에 가둬두고, 하위 서버가 "나 10개만 줘!"라고 역으로 요청(Pull)할 때만 물을 흘려보내는 궁극의 방어막을 뜯어봅니다. Industry
4 Reactive Architecture (End-to-End) 웹 서버(WebFlux)부터 데이터베이스(R2DBC)까지, 단 한 곳의 병목(Blocking)도 허용하지 않는 완전한 비동기 생태계 설계를 장악합니다. Industry

6. Learning Topics

Basic

Core Topic 01: 스레드 고갈과 논블로킹의 반격 (The Thread Illusion)

  • Why to Learn: 유저가 1만 명 몰렸을 때 톰캣(Tomcat) 스레드를 1만 개로 늘리면 CPU가 스레드 교체(Context Switching)만 하다가 뻗어버리는 물리적 한계를 직시하고, 이벤트 루프로 전환하기 위함입니다.
  • What to Learn:
    • Concepts: Thread-per-request Model, Blocking I/O, Context Switching Overhead, Asynchronous Non-blocking, Event Loop.
    • Skills: 기존 블로킹 API 성능 테스트를 통한 스레드 풀(Thread Pool) 병목 지점 식별.
  • How to Learn:
    • 1단계: 블로킹의 지옥: 식당에 점원(스레드)이 10명 있습니다. 손님이 짬뽕을 시키면 점원은 주방(DB)에 주문을 넣고, 짬뽕이 나올 때까지 주방장 앞에서 10분 동안 아무 일도 안 하고 서서 멍을 때립니다(Blocking). 손님이 11명이 오면 식당은 마비됩니다.
    • 2단계: 논블로킹의 구원: 점원이 딱 1명(Event Loop)입니다. 손님 주문을 주방에 넣고 진동벨(Callback)을 준 뒤, 바로 다음 손님 주문을 받습니다(Non-blocking). 짬뽕이 완성되어 진동벨이 울리면(Event) 그때 배달만 해줍니다. 점원 1명이 손님 1,000명을 쳐내는 기적을 해부합니다.
  • Implement: Thread-per-request vs Event Loop 성능 비교 스크립트. Python threading (100개 스레드 생성 오버헤드 측정) vs asyncio (단일 스레드 코루틴 100개 실행). 둘 다 sleep(1)(I/O 바운드 모사)을 실행했을 때, asyncio가 압도적으로 메모리를 덜 먹고 빠르게 종료되는 현상 렌더링.

Core Topic 02: 데이터의 강물, 리액티브 스트림즈 (Reactive Streams)

  • Why to Learn: 비동기 논블로킹 코드를 짜다 보면 콜백(Callback) 함수가 꼬리를 물어 코드가 쓰레기통(Callback Hell)이 되는 현상을, 우아한 함수형 스트림 조작으로 정화하기 위함입니다.
  • What to Learn:
    • Concepts: Reactive Streams API, Publisher / Subscriber, Flux(0N개) / Mono(01개), Operator Chain (Map, Filter).
    • Skills: 비동기 데이터 리스트를 리액티브 연산자 체인으로 가공 및 변환(Transform)하기.
  • How to Learn:
    • 1단계: 콜백 지옥: DB.find(user -> HTTP.get(user.id, data -> File.write(data, result -> ...))). 비동기로 짰더니 들여쓰기가 끝없이 깊어져 에러 처리(try/catch)가 불가능해지는 참사를 뜯어봅니다.
    • 2단계: 파이프라인 조립: Publisher가 강물을 뿜어냅니다. 그 강물에 filter(조건), map(변환), flatMap(비동기 호출) 파이프를 달아놓기만 하면, 데이터가 흐르면서 알아서 깎이고 가공되어 Subscriber의 입에 골인하는 함수형(Functional) 선언형 프로그래밍을 해부합니다.
  • Implement: 리액티브 연산자 모사 파이썬 스크립트. 배열 [1, 2, 3, 4, 5]를 소스로 하여, map(x*2), filter(x>5) 체인을 통과한 비동기 방출 제너레이터 구현. 최종적으로 Subscriber[6, 8, 10]을 비동기적으로 받아 먹는(onNext) 콘솔 로그 데모.

Practical

Core Topic 03: 댐과 수문, 배압 메커니즘 (Backpressure)

  • Why to Learn: 엄청난 속도로 쏟아지는 스트림 데이터를 멍청하게 계속 받아먹다가 메모리가 터지는(OOM) 것을 막기 위해, 데이터 흐름의 주도권을 수신자(Subscriber)가 가져오게 만들기 위함입니다.
  • What to Learn:
    • Concepts: Push vs Pull Model, Backpressure (배압), Subscription.request(N), Buffer, Drop / Latest 전략.
    • Skills: OOM(Out of Memory) 방지를 위한 Backpressure 전략(버퍼링 혹은 데이터 버리기) 적용.
  • How to Learn:
    • 1단계: Push 모델의 파국: 소방 호스(Publisher)가 물을 초당 100리터씩 뿜어냅니다. 내가 가진 물통(Subscriber)은 초당 10리터밖에 처리 못 합니다. 물이 다 넘쳐서 시스템 메모리가 폭발(OOM)합니다.
    • 2단계: Pull 모델 (수문 개방): 소방 호스와 물통 사이에 댐(Subscription)을 설치합니다. 물통이 댐에게 "나 지금 비었으니까 10리터만(request(10)) 줘!"라고 요청할 때만 물을 찔끔 흘려보냅니다. 댐 뒤편에 물이 꽉 차면 어쩔 수 없이 오래된 물을 버리거나(Drop), 생산자에게 물을 좀 끄라고(Backpressure) 압력을 가하는 궁극의 생존 기전을 뜯어봅니다.
  • Implement: Backpressure 컨트롤러 로직. Publisher가 초당 100개 이벤트를 생성(Push 시도). Subscriber는 request(5) 메서드를 호출. Publisher는 요청받은 5개만 보내주고 일시 정지(Suspend). Subscriber 처리가 끝나면 다시 request(5)를 호출하여 OOM 없이 데이터를 안벽하게 소비하는 핑퐁(Ping-Pong) 데모.

Advanced

Core Topic 04: 완벽한 톱니바퀴, End-to-End 리액티브 아키텍처

  • Why to Learn: 겉모습만 리액티브(Spring WebFlux)고 속(Database/Network)은 블로킹으로 썩어있는 "Fake Reactive" 아키텍처를 색출해 내고, 처음부터 끝까지 완전한 비동기 스택을 뚫어내기 위함입니다.
  • What to Learn:
    • Concepts: Full Reactive Stack, R2DBC vs JDBC, WebClient vs RestTemplate, Asynchronous Boundary.
    • Skills: 레거시 블로킹(JDBC 등) 구간을 억지로 래핑(Wrapping)할 때 사용할 별도의 격리된 스레드 풀(Thread Pool) 설계.
  • How to Learn:
    • 1단계: 독약(Blocking) 한 방울: NGINX \rightarrow WebFlux \rightarrow JDBC(RDBMS). JDBC는 태생이 블로킹입니다. 아무리 앞단에서 이벤트 루프가 춤을 춰도, DB 쿼리를 날리는 순간 소중한 이벤트 루프 스레드 하나가 멈춰(Block) 버립니다. 독약 한 방울이 전체 시스템을 좀먹는 아키텍처의 비극을 해부합니다.
    • 2단계: R2DBC와 격리 풀: JDBC 대신 논블로킹 DB 드라이버인 R2DBC를 도입하여 End-to-End 비동기를 완성합니다. 만약 무조건 써야 하는 구형 외부 API(블로킹)가 있다면? 이벤트 루프 스레드를 보호하기 위해 해당 API 호출 전용 "격리된 일반 스레드 풀(Elastic Pool)"로 작업을 던져서(Offloading) 리액티브 코어를 보호하는 방어막을 뜯어봅니다.
  • Implement: 블로킹/논블로킹 혼합 스레드 모니터링 시뮬레이션. Event_Loop_Thread(1개)Worker_Thread_Pool(10개). 리액티브 요청은 Event Loop가 처리하다가, 의도적으로 Blocking_Task가 들어왔을 때 이를 Event Loop에서 실행하여 전체 큐가 마비되는 재앙(지연시간 10초 폭발)과, 이를 Worker_Thread로 우회시켜 Event Loop의 1ms 응답성을 방어해 내는 두 가지 시나리오 렌더링.

7. Terminology

Term (EN / ko, abbr) 1문장 정의 단계(기본/권장/실무/심화) 역할/맥락 관련 개념 유사/대비/함께 사용 오해 포인트 Evidence(Primary/Secondary/Industry) Flags(core)
Asynchronous Non-blocking 스레드가 특정 작업(DB 조회 등)을 요청한 뒤 응답을 기다리며 멈춰(Block) 있지 않고, 바로 다른 유저의 요청을 처리하러 떠나는 극한의 스레드 최적화 기법입니다. 기본 이벤트 루프 기반 Event Loop / Callback Synchronous Blocking 처리 속도 자체가 빨라지는 게 아니라, 동일한 하드웨어 자원으로 더 많은 '동시 접속자'를 감당(Throughput)할 수 있게 됨 P1:CS2023 core
Reactive Streams 데이터가 1개일지 무한개일지 모르는 비동기 스트림을, 함수형 파이프라인(Map, Filter)을 통해 조립하고 연결하여 콜백 지옥 없이 우아하게 처리하는 표준 스펙입니다. 권장 비동기 파이프라인 설계 RxJava / WebFlux Imperative(명령형) 프로그래밍 코드를 '선언(Declare)'하는 순간에는 아무 일도 안 일어나며, 누군가 Subscribe를 해야 비로소 데이터가 흐름 P5:SFIA core
Backpressure (배압) 발행자(Publisher)가 데이터를 쏟아내는 속도보다 구독자(Subscriber)의 처리 속도가 느릴 때, "내가 처리할 수 있는 만큼만 보내(Pull)!"라고 역으로 목을 조르는 메모리(OOM) 방어 기제입니다. 실무 OOM(메모리 초과) 방어 request(n) / Pull Model Push Model 단순히 에러를 던지는 게 아니라, 데이터의 흐름 자체를 물리적으로 제어하여 시스템의 파국을 막음 Industry core
Fake Reactive 프레임워크(WebFlux)는 리액티브를 썼지만, 그 내부 로직 어딘가에 JDBC나 Thread.sleep 같은 구식 블로킹 코드가 섞여 있어 결국 이벤트 루프 스레드가 멈춰버리는 끔찍한 안티 패턴입니다. 심화 아키텍처 병목 R2DBC (논블로킹 DB) End-to-End Reactive 리액티브 코어 스레드(보통 4개) 중 하나라도 Block 되면 시스템 전체 성능의 25%25\%가 증발함 Industry core

8. References

Primary

  • [P1] CS2023 - Parallel and Distributed Computing (PDC) - Asynchronous and Reactive Programming
  • [P5] SFIA - Programming/Software Development (PROG) - Reactive Systems

Secondary

  • [The Reactive Manifesto] - Reactive principles (Responsive, Resilient, Elastic, Message Driven)
  • [Reactive Design Patterns] Roland Kuhn - Flow Control and Backpressure

Industry

  • [Spring.io] - Reactive Programming with Spring WebFlux
  • [Project Reactor Reference Guide] - Understanding Backpressure and Schedulers

9. Final Checklist

Primary

  • 전통적인 톰캣(Tomcat)의 Thread-per-request 모델이 트래픽 폭주 시 어떻게 스레드 컨텍스트 스위칭 오버헤드로 인해 CPU 자원을 낭비하고 뻗어버리는지 물리적으로 증명할 수 있는가?
  • NodeJS나 WebFlux의 이벤트 루프(Event Loop) 모델이 소수의 스레드만으로 수만 개의 연결(Connection)을 어떻게 논블로킹(Non-blocking)으로 쳐내는지 비동기 원리를 설명할 수 있는가?

Secondary

  • 리액티브 스트림즈(Reactive Streams) API의 Publisher, Subscriber, Subscription 3자 관계에서, 구독자가 Subscription.request(n)을 호출할 때만 데이터가 흐르는 지연 실행(Lazy Evaluation) 메커니즘을 해부할 수 있는가?
  • 데이터 발생 속도(Push)가 처리 속도를 압도할 때 발생하는 OutOfMemory(OOM) 에러를, 배압(Backpressure)의 Drop, Latest, Buffer 전략을 통해 어떻게 타협하고 희생(Trade-off)시킬지 저울질할 수 있는가?

Industry

  • 기존 Spring MVC(블로킹) 기반의 코드를 Spring WebFlux(리액티브)로 전환할 때, RDBMS(JDBC)를 비동기 R2DBC로 교체해야만 End-to-End 논블로킹이 완성되는 'Fake Reactive'의 맹점을 아키텍처 관점으로 튜닝할 수 있는가?
  • 어쩔 수 없이 블로킹 외부 API를 호출해야 하는 구간이 생겼을 때, subscribeOn(Schedulers.boundedElastic()) 등을 통해 리액티브 메인 이벤트 루프 스레드를 보호(Offloading)하는 방어막을 설계할 수 있는가?

System Architecture · Event-Driven & CQRS

3 / 7