LESPINSIDE

리액티브 프로그래밍 대 리액티브 시스템

February 05, 2017 | 21 Minute Read

본 글은 Lightbend사의 Jonas Bonér와 Viktor Klang가 작성한 Reactive Programming versus Reactive Systems - Landing on a set of simple Reactive design principles in a sea of constant confusion and overloaded expectations을 번역한 것입니다.

요약

2013년 리액티브 선언문을 공동 저작할 때부터 지금까지 우리는 리액티브라는 주제가 소수의 회사에서 프린지(fringe) 프로젝트 수행에만 사용하는 사실상 애플리케이션을 제작하는 기술로 인정받지 못하던 것에서부터 미들웨어 분야의 수많은 영향력 있는 회사들이 사용하는 전반적인 플랫폼 전략의 일부가 되는 것을 목격했다.

이 글에서는 리액티브 프로그래밍 스타일로 코드를 작성하는 것과 전체적으로 응집력(cohesive) 있는 리액티브 시스템을 설계하는 것의 차이를 살펴본다. 이를 통해 “리액티브”의 다양한 측면을 정의하고 명확히 하는 것이 목표이다.

(바쁜 사람들을 위한) 핵심 내용

  • 2015년부터 특히 2016년에 상용 미들웨어 벤더사와 사용자 모두 리액티브에 대한 관심이 급격히 증가했다.
  • 구현 관점에서 리액티브 프로그래밍은 리액티브 시스템의 일부이다.
  • 리액티브 프로그래밍은 컴포넌트 수준에서 내부 로직과 데이터 플로우(flow) 관리를 위한 성능과 자원 효율성을 통해 개발자의 생산성을 높여준다.
  • 리액티브 시스템은 시스템 수준에서 “클라우드 네이티브”[1] 혹은 다른 대규모 분산 시스템을 구축하기 위한 복원성과 탄력성을 통해 아키텍트와 데브옵스의 생산성을 높여준다.
  • 리액티브 시스템의 컴포넌트 안에서 리액티브 프로그래밍을 사용하는 것은 매우 유용하다.
  • 리액티브 프로그래밍을 사용하여 작성한 컴포넌트들로 시스템을 만들 때 리액티브 시스템을 사용하는 것은 매우 유용하다.

리액티브 - 설계 원칙들의 집합

최근의 성공 지표 중 하나는 리액티브라는 용어가 과도하게 사용되어 “스트리밍”, “가벼운”, “실시간” 등과 관련되어 다양한 상황에서 여러 가지 의미로 사용된다는 것이다.

이 문서에서 말하는 “리액티브”는 응집력 있는 시스템을 만들어내는 설계 원칙들의 집합이다. 이것은 구현 기술, 툴링(tooling) 및 디자인 패턴들이 컴포넌트가 되는 분산 환경에서 시스템 구조와 설계에 대해서 생각하는 관점이다.

다음과 같은 비유를 생각해보자. 스포츠 팀(축구, 야구 등)은 뛰어난 개인들로 구성되는 경우가 많다. 그런데도 팀 내에서 의사소통이 제대로 되지 않을 때 “열등한” 팀에게 지는 경우가 비일비재하다. 이처럼 시너지 효과가 부족할 경우 효과적으로 팀을 운용할 수 없다.

이것은 개별적으로는 훌륭한 리액티브 서비스들을 생각 없이 어설프게 합친 것과 리액티브 시스템의 차이와 같다.

리액티브 시스템에서 모든 차이를 만드는 것은 개별적인 컴포넌트 간의 상호 작용이다. 이들은 개별적으로 동작하지만, 함께 목적하는 바를 성취한다.

리액티브 시스템은 이러한 개별적인 서비스들이 서로를 인지하면서 하나로 합쳐져 외부에 반응하는 구조적 스타일에 기반을 둔다. 이것은 스케일을 키우거나(up) 줄일(down) 수 있고 부하를 분산할 수 있게하며 이러한 단계 중 일부를 미리 수행할 수도 있다.

사람들이 소프트웨어 개발과 설계의 관점에서 이야기하는 리액티브는 일반적으로 아래 세 가지 중 하나이다.

  • 리액티브 시스템 (구조와 설계)
  • 리액티브 프로그래밍 (선언적 이벤트 기반)
  • 함수형(Functional) 리액티브 프로그래밍 (FRP)

우리는 처음 두 개에 대하여 중점을 두고 이 구현과 기술이 뜻하는 의미를 살펴볼 것이다. 특히 언제 그것들을 사용할 것인지, 어떻게 서로 연관되는지, 각각에서 어떠한 이점을 기대할 수 있는지 논의할 것이다. 특히나 멀티코어, 클라우드, 모바일 구조에서 시스템을 구현하는 관점에서 이야기할 것이다.

2013년, Akka 기반의 시스템을 만들고 유지보수, 운영하면서 동시성과 분산 문제를 해결하기 위한 전통적인 방식에 비해 큰 이점을 목격했다. 그리고 그 긴 경험 끝에 그간 경험들과 교훈들이 리액티브 선언문에서 빛을 발했다.

현대 시스템을 이끄는 것은 응답성(Responsiveness)이다. 클라이언트/고객은 제때에 가치를 얻지 못하면 다른 곳으로 가버린다는 것을 알아야 한다. 근본적으로 가치를 얻지 못하는 것과 필요할 때 가치를 얻지 못하는 것은 다를 게 없다.

