리액티브 스트림즈 1편 - 리액티브란 무엇인가, 옵저버 패턴에서 백프레셔로
요즘 스터디로 실전! 스프링 5를 활용한 리액티브 프로그래밍(원서 Hands-On Reactive Programming in Spring 5)을 보고 있습니다. 개념이 만만치 않아서, 정리할 겸 시리즈로 남겨보려 합니다. 첫 편은 코드보다 “리액티브가 대체 무엇이고, 그 핵심이 뭔가”부터 짚고 가보려고 합니다.
리액티브 프로그래밍이란
리액티브 프로그래밍이라는 말, 저만 어렵게 느껴지는 건지 모르겠지만, 솔직히 원론적인 의미 이상으로 받아들이기는 어려웠습니다. 😂 리액티브를 이해하기 전에, 먼저 기존의 프로그래밍 방식부터 보겠습니다. 우리가 보통 짜는 코드는 “지금 이 값을 줘”라고 필요할 때 당겨오는(pull) 명령형 프로그래밍이라고 표현합니다. 하지만 리액티브 프로그래밍은 방향이 정반대입니다. “값이 바뀌거나 흘러오면, 그때 이렇게 반응(react)해줘”를 미리 엮어두는 방식이라고 정리할 수 있을 것 같습니다.
비유하면 엑셀, 스프레드시트가 떠오릅니다. A1 셀에 =B1+C1이라고 적어두면, 이후 B1이 바뀔 때마다 A1이 알아서 다시 계산됩니다. 내가 매번 “지금 더해줘”라고 부르지 않아도, 값의 변화가 흘러 내려가는 거죠. 이렇게 데이터의 흐름(stream)과 그 변화를 선언적으로 먼저 다뤄보는 게 리액티브의 감각을 깨우칠 수 있는 가장 첫 번째 걸음마일 것 같습니다! 😺
리액티브의 조상, 아니 화석 그 자체인 옵저버 패턴
그런데 리액티브의 발상이 완전히 새로운 방식은 아닙니다. 과거 객체지향에서부터 오래 써온 옵저버(Observer) 패턴이 리액티브의 친숙한 조상이거든요. 관찰 대상(Subject)의 상태가 바뀌면, 거기 등록해 둔 구독자(Observer)들에게 변화를 알려주는 구조입니다.

