백엔드

리액티브 프로그래밍 (Reactive Programming)

fidget278 2022. 6. 13. 23:16

사전 지식

동기(synchronous )

동시에 일어난다는 뜻으로, 요청을 하면 바로 응답을 받는다는 의미이다. 

요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야 한다. 

설계가 간단하고 직관적이지만, 결과가 주어질 때까지 진행을 멈추고 계속 대기해야하는 단점이 있다.

ex) C언어의 scanf() 는 사용자가 입력할 때까지 기다리므로 동기 방식이다.

 

비동기(Asynchronous)

동시에 일어나지 않는다는 뜻으로, 요청과 결과가 동시에 일어나지 않는다는 의미이다. 

요청한 내용에 대해 바로 응답 받지 않아도 된다. (바로 응답이 와도 상관은 없다.)

동기 방식보다 복잡하지만 결과가 주어지는데 시간이 걸리더라도 그 시간 동안 다른 작업을 할 수 있어서

자원을 효율적으로 사용할 수 있다. 

ex) 대표적으로 스레드(Thread), 콜백(Callback), 프로미스(Promise), 코루틴(Coroutine), 퓨처(Future) 등이 있다.

언어와 라이브러리에 따라 지원되는 경우가 다르다. 

 

콜백(Callback)

다른 함수의 인자로 이용되는 함수이거나 어떤 이벤트에 의해 호출되어지는 함수이다.

 

블로킹(Blocking)

자신의 작업을 진행하다가 다른 주체의 작업이 시작되면 다른 작업이 끝날 때까지 기다렸다가 자신의 작업을 재개하는 것.

ex) A함수가 B함수를 호출하면 제어권이 B 함수에게 넘어가고, A함수는 제어권이 없기에 실행을 멈췄다가 B함수가 함수 실행을 마치고 제어권을 돌려주면 그 때 다시 재개한다. 

 

논-블로킹(Non-Blocking)

다른 주체의 작업에 관련없이 자신의 작업을 하는 것.

ex) A함수가 B함수를 호출하면 B함수는 실행되지만 제어권은 A함수가 그대로 가지고 있고, B함수를 호출한 이후에도 자신의 코드를 계속 실행한다.

 

옵저버 패턴 (Observer Pattern) - 관찰자 패턴

옵저버 패턴은 일대다의 관련성을 갖는 객체들 중 한 객체의 상태가 변하면 다른 모든 객체에 그 사항을 알리고 필요한 수정이 자동으로 이루어지도록 하는 디자인 패턴이다. ConcreteSubject의 상태 변경이 일어났을 때 Subject의 notify() 함수를 통해 Observer의 상태를 변경한다. 옵저버 패턴에서 Subject는 Observer의 상태를 고려하지 않고 이벤트를 보내기 때문에 Push 방식이라고 한다.

 

이터레이터 패턴 (Iterator 패턴) - 반복자 패턴

반복자 패턴은 컬렉션 구현 방법을 노출 시키지 않으면서 컬렉션 안에 들어있는 모든 엘리먼트에 접근할 수 있는 방식을 구현한 패턴이다. 옵저버 패턴과 달리 반복자 패턴은 next()를 통해 데이터를 리턴 받기 때문에 Pull 방식이라고 한다.

 

리액티브 프로그래밍 (Reactive Programming)

기존 전통적인 서버 아키텍쳐에서는 요청을 처리하기 위해 하나의 요청에 대해 하나의 스레드를 할당해 처리하는 방법인 동기 블로킹 방식을 사용하였다. 이런 아키텍쳐에서는 스레드를 만드는 시간을 줄이기 위해 스레드 풀을 만들어 처리한다. 하지만 스레드 풀을 넘어서는 요청이 들어오면 CPU와 메모리가 충분해도 처리할 스레드가 부족해 요청을 처리하지 못하는 현상이 발생한다. 그렇다고 무작정 스레드 갯수를 늘리면 이번엔 스레드가 충분함에도 과도한 컨텍스트 스위칭이 발생하여 CPU와 메모리 때문에 처리율이 떨어질 수 있다. 서버의 Scale-up, Scale-out이 일시적인 해결책이 될 수는 있지만 근본적인 문제의 해결이 아니기 때문에 인프라의 자원 낭비로 이어질 것이다. 즉, 기존 아키텍쳐의 동기 블로킹 방식은 구현도 쉽고 안정성도 어느정도 보장된 방식이지만, 한 번에 많은 요청이 들어올 경우 효율이 떨어질 수 있다.

 