응답성을 촉진하는 것에는 두 가지 과제가 있다. 장애의 상황에서도 응답해야 한다는 복원성(Resilience)과 부하의 상황에서도 응답해야 한다는 탄력성(Elasticity)이다. 리액티브 선언문은 이것을 성취하기 위해서 시스템이 메시지 기반(Message Driven)이어야 한다고 규정하고 있다.

리액티브 선언문의 네가지 신조 리액티브 선언문의 네가지 신조

2016년 JVM 분야의 몇몇 주요 벤더사들은 리액티브 프로그래밍을 수용하기 위한 핵심 계획을 발표했다. 이것은 오늘날 회사들이 겪는 문제들이 무엇인지 말해주는 것이다.

전통적인 프로그래밍 기법에서 이러한 변화를 시도하는 것은 도전적인 일이다. 기존에 사용하던 기술들에 대한 호환성을 유지하고 사용자의 기반을 다른 사고방식으로 유도해야 한다. 동시에 내부 개발자 양성과 운영 경험을 구축해야 한다. 이러한 회사들의 투자는 결코 쉬운 일이 아니며 큰 공학적 도전이라는 것은 말할 필요도 없다.

리액티브 프로그래밍 분야에서 다양한 활동들이 일어나고 있는 반면에 시스템 구조 측면에서는 구조와 운영 경험이 쌓이기까지 시간이 걸릴 것이다. 이것은 새로운 프로그래밍 패러다임을 적용하는 것만으로 자동으로 해결되는 문제가 아니다. 리액티브 선언문 아래에서 커지는 공감대를 보는 것은 흥미로운 일일 것이다.

이제 함수형 리액티브 프로그래밍에 대해서 이야기해보고 왜 더는 이 글에서 논의하지 않는지 알아보자.

함수형 리액티브 프로그래밍 (FRP)

“FRP”라고 불리는 함수형 리액티브 프로그래밍은 흔히 잘못 이해되고 있다. FRP는 20년 전 Conal Elliott에 의해 매우 정교하게 정의 되었다. 이 용어는 최근 다른것들 사이에서 Elm, Bacon.js, Reactive Extension(RxJava, Rx.NET, RxJS)과 같은 기술을 설명하는데 잘못 사용되고 있다[2]. FRP를 지원한다고 말하는 대부분의 라이브러리들은 리액티브 프로그래밍를 이야기하는 것이다. 그러므로 더는 여기서 이야기하지 않겠다.

리액티브 프로그래밍

리액티브 프로그래밍과 함수형 리액티브 프로그래밍을 혼동해서는 안 된다. 리액티브 프로그래밍은 비동기 프로그래밍의 일부이며 실행 스레드가 제어 흐름을 주도하는 것이 아닌 새로운 정보가 로직을 주도하는 패러다임이다.

이것은 하나의 문제를 각각 비동기와 논 블로킹 방식으로 실행될 수 있는 여러 단계로 분리할 수 있다. 그리고 무한한 입력이나 출력을 생성할 수 있는 작업 흐름(workflow)을 만들기 위해 결합한다.

옥스퍼드 사전은 비동기를 “동시에 존재하거나 발생하지 않는” 이라고 정의한다. 우리의 문맥에서는 클라이언트에서 서비스로 전송된 요청이 이후 임의의 시점에 처리된다는 의미이다.

이것은 논 블로킹을 가능하게 하는 리액티브 프로그래밍에서 굉장히 중요한 기술이다. 실행 스레드들이 공유 자원을 점유하기 위해 경쟁할 때 (현재 수행하는 일이 끝날 때까지 실행 스레드의 실행을 막는) 블로킹으로 기다릴 필요가 없는 것이다. 대신 자원을 점유하기 전까지 다른 유용한 작업을 수행할 수 있다. Amdahl의 법칙[3]에 따르면 확장성의 가장 큰 적은 경쟁이며 그러므로 리액티브 프로그램은 거의 블로킹되지 않아야 한다.

동기, 블로킹 통신(좌)는 자원을 비효율적으로 사용하며 병목현상이 발생하기 쉽다. 리액티브 방식(우)는 위험을 줄이고 값 비싼 자원을 보전하며 하드웨어/인프라에 대한 요구가 덜하다.
동기, 블로킹 통신(좌)는 자원을 비효율적으로 사용하며 병목현상이 발생하기 쉽다. 리액티브 방식(우)는 위험을 줄이고 값 비싼 자원을 보전하며 하드웨어/인프라에 대한 요구가 덜하다.

리액티브 프로그래밍은 일반적으로 이벤트 기반이다. 이것은 리액티브 시스템이 메시지 기반인 것과 대조적이다. 이벤트 기반과 메시지 기반 사이의 구분에 대해서는 다음 부문에 다루고 있다.

리액티브 프로그래밍 라이브러리의 API는 일반적으로 둘 중 하나이다.

  • 콜백 기반 : 사이드 이펙트(side-effect)를 발생시키는 익명 콜백을 이벤트 발생지에 붙여 이벤트가 데이터 흐름 체인을 지나갈 때 호출함
  • 선언적(declarative) 방식 : map, filter, fold와 같이 잘 정립된 결합자(combinator)를 사용하는 함수형 결합을 통해 이루어지는 짐

대부분 라이브러리들은 이 두 가지 스타일을 섞어서 제공한다. 또한 windowing, counts, trigger와 같은 스트림 기반 연산자들을 추가로 제공하기도 한다.

