Redis Stream vs Kafka
Redis Stream과 Kafka는 모두 메시지 큐로 사용할 수 있지만, 내부 동작과 보장 수준이 다르다. 클러스터 레벨의 차이는 이전 글에서 다뤘고, 이 글에서는 메시지 큐로 사용할 때의 동작 차이에 집중한다.
Delivery Guarantee
메시지 전달 보장은 세 단계로 나뉜다.
| 레벨 | 의미 | 리스크 |
|---|---|---|
| At-most-once | 최대 1번 전달, 유실 가능 | 메시지가 사라질 수 있다 |
| At-least-once | 최소 1번 전달, 중복 가능 | 같은 메시지를 두 번 처리할 수 있다 |
| Exactly-once | 정확히 1번 전달 | 이론적으로 가장 이상적 |
Redis Stream
- 기본은 at-most-once이다.
XREAD로 읽으면 ack 없이 소비된다 - Consumer Group +
XACK사용 시 at-least-once가 된다. Consumer가 메시지를 읽고 처리한 뒤XACK를 보내야 완료 처리된다 - ACK되지 않은 메시지는 PEL(Pending Entries List) 에 남는다
- Exactly-once는 지원하지 않는다. 애플리케이션 레벨에서 멱등성으로 처리해야 한다
Kafka
acks=0이면 at-most-onceacks=all+ Consumer의 수동 offset commit이면 at-least-once- Exactly-once semantics(EOS): Idempotent Producer + Transactional API 두 메커니즘의 조합으로 달성한다
다만 Kafka의 Exactly-once는 Kafka 내부 파이프라인에서만 보장된다.
Producer → Kafka → Consumer 안에서의 중복 제거이지, Consumer가 외부 DB에 쓰는 시점까지 보장하는 것은 아니다.
외부 시스템까지 포함하면 결국 at-least-once + 멱등성이 현실적인 답이다.
Kafka Exactly-once의 내부 동작
두 가지 메커니즘이 조합된다.
Idempotent Producer
Producer에 고유한 PID(Producer ID)와 메시지별 sequence number가 부여된다. Broker가 (PID, sequence) 쌍을 추적하여, 동일한 메시지가 재전송되면 중복 저장하지 않는다.
네트워크 재시도로 인한 중복을 Broker 레벨에서 제거하는 것이다.
Transactional API
- Producer가
beginTransaction()→ 여러 Partition에 메시지 전송 →commitTransaction()또는abortTransaction() - Consumer 쪽에서
isolation.level=read_committed로 설정하면 커밋된 트랜잭션의 메시지만 읽는다 - “Topic A에서 읽고 → 처리하고 → Topic B에 쓰는” 파이프라인을 하나의 트랜잭션으로 묶을 수 있다
PEL (Pending Entries List)
Redis Stream Consumer Group의 핵심 메커니즘이다.
Consumer가 XREADGROUP으로 메시지를 읽으면 해당 메시지는 PEL에 등록된다. XACK를 보내야 PEL에서 제거된다.
- Consumer A가 msg-1, msg-2를 읽는다. PEL에 두 메시지가 등록된다
- Consumer A가 msg-1 처리 완료 후
XACK를 보낸다. PEL에서 msg-1이 제거된다 - Consumer A가 죽는다. msg-2는 PEL에 남아있다
XCLAIM또는XAUTOCLAIM으로 Consumer B가 msg-2를 인계받아 재처리한다
PEL이 있기 때문에 Consumer 장애 시에도 메시지가 유실되지 않는다. XPENDING 명령으로 PEL 상태를 조회할 수 있고, 일정 시간 이상 ACK되지 않은 메시지를 모니터링하여 장애를 감지할 수 있다.
Backpressure 처리
Consumer가 메시지 처리 속도를 따라가지 못하면 메시지가 쌓인다. 이 상황에 대한 대응 전략이 다르다.
Redis Stream
MAXLEN: Stream의 최대 길이를 제한한다. 초과 시 오래된 메시지부터 삭제된다MINID: 특정 ID 이전의 메시지를 삭제한다XTRIM: 수동으로 메시지를 잘라낸다- Consumer lag 모니터링은
XINFO GROUPS로 확인 가능하지만, 전용 모니터링 도구가 빈약하다
Kafka
- Consumer lag을 Partition의 최신 offset과 Consumer Group의 현재 offset 차이로 측정한다. Burrow, Kafka Exporter 등 전용 모니터링 도구가 풍부하다
- Consumer가
pause()/resume()으로 특정 Partition 소비를 일시 중단할 수 있다 - retention 정책으로 오래된 메시지를 자동 삭제한다. 시간 기반(
retention.ms)과 크기 기반(retention.bytes) 모두 가능하다 - Partition 추가로 Consumer를 수평 확장할 수 있다
Consumer Group 모델 비교
| Redis Stream | Kafka | |
|---|---|---|
| Consumer 식별 | Consumer 이름 (문자열) | Group 내 자동 할당 |
| Partition 할당 | 없음 (단일 Stream에서 경쟁 소비) | Partition별 Consumer 1:1 배정 |
| Rebalance | 없음 (수동 XCLAIM) | 자동 (Eager/Cooperative) |
| Offset 관리 | PEL + XACK (메시지별) | Offset commit (Partition별) |
| 병렬 처리 상한 | Consumer 수 제한 없으나 단일 Stream 병목 | Partition 수 = 병렬 상한 |
Redis Stream은 하나의 Stream에서 여러 Consumer가 경쟁적으로 소비한다. 먼저 읽는 Consumer가 가져가는 방식이라 별도 할당 로직이 없다. 단순하지만, 처리량이 단일 Stream의 한계에 묶인다.
Kafka는 Partition을 Consumer에게 배타적으로 할당한다. Partition 0은 Consumer A만, Partition 1은 Consumer B만 읽는다. 그래서 Partition 수가 병렬 처리의 상한이 된다.
선택 기준
| 요구사항 | Redis Stream | Kafka |
|---|---|---|
| 간단한 작업 큐, 낮은 운영 부담 | 적합 | 과도할 수 있다 |
| 높은 처리량, 수평 확장 | 키 샤딩 필요 (수동) | Partition으로 네이티브 지원 |
| Exactly-once (내부 파이프라인) | 미지원 | 지원 |
| 메시지 영속성 | 메모리 기반 (유실 가능) | 디스크 기반 (retention 설정) |
| 모니터링 | 기본적 (XINFO) | 풍부 (Burrow, Exporter 등) |
| 이미 Redis를 사용 중 | 추가 인프라 불필요 | Kafka 클러스터 별도 운영 |
Redis Stream은 이미 Redis를 사용하고 있고 요구사항이 단순할 때 합리적인 선택이다. 처리량이 단일 노드를 넘어야 하거나, 강한 전달 보장이 필요하거나, 메시지의 장기 보존이 중요하다면 Kafka가 적합하다.