Reactive Programming & Reactor3 #01 - Reactive Programming과 Reactor3 소개

May 30, 2020


최근에 Reactive에 관한 기술을 다시 공부할 필요성을 느끼게 되었다. Reactive의 이점을 제대로 누리기 위해서는 어플리케이션의 로직 및 I/O가 발생하는 부분에서 Async & Nonblocking이란 조건을 만족해야 한다. 기존에는 Reactive하게 개발한다고 하더라도 JDBC가 Blocking하게 동작하기 때문에 Reactive의 이점을 누리기에는 한계가 있었다. 하지만 최근 들어 이러한 한계점을 극복하는 R2DBC(Reactive Relational Database Connectivity) 등의 기술이 발전하고 있기 때문에 Reactive Programming에 관해 다시 살펴볼 좋은 타이밍이라고 생각하고 이에 관한 포스트를 작성하기로 결심하였다.

Spring Webflux의 베이스가 되는 Reactor3의 레퍼런스 문서를 참고하면서 앞으로 포스트를 작성할 예정이다.


1. Reactive Programming 소개

레퍼런스에는 Reactor3가 구현한 Reactive Programming을 다음과 같이 소개하고 있다.

Reactive Programming은 Data Stream 및 변경에 대한 전파와 관련된 Async Programming이다.

무슨 말인고 하니 Data Stream은 Iterable & Iterator 패턴, 변경에 대한 전파는 Observer 패턴에 비유해 보자. Iterable & Iterator 패턴에서

  • Iterable은 리스트나 배열의 특정 원소 값들에 접근할 수 있는 Iterator를 반환하는 책임이 있다.
  • Iterator는 원소 값에 대한 접근을 가능하도록 하는 책임이 있다.

즉, 데이터 관점에서 보면 Iterable은 처리할 데이터들의 묶음을 반환하는 역할을 맡고, Iterator는 그 데이터에 접근하기 위한 역할을 제공한다.

Reactor3에서는 Publisher-Subscriber 모델을 사용한다. Event-Driven Architecture와 유사한데, Publisher는 데이터를 발행한 후 Subscriber에 던져주고 Subscriber는 그 데이터에 접근해 처리하게 된다. 이러한 관점에서 보면 IterablePublisher, IteratorSubscriber에 비유가 가능하다.

Observer 패턴은 대상 오브젝트의 상태가 변하거나 이벤트가 발생했을 경우 이를 감지해 적절한 로직을 수행할 수 있도록 하는 디자인 패턴이다. Reactor3에서는 PublisherSubscriber에게 데이터를 전달해주면 Subscriber는 이를 감지하여 그 데이터를 바탕으로 적절한 로직을 수행한다. (아직은 설명하지 않았지만 Reactive의 주요 특징인 Backpressure도 이와 관련이 있다.)

그리고 Reactive는 명령형이 아닌 선언적인 특징을 가지고 있다. 예를들면 Iterable & Iterator 패턴에서는 next() 메소드를 통해 데이터에 대한 접근을 개발자가 원하는 시기에 수행하는데 반해, Reactor3의 Publisher & Subscriber은 자바8의 Stream처럼 데이터 접근 메소드를 직접 호출하지 않고 데이터 처리에 대한 로직을 선언만 한다.

Async Programming에 대한 설명을 하지 않았는데, 이는 간단하게 위에서 말한것을 모두 Async하게 처리하는 것이 Reactor3가 구현한 Reactive Programming이라고 생각하면 된다.

2. Reactive Programming을 구현한 Reactor3의 특징 및 장점

위에서 Reactor3가 구현한 Reactive Programming의 기본 개념을 알아보았지만, 이러한 패러다임이 우리에게 주는 이점이 무엇이지? 라는 생각이 들것이다. 궁금증을 해소하기 위해 Reactor3에 반영된 Reactive Programming의 특징 및 장점에 대해서 알아보겠다.

1. Asynchronous & Non-Blocking 이다.

Non-Blocking은 어떠한 로직이 멈추지 않고 계속 수행된다. 라는 정도로 이해하면 되겠다. 비동기적으로 처리하지 않는다라는 뜻인 Asynchronous와는 다른 뜻인데 이 차이점에 대해 많은 사람들이 명확하게 차이점을 인지하지 못하고 있어, 간단한 예를들어 설명해보면 아래와 같다.

  • Asynchronous인데 Blocking인 경우
          // 비동기로 데이터를 가져온다. (Asynchronous)
          Future<Data> future = executorService.submit(() -> {
              Data data = dataService.getData();
              return data;
          });
    
          // 데이터를 가져오는것은 다른 스레드에 위임하여 비동기 방식으로 동작하지만,
          // 아래의 비동기 작업이 완료될 때까지 현재 스레드는 blocking 된다. 
          Data data = future.get();
    
          // future.get()의 동작이 완료되어야 아래의 로직이 수행된다.
          service.processLogic2();
    

    Non-Blocking & Async 방식으로 동작해 I/O와 어플리케이션 로직에서의 유휴 리소스를 최소화하므로 적은 리소스로도 많은 퍼포먼스를 발휘할 수 있다.

2. Asynchronous & Non-Blocking한 코드를 작성하기 쉽다.

Asynchronous & Non-Blocking한 코드를 작성할때, 자바에서는 콜백 패턴을 주로 사용하게 된다. 복잡한 비즈니스 로직에 이를 적용하게 되면 Callback 패턴이 중첩된 Callback Hell을 마주할 수 있는데 이는 코드의 가독성과 유지 보수성을 현저히 떨어뜨린다. CompletableFuture로 Callback Hell을 어느정도 방지할 수는 있지만, Reactor3는 데이터에 대한 Asynchronous & Non-Blocking 처리 과정을 추상화하여 Java8의 Stream과 비슷하게 메소드 체이닝 방식으로 코드를 더 쉽게 작성하게 해준다.

3. 풍부한 Operator의 제공

Java8의 스트림의 map(), filter() 등 데이터를 가공 및 처리하는 연산자에 대응되는 풍부한 Operator를 제공한다. Operator는 변형된 새로운 데이터를 다시 제공한다는 관점에서 중간 Publisher가 생겨난다.

4. Subscribe 될 때까지 로직을 수행하지 않는다.

기본적으로 Reactor3를 이용하여 메소드 체이닝 방식으로 Publisher가 제공하는 데이터를 처리 및 가공하는 로직을 작성하더라도 subscribe() 메소드가 호출되지 않으면 아무런 동작을 하지 않는다. 이는 재사용성 및 다른 Publisher간의 합성 기능을 지원하기 위함인것 같다.

5. Backpressure

BackpressureSubscriberPublisher에게 피드백을 주어 Publisher가 어떠한 조치를 취하도록 하는것이다. 예를 들어, Subscriber가 30개의 데이터를 처리한 결과 많은 시간이 걸렸다면, SubscriberPublisher에게 앞으로는 30개보다 더 적은 데이터를 주도록 요청을 하는 경우이다. 무한히 데이터가 흐를수 있다라고 가정을 하는 Data Stream 관점에서는 중요한 특징인 것 같다.

위의 내용들을 토대로 Reactor3가 동작하는 그림을 간단히 묘사하면 아래와 같다. reactor4-diagram-01




참고 리소스
https://projectreactor.io/docs/core/release/reference/
https://tech.io/playgrounds/929/reactive-programming-with-reactor-3/Intro