제어의 흐름이 아닌 데이터의 흐름에 중점을 둔다는 것에서 리액티브 프로그래밍이 데이터 흐름 프로그래밍과 관련되어 있다는 주장도 일리가 있다.

리액티브 프로그래밍 기법을 지원하는 프로그래밍 개념의 예제들은 아래와 같다.

  • Future/Promises : 하나의 값에 대한 컨테이너로 어떠한 값을 비동기적으로 변형할 때 그 값이 아직 사용할 수 없는 상황에서도 추가될 수 있다. 컨테이너는 한 번만 쓸(write) 수 있고 여러 번 읽을(read) 수 있다.
  • 리액티브 스트림과 같은 스트림: 무수한 발생지와 목적지 사이에서 데이터를 처리하는 무한한 흐름으로 비동기, 논 블로킹, 역압(back-pressured)의 변형을 수행하는 파이프라인이다.
  • 데이터 흐름 변수(Dataflow Variables) : 입력, 프로시저, 다른 셀에 의존하여 변경사항이 자동으로 업데이트되는 하나의 할당 변수(메모리 셀)이다. 실질적인 예로 스프레드시트가 있다. 스프레드시트는 한 셀의 값이 변하면 모든 종속 함수를 통과하여 새로운 “다운 스트림(downstream)” 값을 생성한다.

JVM에서 리액티브 프로그래밍을 지원하는 유명 라이브러리들로는 Akka Streams, Ratpack, Reactor, RxJava, Vert.x가 있지만 이 라이브러리들이 전부는 아니다. 이 라이브러리들은 리액티브 프로그래밍 표준인 리액티브 스트림 명세를 구현한다. 리액티브 스트림은 JVM에서 사용되는 리액티브 프로그래밍 라이브러리 간의 상호 운용이 가능하도록 하는 표준으로 스스로에 대한 설명에 따르면 이는 “논 블로킹 역압으로 비동기 스트림을 처리하는 표준을 제공하기 위한 계획” 이다.

리액티브 프로그래밍의 이점 (그리고 한계)

리액티브 프로그래밍의 가장 큰 장점은 멀티코어와 멀티CPU 하드웨어에서 연산 자원 활용을 증가할 수 있다는 것과 Amdahl의 법칙과 Günther의 Universal Scalability Law[4]에 따라 직렬화 지점을 감소시켜 성능을 향상할 수 있다는 것이다.

두 번째 이점은 개발자의 생산성이다. 전통적인 개발 패러다임은 비동기와 논 블로킹 연산 및 입출력을 직선적이고 유지보수 가능하게 다루는 데 어려움이 있다. 리액티브 프로그래밍은 대개 동작 중인 컴포넌트들 사이의 명확한 조정(coordination)에 대한 필요성을 제거하여 이러한 문제를 해결한다.

리액티브 프로그래밍이 빛을 발하는 것은 컴포넌트를 생성하고 작업 흐름을 결합하는 시점이다. 비동기 실행의 모든 이점을 가지기 위해서는 역압이 굉장히 중요하다. 역압은 과도한 사용과 무한한 자원 소비를 피할 수 있다.

사진1 사진2 데이터 흐름 측면에서 안정적인 상태를 유지하기 위해서 풀(pull) 방식의 역압은 새로운 요청을 업스트림으로 보내고 메시지 수신을 다운스트림으로 진행한다. 그러므로 생산자가 소비자를 압도하지 않는다. 그림 Kevin Webber(@kvmwbbr)

리액티브 프로그래밍이 현대 소프트웨어를 구축하기 위해 매우 유용하지만 더 높은 수준의 시스템을 생각하기 위해서는 다른 도구를 사용해야 한다. 바로 리액티브 아키텍쳐이다. 리액티브 아키텍쳐는 리액티브 시스템을 설계하는 과정이다. 나아가서 세상에는 많은 프로그래밍 패러디임이 있고 리액티브 프로그래밍은 그중 하나라는 사실을 기억하는 것이 중요하다. 이것은 그저 도구일 뿐이며 모든 경우에 만능은 아니다.

이벤트 기반 대 메시지 기반

이전에 언급한 것처럼 리액티브 프로그래밍은 수명이 짧은 데이터 흐름 체인을 통해 연산하는 데 중점을 둔다. 이것은 주로 이벤트 기반이다. 반면 리액티브 시스템은 (메시징이라고도 하는) 메시지 기반[5]으로 분산 시스템에서 통신과 조정을 통한 복원성과 탄력성에 중점을 둔다.

수명이 긴 주소가 지정가능한 컴포넌트들을 이용하는 메시지 기반과 데이터 흐름이 주도하는 이벤트 기반의 가장 큰 차이점은 메시지는 통제할 수 있지만 이벤트는 그렇지 않다는 것이다. 메시지는 명백한 하나의 목적지를 가지는 반면 이벤트는 다른 관찰자들이 사실(facts)를 관찰(observe)하는 것이다. 나아가서 메시징은 발신자와 수신자가 발신, 수신이라는 행위와 분리되는 비동기 형태가 적합하다.

이벤트 기반이 "발언대"에서 사실(이벤트)을 전파(broadcasting)하고 다른 관찰자들이 (듣고 있다면) 관찰하는 반면, 메시지 기반은 주소로 지정 가능한 수신자와 하나의 목적을 가진다. 이벤트 기반이 “발언대”에서 사실(이벤트)을 전파(broadcasting)하고 다른 관찰자들이 (듣고 있다면) 관찰하는 반면, 메시지 기반은 주소로 지정 가능한 수신자와 하나의 목적을 가진다.