예를 들어 기존의 톰캣-스프링MVC 방식은 동기 블로킹 방식으로, 톰캣의 최대 쓰레드 개수는 기본 200개이기 때문에 요청이 폭주하게 되면 여유 쓰레드가 없어서 Connection refuse나 Connection timeout이 발생할 수 있다.

 

그렇다면 동기식 어플리케이션을 비동기 어플리케이션으로 바꾸면 어떨까. 이 경우에는 멀티 CPU 코어를 제대로 활용하기 위해 복잡한 병렬 처리 코드가 필요했고, 처리할 데이터가 무한정 많아져서 시스템의 한계를 넘어서는 경우에는 병목 현상이 발생했다. 

 

이러한 문제점들을 극복하기 위해 옛날부터 존재하던 리액티브 프로그래밍 개념이 다시금 주목 받게 된다. 왜냐면 지금의 시대는 IoT와 모바일 등으로 서버가 동시에 수 백만의 장치에 연결될 수 있어 병행과 분산 어플리케이션에 대한 요구가 급격히 증가했고 더불어 클라우드, 컨테이너 기술을 이용해 배포하는 경우가 많아 더 나은 자원 활용과 확장성에 대한 필요성이 증가했기 때문이다. 또한 모든 구현체가 상호운영할 수 있는 표준으로 특정 구현체에 얽매이는 위험을 감소시킬 수 있다.

 

리액티브 프로그래밍이란 데이터의 흐름을 먼저 정의하고, 데이터 또는 이벤트의 변경이 발생하면 이에 반응해 데이터를 스트림 형태로 처리하는 프로그래밍 기법이자 선언적 프로그래밍의 한 종류이다. 리액티브 프로그래밍의 특징은 비동기 프로그래밍을 처리하는 새로운 접근 방식이라는 것이다. 리액티브 프로그래밍은 데이터의 통지, 완료, 에러에 대한 처리를 옵저버 패턴에 영감 받아 설계 되었으며 이터레이터 패턴과 합쳐서 사용한다. 또한 데이터의 손쉬운 비동기 처리를 위해 함수형 언어 접근 방식을 사용한다. 

 

리액티브 프로그래밍이 나오기 전 비동기 프로그래밍은 대부분 콜백(Callback) 기반의 비동기 처리 방식을 사용했다. 간단한 비동기 처리는 콜백 기반의 비동기 프로그래밍으로도 이해하기 쉬운 코드를 작성할 수 있지만, 콜백이 많아져서 꼬리에 꼬리를 무는 콜백의 형태가 되는, 일명 '콜백 지옥' 으로 인해 코드가 복잡해지고 가독성과 유지보수성이 떨어지곤했다. 

 

리액티브 프로그래밍을 적용하면 이러한 문제점을 해결할 수 있어 비동기 기반의 논-블로킹, 이벤트 기반(Event-Driven) 애플리케이션 구현에 유리하다. 

 

* 이벤트 기반(Event-Driven) 프로그래밍이란?

https://heeonii.tistory.com/3

 

이벤트 기반 프로그래밍 - Event driven programming

이벤트 기반 프로그래밍 혹은 EDP라고도 하는 프로그래밍 패러다임에 대해 간단히 세미나를 진행하였습니다! 이벤트기반프로그래밍은 일어나는 일을 뜻하는 이벤트와 특정 방향으로 몰고가다

heeonii.tistory.com

 

리액티브 스트림 (Reactive Streams)

