본문 바로가기

Spring Framework/Spring WebFlux

[Spring WebFlux] 1편: 리액티브 프로그래밍이란 무엇인가

1. 들어가며

Spring으로 백엔드를 개발하다 보면 대부분 Spring MVC로 시작합니다. 그러다 어느 시점엔가 Spring WebFlux라는 이름을 접하게 됩니다. 계기는 보통 하나로 수렴합니다. MVC 기반 서비스가 트래픽 압박을 받을 때, 서버 인스턴스를 늘려도 CPU 사용률은 낮은 채로 스레드만 대기 상태에 걸려 있는 상황을 마주하게 되는 것입니다. 요청 하나에 스레드 하나가 묶이는 서블릿 모델의 구조적 한계이고, WebFlux는 바로 이 지점을 겨냥한 대안으로 등장합니다.

 

그런데 막상 WebFlux 코드를 처음 마주하면 또 다른 벽에 부딪힙니다. return userRepository.findById(id) 한 줄이면 끝나던 MVC 코드가, WebFlux에서는 Monomap, flatMap 같은 낯선 연산자 체인으로 바뀌어 있습니다. 단순히 블로킹을 논블로킹으로 바꾼 것뿐이라고 하기엔 코드 스타일 자체가 완전히 다릅니다. 이 달라진 스타일의 정체가 바로 리액티브 프로그래밍이고, 이번 글에서는 이것이 정확히 무엇을 의미하는지, 왜 선언형 패러다임으로 분류되는지, 그리고 Spring 생태계에서 어떤 형태로 구현되어 있는지를 차례로 짚어보겠습니다.

 

2. 리액티브 프로그래밍의 정의

리액티브 프로그래밍은 데이터 스트림(data stream)변화의 전파(propagation of change)를 중심으로 프로그램을 구성하는, 선언형(Declarative) 패러다임입니다. 선언형이란 "어떻게(How)" 할지를 절차대로 지시하는 대신, "무엇을(What)" 원하는지만 기술하고 실행 방법은 프레임워크에 위임하는 방식을 말합니다. Project Reactor 공식 문서에서도 리액티브 프로그래밍을 데이터 스트림과 변화의 전파에 관심을 두는 비동기(asynchronous) 프로그래밍 패러다임이라고 정의합니다.

 

이 정의에서 핵심은 두 가지입니다.

첫째, 모든 것을 시간에 따라 흘러가는 데이터의 흐름으로 취급합니다. 기존 컬렉션이 "이미 메모리에 존재하는 데이터 묶음"이라면, 리액티브 스트림은 "지금은 없지만 앞으로 도착할 데이터의 흐름"입니다.

 

둘째, 값이 변경되면 그 변화가 자동으로 전파됩니다. 명령형 코드에서는 a = b + c라고 쓰면 그 시점의 b, c 값으로 a가 딱 한 번 계산되고 끝입니다. 이후 b가 바뀌어도 a는 갱신되지 않습니다. 반면 리액티브 관점에서는 bc가 바뀌면 a도 그 변화를 따라 다시 계산되는 것을 기대합니다. 스프레드시트에서 셀 값이 바뀌면 그 셀을 참조하는 다른 셀들이 자동으로 재계산되는 것과 같은 그림입니다.

 

3. 리액티브는 왜 선언형인가

리액티브 코드 한 조각을 직접 보면서 얘기하겠습니다.

userRepository.findAll()
    .doOnNext(this::process)
    .subscribe();

 

여기서 "데이터가 도착하면 스레드를 어떻게 관리할지, 언제 콜백을 실행할지"는 전혀 명시하지 않습니다. 그저 "데이터 스트림에 어떤 연산을 적용할지"만 선언하고, 실제 스케줄링과 비동기 처리는 Reactor 같은 구현체가 담당합니다.

이는 Java Stream API와 스타일이 매우 닮아 있습니다. 실제로 Reactor의 Mono/Flux API는 Stream API의 map, filter, flatMap 같은 연산자 체이닝 방식을 그대로 가져왔습니다. 다만 결정적인 차이가 있습니다.

구분 Stream API Reactor (Mono/Flux)
데이터 처리 시점 즉시(Eager) — 스트림을 만드는 시점에 이미 데이터가 존재 구독 시점(Lazy) — subscribe()가 호출되기 전까지는 아무 일도 일어나지 않음
실행 횟수 한 번 순회하면 스트림이 소모되어 재사용 불가 구독할 때마다 새로 실행 가능
시간 개념 없음 — 이미 존재하는 컬렉션을 다룸 있음 — 시간에 따라 도착하는 비동기 데이터를 다룸
비동기 처리 기본적으로 동기 스케줄러를 통한 비동기 처리가 핵심

 

Stream API가 "이미 있는 데이터를 선언적으로 가공하는 도구"라면, 리액티브 스트림은 여기에 "시간"과 "비동기"라는 축을 더한 것이라고 볼 수 있습니다.

 

비동기·논블로킹 로직을 명령형 콜백으로 작성하면 콜백이 콜백을 부르는 중첩 구조, 이른바 콜백 지옥에 빠지기 쉽습니다. 선언형 연산자 체이닝은 "이 데이터가 오면 → 이렇게 변환하고 → 이런 경우 에러 처리하고 → 이렇게 합쳐라"는 흐름을 파이프라인처럼 표현할 수 있어서, 복잡한 비동기 로직의 가독성과 조합성(composability)을 크게 높여줍니다. 리액티브가 선언형 스타일을 택한 것은 우연이 아니라, 비동기 처리라는 문제 자체와 잘 맞아떨어지기 때문입니다.

 