리액티브 선언문의 용어집에는 개념적인 차이를 다음과 같이 정의한다.

메시지는 특정 대상으로 보내지는 데이터 항목이다. 이벤트는 컴포넌트가 주어진 상태에 도달했을 때 발생시키는 신호이다. 메시지 기반 시스템에서는 주소 지정이 가능한 수신자가 메시지 도착을 기다리고 메시지에 응답하며 그렇지 않을 경우 휴면한다. 이벤트 기반 시스템 알림에서 리스너는 이벤트 발생지에 소속되어 이벤트가 발생할 때 호출된다. 즉, 이벤트 기반 시스템은 주소 지정이 가능한 이벤트 발생지에 초점을 맞추고 메시지 기반 시스템은 주소 지정이 가능한 수신자에 집중한다.

메시지는 분산 시스템에서의 통신이나 네트워크 통신을 필요로 할 때 사용하지만 이벤트는 지역적으로 사용한다. 일반적으로 이벤트 기반의 시스템 간에 연결은 메시징을 통해 이루어지는데 메시지 안에 이벤트를 담아 네트워크 통신한다. 이것은 이벤트 기반 프로그래밍 모델의 상대적 간결함을 분산 환경에서도 유지하도록 해준다. 그리고 전문화되고 범위가 잘 지정된 경우에 원활하게 동작한다. (즉 AWS Lambda와 분산 스트림 처리를 하는 Spark Streaming, Flink, Kafka, Akka Streams를 사용하는 Gearpump, 분산 Publish/Subscribe 제품인 Kafka, Kinesis)

하지만 얻는게 있으면 잃는 것도 있다. 이 프로그래밍 모델은 추상화와 간결함을 얻는 반면 제어의 관점에서 잃는 것이 있다.

메시징은 부분적인 장애, 장애 감지, 삭제/중복/재정렬 된 메시지, 이벤트의 견고함, 동시성 관리와 같은 분산형 시스템의 현실과 제약을 포용하도록 강제한다. 그리고 과거에 수없이 많이 행해져 왔던 (EJB,RPC,CORBA,XA 등) 마치 네트워크가 없는 것처럼 어설픈 추상화 뒤에 숨기는 대신 이러한 문제들을 전면에서 다룬다.

의미(semantics)와 응용 가능성(applicability)에서 이러한 차이는 복원성, 탄력성, 이동성, 위치 투명성 및 분산 시스템의 복잡성 관리와 같은 애플리케이션 설계에 중대한 영향을 미친다. 이에 대해서는 뒤에서 더 설명할 것이다.

리액티브 프로그래밍을 사용하는 리액티브 시스템에서는 이벤트와 메시지가 모두 존재한다. 하나는 통신을 위한 훌륭한 도구(메시지)이며 다른 하나는 사실(facts)을 표현하는 훌륭한 방법(이벤트)이다.

리액티브 시스템과 아키텍쳐

리액티브 시스템 은 리액티브 선언문에 정의된 것처럼 오늘날의 애플리케이션에 대한 높은 요구사항을 충족시키는 현대적인 시스템을 만들기 위한 구조적인 설계 원칙의 집합이다.

리액티브 시스템의 원칙은 전혀 새로운 것이 아니다. 70년대와 80년대로 거슬러 올라가면 Jim Gray와 Pat Helland의 Tandem System과 Joe Armstrong과 Robert Virding의 Erlang와 같은 사람들에게 영감을 주는 업적이 있다. 그러나 이 사람들은 시대를 앞서갔다. 상용 시스템 개발에 대한 현재 “모범 사례”에 대해서 다시 생각하기 시작한 것은 최근 5~10년밖에 되지 않았다. 이것은 오늘날의 멀티코어, 클라우드 환경, 사물 인터넷 환경에 리액티브 원칙을 적용하는 법을 익히는 것을 의미한다.

리액티브 시스템의 근간은 메시지 전달이다. 메시지 전달은 컴포넌트 사이에 일시적인 경계를 만드는 것이다. 이것은 컴포넌트 간의 (동시성을 제공하는) 시간 결합도와 (분산과 이동성을 제공하는) 공간 결합도를 낮춘다. 이러한 결합도를 낮추는 일은 컴포넌트 간의 완벽한 분리를 위해 필수적이다. 그리고 이러한 분리는 복원성탄력성 모두의 기초가 된다.

프로그램에서 시스템으로

우리는 시스템을 만드는 것인 만큼 더는 한 개의 연산을 위해 종단 사이(end-to-end) 로직을 수행하는 프로그램을 만들지 않는다.

세상은 점점 더 연결되고 있다. 시스템은 집합체이다. 시스템은 수많은 컴포넌트로 이루어지고 그러한 컴포넌트들 또한 시스템이 될 수 있다. 그것은 소프트웨어가 원활히 동작하기 위해서 다른 소프트웨어에 점점 더 의존하고 있다는 것을 의미한다.

오늘날 우리가 만드는 시스템은 작거나 큰, 적거나 많은, 서로 가깝거나 지구 반대편에 있는 컴퓨터에서 동작한다. 그리고 사람들의 삶이 원활하게 동작하는 시스템에 대한 의존성이 커져 그러한 사용자의 기대를 만족하게 하기 점점 더 힘들어지고 있다.

