리액티브 스트림즈 1편 - 리액티브란 무엇인가, 옵저버 패턴에서 백프레셔로

2022-04-10 · 엔지니어링 · 시리즈 · 리액티브 스트림즈
리액티브 스트림즈 1편 - 리액티브란 무엇인가, 옵저버 패턴에서 백프레셔로

요즘 스터디로 실전! 스프링 5를 활용한 리액티브 프로그래밍(원서 Hands-On Reactive Programming in Spring 5)을 보고 있습니다. 개념이 만만치 않아서, 정리할 겸 시리즈로 남겨보려 합니다. 첫 편은 코드보다 “리액티브가 대체 무엇이고, 그 핵심이 뭔가”부터 짚고 가보려고 합니다.

리액티브 프로그래밍이란

리액티브 프로그래밍이라는 말, 저만 어렵게 느껴지는 건지 모르겠지만, 솔직히 원론적인 의미 이상으로 받아들이기는 어려웠습니다. 😂 리액티브를 이해하기 전에, 먼저 기존의 프로그래밍 방식부터 보겠습니다. 우리가 보통 짜는 코드는 “지금 이 값을 줘”라고 필요할 때 당겨오는(pull) 명령형 프로그래밍이라고 표현합니다. 하지만 리액티브 프로그래밍은 방향이 정반대입니다. “값이 바뀌거나 흘러오면, 그때 이렇게 반응(react)해줘”를 미리 엮어두는 방식이라고 정리할 수 있을 것 같습니다.

비유하면 엑셀, 스프레드시트가 떠오릅니다. A1 셀에 =B1+C1이라고 적어두면, 이후 B1이 바뀔 때마다 A1이 알아서 다시 계산됩니다. 내가 매번 “지금 더해줘”라고 부르지 않아도, 값의 변화가 흘러 내려가는 거죠. 이렇게 데이터의 흐름(stream)과 그 변화를 선언적으로 먼저 다뤄보는 게 리액티브의 감각을 깨우칠 수 있는 가장 첫 번째 걸음마일 것 같습니다! 😺

리액티브의 조상, 아니 화석 그 자체인 옵저버 패턴

그런데 리액티브의 발상이 완전히 새로운 방식은 아닙니다. 과거 객체지향에서부터 오래 써온 옵저버(Observer) 패턴이 리액티브의 친숙한 조상이거든요. 관찰 대상(Subject)의 상태가 바뀌면, 거기 등록해 둔 구독자(Observer)들에게 변화를 알려주는 구조입니다.

옵저버 패턴: Subject가 상태 변화를 등록된 Observer들에게 통지(push)하는 구조와, 완료·에러 신호와 백프레셔가 없다는 한계

자바에서도 리스너, 이벤트 핸들러처럼 같은 모양을 늘 써왔습니다. 그런데 이 패턴만으로 충분했을까요? 옵저버 패턴엔 해결할 수 없는 빈 틈이 있었습니다.

  • “이제 끝났다”(완료)나 “에러가 났다”라는 신호를 명확히 전달할 수 있는 표준이 없습니다.
  • 받는 쪽(구독자)가 느려도 그냥 계속 이벤트는 무한정 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): 컴포넌트들을 비동기 메시지로 느슨하게 연결한다. 위 셋을 떠받치는 토대입니다. 비동기 메시지 전달이 위치 투명성(어디 있든 메시지로 통신)과 격리, 그리고 부하 관리를 가능하게 하는데, 바로 이 부하 관리 자리에서 우리가 본 백프레셔가 등장합니다.

정리하면 메시지 기반이라는 토대 위에서 탄력성·회복성을 얻고, 그 결과가 사용자에겐 응답성으로 나타나는 구조입니다. 흥미롭게도 선언문조차 백프레셔를 “부하를 감당하는 수단”으로 콕 집어 언급합니다.

참고

시리즈 리액티브 스트림즈 전 9편

  1. 01 리액티브란 무엇인가, 옵저버 패턴에서 백프레셔로 현재 글
  2. 02 명세와 백프레셔, 네 개의 인터페이스 다음
  3. 03 직접 만들어보기, pull에서 push까지
  4. 04 백프레셔, 흐름을 누가 통제하는가
  5. 05 구현체들 둘러보기
  6. 06 Project Reactor 입문, Flux와 Mono
  7. 07 Spring WebFlux와 논블로킹 스택
  8. 08 끝까지 논블로킹, 그리고 한계
  9. 09 가상 스레드와 리액티브의 미래

댓글

GitHub(giscus) 댓글은 설정 완료 후 활성화됩니다.