여기서 2번에서 말한 "변화의 전파"가 코드로 어떻게 구현되는지도 드러납니다. doOnNext(this::process)는 그 자체로 실행되는 코드가 아니라, "새 데이터가 도착하면 이 동작을 실행하라"는 등록입니다. 실제로 데이터가 도착하는 시점, 즉 변화가 발생하는 시점에 맞춰 process가 실행됩니다. 연산자 체인 전체가 "값이 바뀌면 그 변화를 따라 자동으로 실행되는 파이프라인"인 셈이고, 이것이 스프레드시트의 셀 재계산과 같은 원리로 코드에 녹아든 형태입니다.

 

4. 이벤트 루프는 실제로 어떻게 동작하는가

1번에서 스레드-per-request 모델의 한계를 짚었으니, 이제 리액티브 쪽에서는 이 문제를 구체적으로 어떻게 우회하는지 살펴보겠습니다.

 

MVC 방식에서 스레드 A가 DB 조회를 요청하면, 결과가 돌아올 때까지 스레드 A는 그 자리에서 대기합니다. 다른 요청이 들어와도 스레드 A는 쓸 수 없으니, 새 스레드 B를 배정해야 합니다.

 

WebFlux 방식에서는 흐름이 다릅니다.

  1. 스레드(이벤트 루프)가 DB 조회 요청을 보냅니다.
  2. 결과를 기다리지 않고, "결과가 오면 이 콜백을 실행해달라"는 등록만 남긴 뒤 스레드를 즉시 반납합니다.
  3. 반납된 스레드는 그사이 다른 요청을 처리합니다.
  4. DB 조회 결과가 도착하면, 이벤트 루프가 등록된 콜백(연산자 체인)을 꺼내 실행합니다.

 

핵심은 "대기"와 "점유"를 분리했다는 점입니다. 요청 하나가 완료되기까지 걸리는 시간(응답 지연)은 그대로지만, 그 시간 동안 스레드를 붙들고 있지 않기 때문에 소수의 스레드만으로도 훨씬 많은 요청을 동시에 처리할 수 있습니다. 흔히 소수의 이벤트 루프 스레드(예: CPU 코어 수에 맞춘 개수)로 수천 개의 동시 요청을 처리한다고 설명되는 것이 이 구조 덕분입니다.

 

지금까지 본 doOnNextsubscribe() 같은 연산자들은 사실 PublisherSubscriber라는 인터페이스 위에서 동작합니다. 이 "콜백 등록"이라는 개념을 표준화한 것이 바로 이 인터페이스들이고, 정확히 어떤 규칙으로 동작하는지는 2편에서 다룹니다.

 

5. Spring 생태계에서의 위치

지금까지 본 Mono, Flux, subscribe() 같은 이름들은 전부 하나의 뿌리에서 나옵니다. 가장 아래에 Reactive Streams라는 표준 스펙이 있고, Project Reactor가 이 스펙을 구현한 라이브러리로서 Spring이 채택한 도구이며, Spring WebFlux는 Reactor를 기반으로 지어진 논블로킹 웹 프레임워크입니다. 즉 스펙이 "규칙"을, Reactor가 그 규칙을 구현한 "도구"를, WebFlux가 그 도구로 지은 "웹 프레임워크"를 맡고 있는 계층 구조입니다.

 

이 규칙이 정확히 무엇을 정의하는지 — PublisherSubscriber가 어떤 계약을 맺고, 배압(Backpressure)은 어떻게 작동하는지 — 는 2편에서 Reactive Streams 스펙을 직접 파헤치며 다룹니다.

 

6. 트레이드오프

리액티브가 항상 정답은 아닙니다. 도입 전에 고려해야 할 현실적인 제약들이 있습니다.

  • 디버깅 난이도: 비동기 체인 때문에 스택 트레이스가 복잡해집니다.
  • 학습 곡선: 연산자 체이닝, 콜드/핫 스트림 같은 새 개념을 익혀야 합니다.
  • 생태계 성숙도: JDBC처럼 태생적으로 블로킹인 라이브러리는 R2DBC 같은 별도의 논블로킹 드라이버로 바꿔야 합니다.
  • 워크로드 성격: CPU 바운드 작업이 대부분인 서비스라면 리액티브가 주는 이점이 크지 않을 수 있습니다.

 

7. 마무리 및 다음 편 예고

이번 글에서는 리액티브 프로그래밍을 선언형 패러다임의 관점에서 정의하고, 이벤트 루프가 스레드를 어떻게 아껴 쓰는지, Spring 생태계에서 이 개념이 어떤 계층 구조로 구현되어 있는지를 살펴봤습니다.

 

다음 편에서는 이 모든 것의 근간이 되는 Reactive Streams 스펙을 직접 파헤쳐 보겠습니다.

https://dmoritle.tistory.com/283

 

[Spring WebFlux] Reactive Streams 스펙 - 계약과 용어

1. 들어가며배압(Backpressure)이라는 개념이 없던 시절, 비동기 스트림 처리 라이브러리들은 저마다 다른 방식으로 동작했습니다. 넷플릭스가 만든 초기 RxJava, 라이트벤드의 Akka Streams 등이 각자의

dmoritle.tistory.com

 

 

Publisher, Subscriber, Subscription, Processor 네 개의 인터페이스가 정확히 어떤 계약(Contract)을 맺고 있는지, 그리고 Signal·Demand·Emit·Sequence 같은 용어들이 실제로 무엇을 가리키는지 코드와 함께 정리합니다.