사용자 혹은 사업이 의존할 수 있는 시스템을 제공하기 위해서 시스템은 응답이 빨라야 한다. 만약 아무리 정확한 응답을 제공하더라도 그 응답이 필요한 시기에 제공되지 않는다면 그것은 아무 의미가 없기 때문이다. 빠른 응답성을 얻기 위해서 응답이 장애의 상황(복원성)에서도 유지되어야 하며 계속해서 변하는 부하(탄력성)에서도 유지되어야 한다. 이것이 가능하기 위해서 시스템은 메시지 기반 이어야 하며 우리는 이것을 리액티브 시스템 이라고 부른다.

리액티브 시스템의 복원성

복원성은 장애가 발생한 상황에서의 응답성 이며 시스템 고유의 기능적 속성이다. 이것은 어느 날 갑자기 적용할 것이 아니라 설계되어야 할 것이다.

복원성은 장애 허용(fault-tolerance)과는 다른 것이다. 이것은 장애 상황에서 우아하게 기능을 낮추는 것(degradation)에 대한 것이 아니며 장애의 상황에서 스스로 완벽히 복구되는 것을 의미한다.

복원성은 컴포넌트의 분리와 장애에 대한 억제가 필요하다. 이를 통해 장애가 주변 컴포넌트로 퍼져나가는 것을 피할 수 있다.

스스로 복구하는 복원성을 가진 시스템의 핵심은 장애가 억제되며, 메시지로 구체화하고, 다른 (감독관(supervisor) 역할을 하는) 컴포넌트들에 전달되고 장애가 발생한 컴포넌트 밖의 안전한 곳에서 관리된다는 것이다. 메시지 기반이 그것을 가능하게 해준다. 모두가 겪거나 혹은 무시했던 강하게 연결된, 부서지기 쉬운, 깊게 중첩된 동기화 호출 체인에서 벗어날 수 있다. 호출 체인에서 장애 관리를 분리하여 서버의 장애 처리를 클라이언트에게 책임지게 하지 않는 것이다.

리액티브 시스템의 탄력성

탄력성 은 부하의 상황에서의 응답성 이다. 시스템의 처리량이 필요에 따라 자동으로 커지거나(scale up) 작아질 수(scale down) 있을 뿐만 아니라 (하나의 머신 안에서 코어를 추가하거나 빼는 것) 양적으로 늘어나거나(scale out) 줄어들 수(scale in) 있다(데이터 센터의 노드나 머신을 추가하거나 빼는 것). 이것은 시스템을 자원, 비용 효율적으로 만들며 환경친화적이고 사용한 만큼 지불하는 클라우드 컴퓨팅의 이점을 취하기 위한 필수 요소이다.

시스템은 적응력이 좋아야 한다. 중재 없는 자동 스케일링, 상태와 행동 복제, 통신의 로드 밸런싱, 장애복구와 업그레이드가 시스템을 다시 프로그래밍하거나 설정하지 않고 가능해야 한다. 이것을 가능하게 하는 것이 위치 투명성 이다. 위치 투명성은 CPU 코어에서부터 데이터 센터에 이르기까지 모든 차원의 스케일링을 같은 방식으로 하는 능력이다.

리액티브 선언문에 적힌 내용 을 보자.

이 문제를 대단히 단순화하는 핵심 통찰력은 우리가 모두 분산 컴퓨팅을 수행하고 있다는 것을 깨닫는 것이다. 단일 노드 (QPI 링크를 통해 통신하는 여러 독립 CPU 포함) 또는 노드 클러스터 (네트워크를 통해 통신하는 독립적인 시스템)에서 시스템을 실행하든 상관없다. 이 사실을 받아들인다는 것은 멀티 코어에서 수직으로 확장하거나 클러스터에서 수평으로 확장하는 것이 개념적으로 차이가 없다는 것을 의미한다. 비동기 메시지 기반(이벤트 기반과 대조)로 활성화된 공간 분리 (분리 (그리고 억제) 정의 참조)와 실행시간 인스턴스의 참조 분리는 우리가 위치 투명성이라고 부르는 것이다.

수신자가 어디에 있던지 우리는 같은 방식으로 통신한다. 이것을 동일하게 수행할 수 있는 것은 메시지를 이용하는 방법밖에 없다.

리액티브 시스템의 생산성

대부분 시스템은 본질적으로 집합체이기 때문에 가장 중요한 점 중 하나가 시스템 구조가 생산성을 저하에 최소한으로 영향을 미쳐야 한다는 것이다. 이것은 컴포넌트를 개발하는 것과 유지 보수하는 시점에 모두 적용되어야 하며 동시에 우발적 복잡성(accidental complexity) 을 최소화해야 한다.

제대로 설계되지 않은 시스템은 라이프 사이클 안에서 유지 보수하기가 더욱 힘들어지고 문제점을 제한하고 수정하기 위해 이해하는 시간과 노력의 양이 증가하기 때문에 중요하다.