리액티브 스트림은 2013년 Netflix, Pivotal, Typesafe의 개발자들이 발의해서 처음 만들어진 리액티브 프로그래밍을 위한 명세이며 논-블로킹과 백프레셔를 갖춘 비동기 스트림 처리를 위한 표준이다. 옵저버 패턴, 반복자 패턴, 함수형 패러다임의 조합으로 정의 된다. 리액티브 스트림에 정의된 인터페이스를 구현하면 리액티브 프로그래밍을 구현할 수 있다. 

 

리액티브 스트림의 목표는 개발자들로 하여금 용어를 통일하고 라이브러리들이 리액티브 스트림 사양을 준수하여 일관성 있는 구현을 따르도록 만드는 것이다. 리액티브 스트림 사양이 만들어지기 전에는 표준화된 사양이 없어 라이브러리마다 각자의 방식으로 구현을 해야했다. 지금은 리액티브 스트림 표준 사양을 지원하는 라이브러리들이 많이 발표 되었고 JDK9에는 Flow라는 이름으로 리액티브 스트림 구현이 추가되었다. 

 

리액티브 스트림 API 명세 : https://github.com/reactive-streams/reactive-streams-jvm

 

백프레셔(Back-Pressure) - 배압

리액티브 스트림은 Observer 패턴과 Iterator 패턴의 조합이라고 설명했다.

 

기존 Observer 패턴에는 빠른 프로듀서와 느린 컨슈머 이슈가 존재한다. Observer가 이벤트를 처리하는 속도보다 Subject가 이벤트를 발행하는 속도가 빠를 때 발생하는 현상이다. 예를 들어 1초에 1개의 요청을 처리하는 Observer에게 1초에 10개씩 요청이 들어온다면 요청은 처리되지 못한다. 유실 없이 요청을 처리하기 위해 버퍼를 두어 밀린 요청을 넣어둔다고 해도 결국 계속 쌓인다면 버퍼도 터져버릴 것이다. 

 

리액티브 스트림에서는 이러한 문제를 해결하기 위해 백프레셔를 도입하였다. 

백프레셔는 배압이란 뜻으로, 배관 등에서 과투입되는걸 막기 위해 역으로 압력을 주어 압력을 약화 시키는 것을 말한다.

마찬가지로 Observer가 오버플로우 되는 것을 막기 위한 용도이며, 옵저버 패턴의 Push 방식과 이터레이터 패턴의 Pull 방식을 조합하여 Observer는 자신이 처리 가능한 갯수만큼의 이벤트를 Subject에게 요청하고 Subject는 요청에 의해서 이벤트를 전달해준다. 이것을 dynamic pull 이라고 부른다.

 

 

리액티브 스트림의 구현

리액티브 스트림에서 제공하는 핵심 인터페이스는 아래와 같다.

출처 : https://devsh.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B8%B0%EC%B4%88-3-%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C-%EC%8A%A4%ED%8A%B8%EB%A6%BC?category=828750

 

 

Publisher : 발행자는 데이터를 생성하고 구독자에게 통지한다. 

이 때 발행자가 제공할 수 있는 데이터의 양은 무한하고 순차적 처리를 보장한다.

// 리액티브스트림의 발행자. Observer 패턴의 Subject에 해당.
public interface Publisher<T> {
   public void subscribe(Subscriber<? super T> s); // Observer 패턴의 registerObjserver에 해당
}

 

Subscriber : 구독자의 메서드들은 리액티브 스트림에서 구독자와 발행자간의 데이터 전달에 사용하는 규약(Protocol)이라고 설명하고 있다. 옵저버 패턴에서 Subject에서 직접 Subscriber에게 통지해준 것과 달리 리액티브 스트림에서는 Subsciption을 통해 Subscriber.onNext, onComplete, onError 로 전달된다. 

 

public interface Subscriber<T> {
    public void onSubscribe(Subscription s); // 구독 시 최초에 한 번만 호출. 초기화 작업 하는 곳
    public void onNext(T t); // 옵저버 패턴의 notify + 이터레이터 패턴의 next
    public void onError(Throwable t); // next에서 Exception이 발생했을 경우 에러 처리 하는 곳 
    public void onComplete(); // 이터레이터 패턴의 hasNext. 
    //모든 데이터를 통지한 시점에 호출되어 데이터 통지가 성공적으료 완료되었음을 알림

}