자바에서도 리스너, 이벤트 핸들러처럼 같은 모양을 늘 써왔습니다. 그런데 이 패턴만으로 충분했을까요? 옵저버 패턴엔 해결할 수 없는 빈 틈이 있었습니다.
- “이제 끝났다”(완료)나 “에러가 났다”라는 신호를 명확히 전달할 수 있는 표준이 없습니다.
- 받는 쪽(구독자)가 느려도 그냥 계속 이벤트는 무한정 push 됩니다. 받는 속도를 조절하는 장치가 없습니다.
- 여러 흐름을 변환하고 합치는(
map·filter·merge) 합성적인 흐름의 표준이 없습니다.
리액티브 스트림즈는 바로 이 옵저버 패턴에 완료·에러 신호와 받는 속도 조절, 합성을 더해 표준으로 다듬은 것이라고 보는 게 더 자연스러울 것 같습니다. 실제로 RxJava를 두고 “제대로 만든 옵저버 패턴”이라 부르기도 하는데, 그게 바로 그 이유입니다.
리액티브는 비동기? 논블로킹?
흔히 “리액티브 = 논블로킹”이라고 합니다. 물론 그 말도 완전히 틀린 말은 아니지만, 명세를 들여다보면 정체성이 좀 다릅니다. 리액티브 스트림즈는 스스로를 “비동기 스트림 처리와 논블로킹 백프레셔(backpressure)를 위한 표준”이라고 정의합니다.
그러니까 근본은 백프레셔, 즉 흐름 제어입니다. 빠른 생산자가 느린 소비자를 덮치지 않도록, 소비자가 “나는 이만큼만 받을 수 있어”라고 받을 양을 정하는 것. 이게 리액티브 스트림즈를 그냥 “비동기”와 구분 짓는 진짜 정체성입니다.
그럼 “논블로킹”은 뭘까요? 그 백프레셔(받을 양을 알리는 신호)가 블로킹 없이 이뤄져야 한다는 의미이지, “리액티브 = 논블로킹 I/O”라는 뜻은 아닙니다. 실제로 리액티브 스트림즈 파이프라인은 단일 스레드에서 동기적으로도 돌 수 있습니다. 받을 양을 소비자가 정한다는 것, 그게 핵심입니다.
그래서 리액티브 스트림즈란
여기까지가 “리액티브”라는 큰 그림이었다면, 이 시리즈의 제목이기도 한 리액티브 스트림즈(Reactive Streams)는 그 그림을 코드가 합의할 수 있게 못 박은 하나의 명세이자 약속이라고 볼 수 있습니다. 다시 한 줄로 정의하자면 비동기 스트림을 백프레셔와 함께 주고받는 방식을 표준 인터페이스로 정한 약속입니다.
왜 굳이 표준이 필요했을까요? 리액티브가 인기를 끌면서 RxJava, Reactor 같은 라이브러리가 각자의 방식으로 비동기 스트림을 다뤘는데, 그 기술들의 핵심 역할은 같지만, 각자의 명세가 다르다는 게 문제였습니다. 그래서 “비동기 스트림을 주고받는 최소 공통 약속”을 정한 게 리액티브 스트림즈 명세(1.0, 2015년)입니다. 워낙 기본적인 명세가 되는 것이라 나중엔 JDK 9의 java.util.concurrent.Flow로 표준 라이브러리에까지 들어왔습니다.
이 약속은 인터페이스 딱 네 개(Publisher·Subscriber·Subscription·Processor)로 이뤄지는데, 그 한가운데에 방금 본 백프레셔가 있습니다. 구체적인 인터페이스와 동작은 이후 2편에서 하나씩 뜯어보겠습니다.
리액티브 선언문
마지막으로 시스템 차원의 큰 그림도 한 장으로 정리된 게 있습니다. 리액티브 선언문(Reactive Manifesto, 2014년에 정리됨)인데, 총 네 가지 성질에 대해 이야기합니다.

- 응답성(Responsive): 가능한 한 빨리, 그리고 무엇보다 일정하게 응답한다. 빠를 때와 느릴 때가 들쭉날쭉하지 않고 응답 시간이 예측 가능한 게 핵심입니다. 사용자가 체감하는 최종 목표이자, 나머지 셋이 떠받쳐 만들어내는 결과이기도 합니다.
- 회복성(Resilient): 일부가 실패해도 전체가 무너지지 않고 응답을 유지한다. 한 컴포넌트의 장애가 옆으로 번지지 않게 격리하고(isolation), 복구는 다른 컴포넌트에 맡겨 회복하는 식입니다.
- 탄력성(Elastic): 부하가 늘고 줄어듦에 따라 자원을 늘리거나 줄여 대응한다. 특정 지점에 병목이 없어야 가능한데, 그래서 상태를 한곳에 묶지 않고 나눌 수 있는 설계가 전제됩니다.
- 메시지 기반(Message Driven): 컴포넌트들을 비동기 메시지로 느슨하게 연결한다. 위 셋을 떠받치는 토대입니다. 비동기 메시지 전달이 위치 투명성(어디 있든 메시지로 통신)과 격리, 그리고 부하 관리를 가능하게 하는데, 바로 이 부하 관리 자리에서 우리가 본 백프레셔가 등장합니다.
정리하면 메시지 기반이라는 토대 위에서 탄력성·회복성을 얻고, 그 결과가 사용자에겐 응답성으로 나타나는 구조입니다. 흥미롭게도 선언문조차 백프레셔를 “부하를 감당하는 수단”으로 콕 집어 언급합니다.
참고
- 김중철 역, 실전! 스프링 5를 활용한 리액티브 프로그래밍 (원서: Oleh Dokuka·Igor Lozynskyi, Hands-On Reactive Programming in Spring 5)
- Reactive Streams · The Reactive Manifesto
- 토비의 봄 TV: 스프링 리액티브 프로그래밍 (1) Reactive Streams (리액티브 스트림즈 개념을 라이브 코딩으로 풀어주는 입문 영상)
시리즈 리액티브 스트림즈 전 9편
댓글
GitHub(giscus) 댓글은 설정 완료 후 활성화됩니다.