리액티브 시스템은 우리가 아는 한 (멀티코어, 클라우드, 모바일 구조에서) 가장 생산적인 시스템 구조이다.

  • 장애를 고립하는 것은 컴포넌트 사이에 격벽을 제공하며, 장애의 범위를 제한하여 장애가 전파되는 것을 막을 수 있다.
  • 감독관(supervisor) 계층 구조는 자가 복구 능력과 함께 여러 수준의 방어 체계를 제공하므로 관찰에 대한 비용을 들이지 않고 수많은 일시적 장애의 상당 부분을 없앨 수 있다.
  • 메시지 전달과 위치 투명성은 최종 사용자에게 영향을 미치지 않고 컴포넌트가 오프라인이되거나 교체 혹은 경로를 변경할 수 있도록 해준다. 이를 통해 중단 비용, 상대적 긴급성, 진단 및 수정에 필요한 자원을 줄일 수 있다.
  • 복제는 데이터 손실 위험을 줄일 수 있고 장애가 정보 검색 및 저장에 미치는 영향을 줄여준다.
  • 탄력성은 사용량의 변화에 따라 자원을 절약할 수 있다. 그러므로 부하가 적을 때 운영 비용을 최소화할 수 있고 부하가 증가할 때 중단되는 상황이나 긴급한 확장에 대한 위험을 최소화할 수 있다.

타이타닉에서 제대로 사용되진 못했지만, 격벽은 조선 사업에서 장애가 전파되어 다른 기능에 영향을 끼치는 것을 막기 위해 오래전부터 사용됐다. 타이타닉에서 제대로 사용되진 못했지만, 격벽은 조선 사업에서 장애가 전파되어 다른 기능에 영향을 끼치는 것을 막기 위해 오래전부터 사용됐다.

그래서 리액티브 시스템은 유지 비용이 적게 들며 언제나 장애, 다양한 부하, 변경사항에 대해서 효과적으로 대처할 수 있는 시스템을 제공한다.

리액티브 프로그래밍과 리액티브 시스템은 어떤 관계가 있는가?

리액티브 프로그래밍은 컴포넌트 안에서 내부 로직과 데이터 흐름의 변경을 관리하기 위한 훌륭한 기법이다. 또한 코드를 명확하게 하고 성능과 자원 효율성을 최적화할 방법이다. 리액티브 시스템은 설계 원칙의 집합으로 분산 통신에 특화되어 있으며 분산 시스템에서 복원력과 탄력성을 유지할 수 있는 도구를 제공한다.

리액티브 프로그래밍에서 흔히 발생하는 문제 중 하나는 이벤트 기반 콜백 혹은 선언적 프로그램에서 연산 단계들이 긴밀하게 연결되어 복원성 을 얻기 힘들게 만든다는 것이다. 이것은 데이터를 변형하는 체인의 수명이 짧고 콜백이나 결합자(combinator)가 익명 즉 주소로 지정할 수 없기 때문이다.

이것은 외부신호 를 보내지 않고 성공이나 실패를 관리한다는 것을 의미한다. 주소가 없으면 예외를 어디로 전파해야 하는지가 불분명해져 개별적인 복구가 어려워진다. 그 결과로 장애는 컴포넌트의 전반적인 상태가 아닌 수명이 짧은 고객의 요청에 묶이게 된다. 만약 데이터 흐름 체인의 한 단계에서 실패하면 전체 체인이 다시 실행돼야 하며 클라이언트에게 영향을 미친다. 이것은 클라이언트에게 알리지 않고 자가 회복이 가능한 리액티브 시스템의 메시지 기반과 반대되는 것이다.

순수한 리액티브 프로그래밍이 리액티브 시스템의 접근 방식과 반대되는 또 다른 하나는 공간 분리가 아닌 시간 분리라는 것이다.(앞 서 이야기한 것처럼 네트워크 통신으로 데이터 흐름을 분산하여 메시지를 전달하지 않는 한에서이다)

시간으로부터 분리되는 것은 동시성을 제공하지만 공간에서 분리되는 것은 정적인 상황뿐만 아니라 동적인 토폴러지에서 분산성과 이동성을 제공한다. 이것은 탄력성을 이루기 위해 필수적이다.

위치 투명성의 부재는 순수하게 리액티브 프로그래밍 기법에 기반을 둔 프로그램을 탄력적으로 스케일 아웃하기 어렵게 만든다. 그러므로 그 위에 Message Bus, Data Grid, bespoke network protocols와 같은 추가적인 계층이 필요하다. 여기서 리액티브 시스템의 메시지 기반 방식이 빛을 발한다. 이것은 프로그래밍 모델과 의미를 모든 차원의 스케일에서 유지할 수 있게 해주는 통신 추상화이기 때문에 시스템 복잡도와 시스템 파악을 위한 오버헤드를 줄일 수 있다.

콜백 기반의 프로그래밍에서 일반적으로 언급되는 문제로 콜백 기반의 프로그램을 작성하기는 비교적 쉽지만 오랫동안 실행해보아야 실질적인 결과를 알 수 있다는 것이다.

예를 들어 익명 콜백을 기반으로 하는 시스템은 그것들에 대해 고민하고 유지하고 무엇보다 어디서 왜 제품이 멈추고 오동작이 발생하는지 알기 힘들다.

(Akka 프로젝트나 Erlang 플랫폼 같은) 리액티브 시스템을 위해 설계된 라이브러리와 플랫폼은 오래전에 이러한 교훈을 얻었고 오랜 시간이 지나도 알기 쉬운 수명이 길고 주소가 지정 가능한 컴포넌트들을 사용하고 있다. 장애가 발생했을 때 장애를 발생시킨 메시지와 함께 컴포넌트를 고유하게 식별할 수 있다. 모니터링 솔루션은 컴포넌트 모델의 핵심인 주소 지정 가능성이라는 개념을 통해 전파되는 ID를 활용하여 수집된 데이터를 제공하는 의미 있는 방법을 제공한다.