Subscription : 위에서 설명한대로 빠른 Publisher - 느린 Subscriber 문제를 해결하는 Backpressure를 적용한 것이다. request 메소드를 통해 요청량을 조절한다. request를 통해 MAX개씩 요청하면 순수 push 모델이 되는 것이고 request를 통해 onNext당 1개씩 요청하면 pull 모델이 된다.   

public interface Subscription {
    public void request(long n); // 데이터를 n만큼 요청한다
    public void cancel(); // 구독 취소
}

 

최종적인 흐름은 아래와 같다.

 

1. Subscriber가 publisher.subscribe 메소드를 통해 Publisher 에게 구독을 요청
2. Publisher 는 subscribe.onSubscribe 메소드로 Subscriber에게 Subscription 를 전달
3. Subscriber 는 Subscription.request 을 통해, 자신에게 데이터를 발행할 것을 요구
4. Publisher 는 Subscription를 통해 subscriber.onNext로 데이터를 전달한다. (Subscriber는 2번에서 내부에 Subscription를 set 해놓음)
5. 전달이 잘 끝났으면, onComplete, 오류났다면 onError 로 끝낸다.

 

마지막으로 Proccesor 는 발행자와 구독자를 모두 상속 받는 인터페이스이며 자신만의 추상 메서드는 존재하지 않고, 일반적으로 데이터 가공 단계에서 사용되어 발행자와 구독자의 모든 사양을 따른다.

 

 

####

 

Observer패턴의 문제점을 Iterator 패턴과 합쳤다고 하는데..
발행-구독 패턴과 Iterator의 결합으로도 보이는데... 나름 정리를 해봤지만 아직 확실하게 이해가 안되는 부분이 있는듯 하다

 

Reference

https://sjh836.tistory.com/182

 

Reactive Streams (관찰자 결합, 반복자 결합, Back Pressure, 흐름, Processor, 비동기 및 병렬화 구현방식)

참조문서 http://www.reactive-streams.org https://github.com/reactive-streams/reactive-streams-jvm/ https://engineering.linecorp.com/ko/blog/reactive-streams-with-armeria-1/ 1. Reactive Streams 란?..

sjh836.tistory.com

https://sightstudio.tistory.com/14

 

[Reactive] Reactive Programming 과 Reactive Stream

Spring Webflux를 사용하면서 Reactive Programming 과 Reactive Strem에 대해 공부했던 부분을 정리합니다. 1. 리엑티브 프로그래밍이란? 단순히 이름만 듣고서는 어떤 뜻인지 정확하게 와닫지 않습니다.. [위

sightstudio.tistory.com

https://devsh.tistory.com/entry/리액티브-프로그래밍-기초-옵저버-패턴과-이터레이터-패턴

 

리액티브 프로그래밍 기초 1 - 옵저버 패턴과 이터레이터 패턴

리액티브 프로그래밍 기초 1 - 옵저버 패턴과 이터레이터 패턴 리액티브 프로그래밍 기초 2 - 비동기 프로그래밍 리액티브 프로그래밍 기초 3 - 리액티브 스트림 리액티브 프로그래밍(Reactiv

devsh.tistory.com

https://inpa.tistory.com/entry/%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB-%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%B8%94%EB%A1%9C%ED%82%B9%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC

 

👩‍💻 동기&비동기 / 블로킹&논블로킹 💯 완벽 이해하기

동기&비동기 vs 블로킹&논블록킹 개념 차이 이 개념들을 처음 접하거나 컴퓨터 공학에 대해 잘 모르는 사람은 이 개념들이 서로 뭔가 연관이 있는 것으로 오해하기 쉽다. 아무래도 동기와 블록

inpa.tistory.com

https://javacan.tistory.com/entry/why-reactive-summary

 

왜 리액티브인가 요약

Why Reactive? 책 요약(책은 여기에서 다운로드 가능) 1장. 소개 시스템은 다음과 같아야 함 사용자에게 응답해야 함 실패를 다루고 사용불능 상태에서도 동작해야 함 다양한 부하 상황에서 견뎌야

javacan.tistory.com