# Backpressure: 스트리밍 데이터가 폭주할 때 시스템을 살리는 기술
Table of Contents
댐이 무너지기 직전입니다
여러분이 데이터를 처리하는 서버(Consumer)를 개발했다고 칩시다. 이 서버는 초당 100개의 데이터를 처리할 수 있습니다. 꽤 준수하죠? 그런데 어느 날 이벤트(Producer) 쪽에서 초당 10,000개의 데이터를 쏟아붓기 시작합니다.
어떤 일이 벌어질까요?
- 메모리 큐에 처리 못 한 데이터가 쌓이기 시작합니다.
- GC(Garbage Collector)가 미친 듯이 돌면서 CPU를 잡아먹습니다.
- 결국 Out Of Memory (OOM) 에러와 함께 서버가 장렬히 전사합니다.
- 서버가 재시작되지만, 쌓여있던 데이터를 처리하려다 다시 죽습니다. (무한 반복)
이것이 바로 Backpressure(배압) 처리가 안 된 시스템의 최후입니다. Backpressure란 말 그대로 **“뒤에서 압력을 가한다”**는 뜻입니다. 받는 쪽(Consumer)이 “나 너무 힘드니까 좀 천천히 보내!”라고 보내는 쪽(Producer)에게 신호를 주는 메커니즘이죠.
Backpressure를 처리하는 3가지 전략
물탱크에 물이 넘칠 때 어떻게 해야 할까요? 수도꼭지를 잠그거나, 물을 버리거나, 더 큰 탱크를 가져와야겠죠.
1. 제어(Control): “잠깐만, 천천히!”
가장 이상적인 방법입니다. Consumer가 Producer에게 속도 조절을 요청하는 겁니다.
- TCP의 Window Size: TCP 프로토콜 자체가 이렇게 동작합니다. 수신 측 버퍼가 차면 송신 측에 윈도우 사이즈를 0으로 보내서 전송을 멈추게 하죠.
- Reactive Streams:
request(n)메서드를 통해 “나 지금 n개만 처리할 수 있어”라고 명시적으로 요청합니다. - Node.js Stream:
write()가false를 리턴하면 “버퍼 꽉 찼으니 기다려”라는 뜻입니다.drain이벤트가 발생하면 다시 보냅니다.
2. 버퍼링(Buffering): “일단 쌓아둬”
처리 속도가 들쑥날쑥할 때 유용합니다. 잠깐 데이터가 몰릴 때 큐(Queue)나 카프카(Kafka) 같은 중간 저장소에 쌓아두고 천천히 처리하는 방식입니다. 하지만 이것도 임시방편입니다. 큐도 결국엔 꽉 찹니다. Kafka의 디스크가 가득 차거나, 메모리 큐가 터질 수 있죠. 결국은 소비 속도가 생산 속도를 따라잡아야 합니다.
3. 드랍(Dropping): “못 먹는 건 버려”
실시간성이 중요한 시스템(예: 주식 시세, CCTV 스트리밍)에서는 오래된 데이터를 처리하느니 차라리 버리는 게 낫습니다.
- Drop Oldest: 큐가 차면 가장 오래된 데이터부터 버립니다.
- Drop Latest: 큐가 차면 새로 들어오는 데이터를 튕겨냅니다.
플랫폼별 구현 예시
Kafka Consumer Lag
Kafka는 그 자체로 거대한 버퍼 역할을 합니다. 하지만 Consumer가 너무 느리면 Consumer Lag(아직 안 읽은 메시지 수)이 계속 늘어납니다.
이럴 땐 Backpressure라기보다 **Scaling(확장)**이 필요합니다.
- Consumer 인스턴스를 늘리고, 파티션 수를 늘려 병렬 처리를 해야 합니다.
- Spring Kafka에서는
max.poll.records설정을 줄여서 한 번에 가져오는 양을 조절할 수 있습니다. “한 번에 조금씩만 가져와서 확실히 처리하고 커밋하자”는 전략이죠.
Node.js Streams
Node.js는 스트림 처리에 진심인 언어입니다. pipe() 메서드가 Backpressure를 자동으로 처리해줍니다.
// 나쁜 예: 메모리에 다 읽어놓고 쓰기 (OOM 위험!)
fs.readFile('big-file.txt', (err, data) => {
fs.writeFile('output.txt', data, () => {});
});
// 좋은 예: Stream과 pipe 사용 (자동 Backpressure)
const readStream = fs.createReadStream('big-file.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream.pipe(writeStream);
// 읽는 속도가 쓰는 속도에 맞춰서 자동으로 조절됨
RxJava / Reactor
리액티브 프로그래밍에서는 Backpressure가 핵심 기능입니다.
// Consumer가 감당 안 되면 버퍼에 쌓다가 넘치면 에러 발생
Flowable.range(1, 1_000_000)
.onBackpressureBuffer(1000, BufferOverflowStrategy.ERROR)
.observeOn(Schedulers.io())
.subscribe(data -> slowProcess(data));
마치며
“우리 시스템은 데이터가 적어서 괜찮아요.” 지금은 괜찮겠죠. 하지만 트래픽은 예고 없이 폭주합니다. 마케팅 팀이 대박 이벤트를 터뜨리거나, 디도스 공격이 들어올 수도 있습니다.
Backpressure는 시스템의 안전 밸브입니다. 평소에는 존재감이 없지만, 위기 상황에서 시스템 전체가 셧다운되는 최악의 상황을 막아줍니다. 스트리밍 데이터를 다룬다면, 혹은 Producer와 Consumer의 속도 차이가 우려된다면, 반드시 “버퍼가 꽉 찼을 때 어떻게 할 것인가?”에 대한 전략을 세워두세요.