주소지정 가능성과 장애 관리와 같은 기능을 수행하는 좋은 프로그래밍 패러다임을 선택하는 것은 운영에 도움이 된다는 것이 증명되었다. 이것은 현실의 가혹함을 염두에 두고 설계되었고 장애를 막으려고 시도하는 과정에서 원인을 찾지 못하는 것보다는 장애를 예상하고 받아들일 수 있도록 한다.

전체적으로 리액티브 프로그래밍은 리액티브 아키택쳐에서 사용될 수 있는 굉장히 유용한 기법이다. 이것은 전체 모든 것 중 오직 비동기와 논 블로킹 실행을 통해 데이터 흐름을 관리하는 한 부분에만 도움이 된다는 것을 기억해야 한다. 이것은 대개 한 노드나 서비스 안에서 이야기이다. 노드가 다수가 될 때부터 데이터 일관성, 노드 간 통신, 조정, 버전 관리, 오케스트레이션, 장애 관리, 관심과 책임의 분리와 같은 시스템 아키텍쳐에 대해 진지하게 생각해봐야 한다.

리액티브 프로그래밍은 한 노드나 서비스 안에서 비동기와 논블로킹 데이터 흐름 관리에 중점을 두는 반면 복잡한 리액티브 시스템 아키텍쳐는 노드와 클러스터에서 다수의 서비스를 성공적으로 배포하는 데 훨씬 더 많은 것을 필요로 한다. 리액티브 프로그래밍은 한 노드나 서비스 안에서 비동기와 논블로킹 데이터 흐름 관리에 중점을 두는 반면 복잡한 리액티브 시스템 아키텍쳐는 노드와 클러스터에서 다수의 서비스를 성공적으로 배포하는 데 훨씬 더 많은 것을 필요로 한다.

그래서 리액티브 프로그래밍의 가치를 최대화하기 위해서는 리액티브 시스템을 만드는 도구의 하나로 사용해야 한다. 리액티브 시스템을 구축하기 위해서는 OS 고유의 자원을 추상화하고 기존에 존재하는 래거시 소프트웨어 스택 위에 약간의 비동기 API와 Circuit Breakers를 구축하는 것 이상이 필요하다. 우리가 구축하는 것은 다수의 서비스를 결합하는 분산형 시스템이라는 사실을 알아야 한다. 예상한 대로 동작하는 것뿐만 아니라 장애와 예상치 못한 부하를 직면했을 때도 일관적이고 빠르게 응답하는 경험을 제공해야 한다.

리액티브 프로그래밍과 시스템이 어떻게 빠른 데이터 스트리밍과 관련되어 있는가?

사용자의 관점에서 빠른 데이터 스트리밍(분산된 스트림 처리)[6]는 일반적으로 지역적인 스트림 기반의 이벤트 기반으로 함수형 결합자나 콜백과 같은 리액티브 프로그래밍을 사용하는 최종 사용자 API 추상화를 제공한다.

최종 사용자 API 아래에서 이것은 대체로 메시지 전달을 사용하며 노드 간의 스트림 처리 단계의 분산 시스템, 내구성 있는 이벤트 로그, 복제 프로토콜(replication protocols)을 지원하는 리액티브 시스템의 원칙을 사용한다. 이러한 부분은 보통 개발자에게 드러나 있지 않다. 리액티브 프로그래밍 을 사용자 수준에서 사용하고 리액티브 시스템 을 시스템 수준에서 사용하는 좋은 예이다.

리액티브 프로그래밍과 리액티브 시스템이 마이크로서비스와 어떤 관계가 있는가?

자율[7] 분산 서비스에 대한 시스템을 설계하는 마이크로서비스 기반의 아키텍처는 주로 클라우드를 배포 플랫폼으로 한다. 이러한 시스템에서 리액티브를 이용할 경우 많은 이점을 가질 수 있다.

우리가 봐온 것처럼 리액티브 프로그래밍리액티브 시스템 은 다른 상황에서 다른 이유로 중요하다.

  • 리액티브 프로그래밍은 하나의 마이크로 서비스 안에서 서비스 내부 로직과 데이터 흐름 관리를 구현하는 데 사용된다.
  • 리액티브 시스템 설계는 마이크로 서비스 사이 에서 사용되며 마이크로 서비스들이 분산 시스템의 규칙에 따라 동작하도록 한다. 메시지 기반으로 만들어진 복원성과 탄력성을 통해 반응성을 제공한다.

리액티브 프로그래밍과 시스템이 모바일 애플리케이션과 사물인터넷(IoT)와 어떠한 관계가 있는가?

사물인터넷(IoT)은 디바이스에서 생성한 수많은 데이터들이 탐색, 결합, 분석되어 다시 디바이스로 돌려보내야 하는 상황에서 서로 연결된 디바이스들을 어떻게 다룰 것인가에 대한 과제들을 만들어 냈다. 이것들은 항상 반응성을 유지하는 시스템에서 이루어져야 한다. 이러한 과제들은 센서 데이터를 받는 도중에 쏟아지는 데이터를 관리하는 것을 포함해서 수많은 양의 데이터를 일괄처리하거나 실시간으로 처리하는 것, 그리고 실제 사용 패턴을 시뮬레이션하는 값비싼 연산을 수행하는 것이 포함된다. 일부 IoT 배포는 디바이스에서 보낸 데이터를 처리하는 것뿐만 아니라 디바이스 관리를 위한 백엔드 서비스가 필요하다.

