리액티브 스트림은 백프레셔를 가지고 비동기 요소들 사이의 상호작용을 정의하는 작은 스펙을 말합니다.
https://www.reactive-streams.org/
Reactive Streams is a standard for asynchronous data processing in a streaming fashion with non-blocking back pressure.
직관적으로 이해하기 어려우니 아래 두가지 개념을 먼저 정리해보겠습니다.
스트리밍 처리 방식은 전통적인 데이터 처리 방식과 함께 비교해보면 이해가 잘됩니다.
왼쪽의 전통적인 방식은 사용자 요청이 오면 해당 요청의 페이로드 데이터를 모두 메모리에 저장하고 데이터 베이스에서 조회한 데이터들도 메모리에 저장하며 마지막으로 응답을 위한 데이터 생성해서 메모리에 저장 후 사용자에게 응답을 보낸다. 한번의 요청에 대한 처리하기 위해 많은 데이터가 어플리케이션 메모리에 저장됩니다. 하나의 요청에서 처리하는 데이터가 많을 경우, 메모리가 부족할 가능성(out of memory의 가능성)이 있습니다. 하나의 요청에 데이터가 많지 않더라도 순간적으로 많은 요청이 몰리면 다량의 GC(Garbage Collection)이 발생할 가능성이 있습니다.
이러한 문제를 해결하기위해 스트림 방식을 사용하면 크기가 작은 시스템 메모리로도 많은 양의 데이터를 처리할 수 있습니다. 즉, 최대한 지금 당장 처리할 데이터만 메모리에 저장되어 있기 때문입니다.
백프레셔를 설명하기 전에 옵저버 패턴과 Pull 방식과 Push 방식을 알아보겠습니다.
이벤트를 발행하는 Publisher가 Event를 Comsume하는 Subscriber에게 이벤트를 보내는 방식을 Push 방식이라고 합니다. 하지만 Push 방식을 사용할 때 고려해야할 부분은 이벤트를 보내는 쪽과 받는 쪽의 속도를 잘 생각해봐야합니다. 예를 들어 Publisher가 초당 100개의 Event를 보낼 경우, Subscriber가 좀 더 느린 초당 10개의 이벤트를 처리한다면 Subscriber는 대기하는 이벤트를 위한 Queue를 두어야합니다.
만약 Queue 사이즈 이상으로 이벤트가 발생할 경우, 가변 길이 Queue 를 사용할 경우, Out of Memory가 발생할 테니 사용하면 안될 것이고 고정 길이 Queue를 사용하면 신규로 수신된 메시지를 거절합니다. 거절된 메시지는 재요청하게 되며 재요청 과정에서 네트워크와 CPU 연산 비용이 추가로 발생합니다.
여기까지 봤을 때 Push 방식은 한계가 있어보입니다.
Pull 방식의 경우, Event를 처리하는 Subscriber가 반대로 자신이 처리할 사이즈의 Event를 Publisher에게 요청하는 방식을 말합니다. Publisher는 요청받은 만큼만 전달하면 되고, Subscriber는 더 이상 ‘Out of Memory’ 에러를 걱정하지 않아도 됩니다.
좀더 동적으로 Subscriber가 3개의 Event를 처리하고 있다면 7개의 event를 Publisher에게 요청하게 됩니다.
풀 방식에선 이렇게 전달되는 모든 데이터의 크기를 구독자가 결정합니다. 이런 다이나믹 풀 방식의 데이터 요청을 통해서 구독자가 수용할 수 있는 만큼만 데이터를 요청하는 방식이 백 프레셔입니다.
비동기 시스템에서 사이즈가 정해져있지 않은 데이터 스트림들을 다룰 때 특별한 처리가 필요합니다. 주목할 만한 문제는 너무 빠르게 전달되는 데이터 자원의 양이 스트림을 소비하는 도착지에서 처리하는 양을 압도하지 않게 하기 위한 제어가 필요하다는 것입니다.
결록적으로 데이터를 전달 받는 쪽에서 특정 사이즈를 가진 데이터를 버퍼하도록 강요하지 않으면서 비동기 thread 경계 사이로 스트림 데이터의 교환(다른 쓰레드 혹은 쓰레드 풀로 데이터를 전달)을 관리하는 것이 Reactive Streams의 주된 목적입니다.
이런 목적을 달성하기 위해서 위에서 설명했던 백프레셔를 적용하고 있습니다.