백프레셔는 데이터 스트림 처리에서 생산자(Producer)의 데이터 생산 속도가 소비자(Consumer)의 처리 속도보다 빠를 때 발생하는 문제를 해결하기 위한 매커니즘이다.
쉽게 말해 데이터가 너무 많이, 너무 빨리 쏟아져 들어와서 받는 쪽이 감당하기 어려울 때 보내는 속도를 조절해달라는 요청을 보내 속도를 조절하는 방식이다.
백프레셔가 필요한 이유
데이터를 생산하는 쪽은 매우 빠르게 데이터를 만들 수 있다. 하지만 데이터를 처리하는 쪽은 외부 요인이나 자신의 처리 능력 때문에 생산자만큼 데이터를 소화하지 못할 수 있다.
이런 속도 불균형이 발생하면 다음과 같은 문제가 발생할 수 있다.
- 메모리 고갈 (Out of Memory): 소비자가 처리하지 못한 데이터가 계속 쌓여서 시스템 메모리를 다 써버릴 수 있다.
- 데이터 손실: 버퍼가 가득 차거나 시스템이 과부하되면 일부 데이터가 제대로 처리되지 못하고 유실될 수 있다.
- 성능 저하 및 불안정성: 시스템 전체가 느려지거나 예측할 수 없는 오류로 인해 서비스가 중단될 수 있다.
백프레셔의 작동 방식
백프레셔는 기본적으로 소비자가 생산자에게 "나는 지금 이 정도 데이터를 처리할 준비가 되었으니 이 만큼만 보내주세요"라고 명시적으로 요청하는 방식이다.
이러한 풀(Pull) 기반의 요청 메커니즘 덕분에 생산자는 소비자의 처리 능력에 맞춰 데이터를 보낼 수 있게 된다.
Push vs Pull 방식
백프레셔의 중요성을 이해하려면 데이터 흐름의 두 가지 기본 방식을 알아야 한다.
Push 방식 (대부분의 문제 발생 원인)
전통적인 스트림 처리 방식은 대부분 생산자가 데이터를 생성하는 즉시 소비자에게 Push하는 형태를 가지고 있다.
- 생산자 주도: 생산자가 데이터를 만들어내는 속도에 맞춰 계속 데이터를 소비자에게 밀어 넣는다.
- 문제점: 생산자 속도가 소비자 속도보다 따르면, 소비자는 넘치는 데이터를 처리하지 못해 메모리 오버플로우, 자원 고갈, 데이터 손실 등의 문제가 발생할 수 있다.
- 이는 소비자가 감당할 수 있는 양 이상으로 데이터가 쏟아져 들어와 과부하가 걸리는 것이다.
Pull 방식 (백프레셔의 해결책)
풀 기반 요청은 소비자가 데이터를 요청할 때만 생산자가 데이터를 제공하는 방식이다.
- 소비자 주도: 소비자가 자신의 현재 처리 능력과 준비 상태를 고려하여 생산자에게 "지금 이만큼의 데이터를 처리할 준비가 되었으니 이만큼만 보내주세요"라고 요청하는 것이다.
- 해결책: 생산자는 소비자의 요청 수량만큼만 데이터를 발행하고, 더이상 요청이 없으면 데이터 생산을 일시 중지하거나 속도를 늦춘다.
- 이를 통해 소비자는 자신의 능력 범위 내에서 데이터를 처리할 수 있게 되어 과부하를 방지할 수 있다.
Pull 기반 요청의 상세 작동 과정 (Feedback Loop)
풀 기반 요청은 피드백 루프(Feedback Loop)를 통해 구현된다. 이 루프는 생산자와 소비자 간에 지속적인 소통을 가능하게 하여 데이터 흐름을 동적으로 조절한다.
1. 구독 (Subscription)
- 소비자가 스트림에 구독(Subscribe)한다. 이때 생산자와 소비자 사이에 구독 객체(Subscription)이 생성된다. 이 구독 객체는 흐름을 제어하는 통로 역할을 한다.
- 중요: 구독만으로는 데이터가 바로 흘러나오지 않는다. 생산자는 아직 데이터를 발행하지 않았기 때문이다.
2. 요청 (Request)
- 소비자는 자신의 처리 능력에 맞춰 구독 객체를 통해 생산자에서
request(n)이라는 신호를 보낸다.
- 여기서
n은 소비자가 현재 처리할 수 있는 데이터의 개수이다.
- 이 요청은 "나에게
n개의 데이터를 보내줘"라는 명시적인 지시이다.
3. 데이터 발행 (Data Emission)
- 생산자는 소비자로부터
request(n)요청을 받으면, 요청받은 수량만큼의 데이터(n개)를 스트림을 통해 발행한다.
- 만약 생산자가 발행할 데이터가
n개보다 적으면, 가지고 있는 데이터를 모두 발행하고, 요청 받은 수량이 채워질 때까지 더이상 데이터는 발행하지 않는다.
4. 데이터 처리 (Data Processing)
- 소비자는 발행된
n개의 데이터를 받아서 자신의 로직에 따라 처리한다.
5. 재요청 (Re-request)
- 소비자가
n개의 데이터를 받아서 자신의 로직에 따라 처리한다.
- 모두 성공적으로 처리하면 다시 필요한 만큼의 데이터를 생산자에게 요청한다(
request(n)).
- 이 과정이 반복되면서 생산자는 항상 소비자가 처리할 준비가 된 만큼의 데이터만 보내게 된다.
Pull 기반 요청의 장점
- 과부하 방지: 소비자가 자신의 처리 능력을 넘어서는 데이터를 받지 않으므로, 메모리 고갈이나 시스템 과부하를 효과적으로 방지한다.
- 안정성 및 신뢰성: 시스템이 예측 불가능하게 다운되거나 데이터가 손실되는 위험을 줄인다.
- 자원 효율성: 필요한 만큼만 데이터를 생산하고 전송하므로 불필요한 자원 소모를 줄일 수 있다.
- 소비자 제어: 데이터 흐름의 주도권이 소비자에게 있기 때문에 소비자의 처리량에 맞춰 유연하게 시스템을 운영할 수 있다.
주요 백프레셔 전략
시스템의 특성과 요구사항에 따라 다양한 백프레셔 전략이 사용된다.
1. 버퍼링 (Buffering)
- 개념: 생산자가 생산한 데이터를 소비자가 처리할 때까지 임시 저장 공간(버퍼)에 모아둔다.
- 작동: 버퍼가 가득 차면 생산자에게 일시적으로 데이터 생산을 멈추거나 늦추라고 요청을 한다.
- 그리고 소비자는 버퍼에서 데이터를 가져와 처리한다.
- 장점: 데이터 손실 없이 속도 차이를 완화할 수 있다.
- 단점: 버퍼 크기만큼 메모리를 사용하며 버퍼가 가득 차면 결국 생산자가 멈춰야 한다.
2. 드롭핑/샘플링 (Dropping/Sampling)
- 개념: 소비자가 감당하지 못할 정도로 데이터가 많을 경우 일부 데이터를 의도적으로 버린다.
- 작동: 최신 데이터만 중요하거나, 모든 데이터가 필수적이지 않은 경우 오래된 데이터나 일부 데이터를 버린다.
- 장점: 메모리 사용량이 적고 처리량을 유지할 수 있다.
- 단점: 데이터 손실이 발생할 수 있으므로, 데이터 손실이 허용되는 경우에만 사용해야 한다.
3. 조절 (Throttling/Pacing)
- 개념: 생산자가 특정 속도 이상으로 데이터를 보내지 못하도록 직접적으로 생산 속도를 제한한다.
- 작동: 소비자가 "초당 N개 이하로 보내주세요"같이 명확한 제어 신호를 생산자에게 보내고, 생산자가 이를 준수한다.
- 장점: 데이터 손실 없이 속도를 제어할 수 있다.
- 단점: 생산자의 최대 처리율을 제한하게 되므로 시스템 전반의 처리량이 낮아질 수 있다.
4. 오류 발생
- 개념: 소비자가 더이상 데이터를 처리할 수 없을 때, 스트림에서 오류를 발생시켜 생산을 중단시킨다.
- 작동: "더 이상 처리 못해요"라는 예외를 발생시켜 생산자에게 이를 알리고 스트림을 종료한다.
- 장점: 문제가 발생했음을 즉시 인지하고 적절히 대응할 수 있다.
- 단점: 스트림이 중단되므로, 이를 복구하거나 재시도하는 로직이 필요하다.