잠재적으로 수만 대의 장비가 연결되는 서비스를 만들 때 스케일링할 수 있는 데이터 흐름이 가능한 모델이 필요하다. 데이터 손실이나 서비스 장애와 같은 일들이 발생하기 때문에 디바이스의 장애를 처리하는 전략도 필요하다. 백엔드 시스템은 필요에 따라 스케일링할 수 있어야 하며 복원성을 제공해야 한다. 한마디로 리액티브 시스템 이 필요하다.

IoT 백엔드에서 일반적으로 발생하는 일로 수많은 센서에서 발생하는 데이터의 속도를 맞출 수 없다면 장비와 센서를 위한 역압을 구현할 필요가 있다. 엄청난 수의 장비를 사용하는 IoT 시스템의 종단 간 데이터 흐름을 본다면 데이터 저장, 정제, 처리, 분석을 막힘없이 수행해야 한다. 이를 위해서는 비동기, 논 블로킹, 역압을 지원하는 스트림이 필요하며 이 때가 리액티브 프로그래밍 이 빛나는 순간이다.

리액티브 프로그래밍과 시스템이 전통적인 웹 애플리케이션과 어떤 관계가 있는가?

리액티브 프로그래밍 방식의 웹 애플리케이션은 큰 이점을 가질 수 있다. 서비스 요청을 받고 자원을 비동기적으로 할당한 뒤 응답을 결합하고 바로 클라이언트에게 보내는 것처럼 요청-응답 작업 흐름을 결합할 수 있게 한다. 최근 서버 푸시 방식의 Server-Sent Events와 Websocket의 사용이 증가하고 있다. 그리고 이것을 스케일링 되는 서비스에서 사용하 기 위해서는 많은 커넥션을 효과적으로 관리할 필요가 있으며 IO가 블로킹 되지 않아야 한다. 리액티브 프로그래밍 에서 특히 스트림과 퓨처(Future)는 이때 좋은 도구가 될 수 있다. 이들은 논 블로킹과 비동기 변형을 가능하게 해주며 그 결과를 클라이언트에 푸시해준다. 리액티브 프로그래밍 은 데이터 접근 계층에서도 유용하다. 비동기 드라이버를 이용하여 SQL 혹은 NoSQL 업데이트와 쿼리를 효율적으로 할 수 있다.

웹 애플리케이션은 리액티브 시스템 설계에서도 이점이 있다. 분산 캐시, 데이터 일관성, 노드간 알림 등이 그 장점들이다. 전통적인 웹 애플리케이션은 보통 상태가 없는 노드를 사용한다. 하지만 Server-Sent-Events(SSE)와 WebSocket을 사용하기 시작하면 노드는 상태를 가지게 된다. 적어도 노드들은 클라이언트의 연결에 대한 상태를 가지며 알림을 직접 푸시한다. 이것을 효율적으로 수행하기 위해서는 주소를 가진 수신자에게 바로 메시지를 보내는 것이 중요하기 때문에 리액티브 시스템 설계가 필요하다.

요약

기업 및 미들웨어 벤더사들은 리액티브를 사용하기 시작했다. 2016년에는 리액티브 채택에 대한 기업의 관심이 많이 증가했다. 이 글에서 우리는 리액티브 프로그래밍은 중요한 도구이며 기업들에게 리액티브 시스템이 멀티코어, 클라우드, 모바일 구조에서 종착지라고 묘사했다.

리액티브 프로그래밍은 컴포넌트 수준에서 내부 로직과 데이터 흐름 변형에 대한 성능과 자원 효율성을 통해 개발자에게 생산성을 제공한다. 반면 리액티브 시스템은 “클라우드 네이티브” 와 다른 대규모의 분산 시스템을 구성하는 시스템 수준에서 복원성과 탄력성을 통해 아키텍트와 데브옵스에게 생산성을 제공한다. 우리는 리액티브 시스템의 설계 원칙안에서 리액티브 프로그래밍 기법을 사용하기를 권장한다.

  • 1: 일반적으로 파일 시스템과 같은 OS 기능에 의존하지 않고 설정 가능한 엔드포인트를 사용하여 클라우드와 같은 가상화된 환경에서 실행할 수 있는 애플리케이션을 나타낸다.
  • 2: FRP의 창시자인 Conal Elliott의 발표 에 따르면
  • 3: Amdahl의 법칙은 시스템의 이론적인 속도 향상은 직렬화된 부분에 제한된다는 것을 보여준다. 이것은 새로운 자원이 추가될 때 시스템이 취할 수 있는 이점이 줄어든다는 것을 의미한다.
  • 4: Neil Günter의 Universal Scalability Law은 동시성과 분산 시스템에서 경쟁과 조정의 효과를 이해하기 위한 필수적인 도구이다. 이것은 시스템의 일관성(coherency) 비용이 새로운 자원이 시스템에 추가될 때 부정적인 결과를 초래할 수 있다는 것을 보여준다.
  • 5: 메시징은 동기일 수도 있고(송신자와 수신자가 동시에 사용 가능해야 함) 비동기일 수도 있다(송신자와 수신자가 시간상으로 분리됨). 그것의 의미적인 차이를 논하는 것은 이 글의 범위에서 벗어난다.
  • 6: 예를 들어 Spark Streaming, Flink, Kafka Streams, Beam, Gearpump를 사용하는 것.
  • 7: 자율(autonomous)의 기원은 자신(self)을 뜻하는 그리스어 auto와 법(law)을 뜻하는 nomos이다. 즉 에이전트는 자신의 규칙에 따라 살아간다. 스스로 관리하며 독립적이다.