백엔드 데이터 인프라 54편. Redis Stream — 추가 전용 로그와 Consumer Group. XADD·XREAD·XRANGE·XGROUP·XREADGROUP·XACK·XPENDING 명령어와 event sourcing·durable message queue 패턴, Pub/Sub·List·Kafka 와의 비교까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 54편이에요. 53편 으로 Sorted Set 의 랭킹·rate limiter 패턴까지 잡았다면, 이번 54편은 Redis Stream — 추가 전용 로그와 Consumer Group(한 메시지를 여러 워커가 나눠 받는 소비자 묶음). Redis 자료구조 중에서 가장 Kafka 와 닮은 영역이고, 경량 메시지 브로커 가 필요한 자리에서 강력해요. 자주 쓰는 6종 데이터 타입(49~54편)의 마지막 편.
Redis Stream이 어렵게 느껴지는 이유
Stream 은 Redis 5.0 (2018) 에 추가된 비교적 새로운 자료구조라 처음 보면 두 가지가 헷갈려요.
첫째, Pub/Sub(발행/구독)·List 와 어떻게 다른지가 안 잡혀요. "메시지 전달" 이라는 목적은 셋 다 비슷한데 영속성·재처리·다중 소비자 같은 보장 수준이 달라서, 어느 상황에 어느 것을 골라야 할지 처음에는 명확하지 않아요.
둘째, Consumer Group 모델이 처음 보면 복잡해요. XADD → XREADGROUP → XACK → XPENDING → XAUTOCLAIM 의 흐름에서 PEL(Pending Entries List) 같은 새 개념이 등장하고, "왜 ACK 가 필요한가"·"PEL 은 누가 비우나" 가 헷갈리죠.
이 글에서 우선 Stream vs Pub/Sub vs List 의 차이를 명확히 잡고, Consumer Group 모델을 차근차근 풀어요. 마지막에 Kafka 와 어떻게 닮고 다른가 를 비교하면서 실무 선택의 기준도 같이 잡고요.
Stream 기본 — Append-Only Log
XADD — 항목 추가
> XADD orders:stream * order_id 1001 amount 50000 user 42
"1747475123456-0"
핵심 동작:
*= ID 자동 생성 (밀리초 timestamp + 시퀀스)- ID 형식 =
<milliseconds>-<sequence>(예:1747475123456-0) - 항목 본문 = field-value 쌍 여러 개 (Hash 와 비슷)
- 반환값 = 부여된 ID
시간 순서 보장 — ID 가 자동 증가하므로 추가된 순서 = ID 순서. 다중 클라이언트가 동시에 XADD 해도 같은 timestamp 면 sequence 가 증가.
XLEN — 항목 수
> XLEN orders:stream
(integer) 1
O(1).
XRANGE / XREVRANGE — 범위 조회
> XRANGE orders:stream - + # 전체
> XRANGE orders:stream 1747475000000 + # 특정 시각 이후
- = 가장 작은 ID, + = 가장 큰 ID. 시간 기반 쿼리가 자연스러워요.
XREAD — 읽기 (non-consumer-group)
> XREAD COUNT 10 STREAMS orders:stream 0
> XREAD BLOCK 0 STREAMS orders:stream $ # blocking, 새 항목만
0= 처음부터 모든 항목$= 현재 시점 이후 새로 추가되는 항목만 (tail follow, 새 메시지만 따라가기)BLOCK 0= 새 항목 들어올 때까지 영원히 대기
여기까지가 단일 소비자 모델이에요. 여러 소비자가 같은 메시지를 각자 한 번씩 받으면 fan-out(같은 메시지를 N명에게 복제 전달)이고, 부하를 나눠 받으면 Consumer Group.
Stream vs Pub/Sub vs List — 정확한 비교
여기서 시험 함정이 하나 있어요 — 셋 다 메시지 전달 도구인데 보장 수준 이 완전 달라요.
Pub/Sub — Fire-and-Forget (휘발)
> PUBLISH news "breaking!"
(integer) 2 # 2명의 구독자에게 즉시 전달
영속성이 없어서 지금 구독 중인 사람한테만 메시지가 가요. 구독을 안 했거나 연결이 끊긴 사람은 메시지를 영원히 잃고, 한 번 보낸 건 재처리가 안 돼요. 대신 가장 단순하고 가장 빨라요.
언제 — "실시간 알림인데 놓쳐도 OK" — 채팅·이벤트 브로드캐스트.
List (LPUSH/BLPOP) — Persistent but Single Consumer
Redis persistence 설정에 따라 영속성은 챙길 수 있는데, 한 메시지가 한 명한테만 가요(LPOP 한 명이 가져가면 끝). LPOP 직후 워커가 죽으면 메시지가 손실되는 약점이 있어서 보통 LMOVE 패턴으로 보완해요. Consumer Group 은 없고, 단순한 작업 큐에는 이걸로 충분해요.
언제 — "단순 작업 큐, 1:1 처리" — 51편에서 풀어 본 producer-consumer 패턴.
Stream — Persistent + Multiple Consumers + Replay
영속성을 갖고, 과거 메시지를 영원히 (또는 MAXLEN 까지) 보존하기 때문에 재처리가 돼요. 여러 Consumer Group 이 각자 독립적으로 같은 메시지를 받고(한 메시지 = N 그룹 × 1명), 한 그룹 안에서는 1:N 워커로 부하 분산. ACK 와 PEL 로 at-least-once(최소 한 번 전달 보장)까지 챙기죠. 가장 풍부한 만큼 가장 복잡한 자료구조예요.
언제 — "메시지 손실 안 됨, 재처리 가능, 여러 컨슈머 그룹" — event sourcing(이벤트를 1급 데이터로 저장)·durable queue·Kafka 가 무거울 때.
한눈 비교표
| 항목 | Pub/Sub | List | Stream |
|---|---|---|---|
| 영속성 | X | ◯ | ◯ |
| 재처리 | X | △ (LMOVE 패턴) | ◯ |
| 다중 소비자 그룹 | △ (각자 SUBSCRIBE) | X | ◯ |
| 그룹 내 부하 분산 | X | △ (BLPOP 경합) | ◯ |
| ACK | X | X | ◯ |
| 워커 사망 시 복구 | X | △ (LMOVE) | ◯ (XPENDING + XAUTOCLAIM) |
| 메시지 ID | X | X | ◯ (시간 기반) |
| 복잡도 | 최소 | 중간 | 최대 |
| Kafka 와 위치 | 매우 다름 | 다름 | 가장 가까움 |
한 줄 정리 — Pub/Sub = 휘발 알림 / List = 단순 큐 / Stream = 보장형 큐 + 재처리.
Consumer Group — 4단계 흐름
여기가 Stream 의 가장 강력한 영역. 단계별로 풀어 가요.
1단계: 그룹 생성 (XGROUP CREATE)
> XGROUP CREATE orders:stream order-workers $ MKSTREAM
OK
order-workers= 그룹 이름$= 그룹 생성 시점 이후 항목부터 시작 (0으로 하면 처음부터)MKSTREAM= 스트림이 없으면 자동 생성
2단계: 그룹 멤버로 읽기 (XREADGROUP)
# 워커 1
> XREADGROUP GROUP order-workers worker-1 COUNT 10 STREAMS orders:stream >
# 워커 2 (같은 그룹 다른 워커)
> XREADGROUP GROUP order-workers worker-2 COUNT 10 STREAMS orders:stream >
>= 아직 그룹의 누구도 받지 않은 새 항목만- 그룹 안에서 부하 분산 — 항목 한 개는 한 워커한테만
- 읽은 항목은 PEL(Pending Entries List) 에 자동 등록 (아직 ACK 안 됨)
3단계: ACK (XACK)
> XACK orders:stream order-workers 1747475123456-0
(integer) 1
처리 완료 시 ACK. PEL 에서 제거 — "이 항목은 처리됐다" 신호. ACK 없이 두면 Pending 상태로 영원히 남아요 (메모리 증가).
4단계: 미처리 항목 처리 (XPENDING + XAUTOCLAIM)
# 그룹의 미ACK 항목 확인
> XPENDING orders:stream order-workers
1) (integer) 3 # 미ACK 개수
2) "1747475123456-0" # 가장 작은 미ACK ID
3) "1747475200000-0" # 가장 큰 미ACK ID
4) 1) 1) "worker-1"
2) "2" # worker-1 이 2개 처리 중
2) 1) "worker-2"
2) "1"
# 30초 이상 미ACK 항목을 worker-3 이 가져감
> XAUTOCLAIM orders:stream order-workers worker-3 30000 0-0 COUNT 10
XAUTOCLAIM 의 진가는 "워커가 죽었거나 처리 중 멈춰 30초 넘게 ACK 안 한 항목을 다른 워커가 가져가게" 해주는 거예요. at-least-once 보장의 핵심 메커니즘.
Python 예제
import redis
r = redis.Redis(decode_responses=True)
# 그룹 생성 (한 번만)
try:
r.xgroup_create("orders:stream", "order-workers", id="$", mkstream=True)
except redis.ResponseError:
pass # 이미 존재
# 워커 루프
while True:
msgs = r.xreadgroup(
"order-workers", "worker-1",
{"orders:stream": ">"},
count=10, block=5000
)
for stream, entries in (msgs or []):
for msg_id, fields in entries:
try:
process(fields)
r.xack("orders:stream", "order-workers", msg_id)
except:
pass # ACK 안 함 → 다른 워커가 XAUTOCLAIM 으로 회수
MAXLEN — 무한 증가 방지
Stream 은 추가만 하니 그냥 두면 무한 증가. 운영 시 필수 = MAXLEN.
> XADD orders:stream MAXLEN ~ 10000 * order_id 1001 amount 50000
MAXLEN 10000= 정확히 10,000개로 자름 (cost: 자주 trim 발생)MAXLEN ~ 10000= 근사 (approximate) — 약 10,000개 (cost 매우 저렴, 권장 기본값)MINID옵션 = ID 기반 자르기 (시간 기반 retention)
~ 옵션은 조금 더 보존되더라도 정확히 자르지 않아 성능 부담이 거의 없어요. 운영 환경 표준.
XTRIM 으로 나중에 자르기도 가능:
> XTRIM orders:stream MAXLEN ~ 10000
Redis Stream vs Kafka — 실무 선택의 기준
여기서 정말 중요한 자리. "Stream 이 Kafka 와 가깝다" 라고 했는데, 실제로 어느 쪽을 골라야 할까요?
닮은 점
Append-only log 구조 위에 시간 기반 ID를 얹고, Consumer Group 모델로 부하 분산과 다중 그룹을 같이 챙겨요. Pending/Acknowledge 로 at-least-once 를 보장하고, 과거 메시지 재처리도 둘 다 가능합니다.
다른 점 — Kafka 가 더 강한 영역
처리량부터 차이가 큽니다. Kafka 는 초당 수십~수백만 msg 를 받아내는데 Redis Stream 은 초당 수만~수십만 선이에요. 영속성 모델도 갈려요 — Kafka 는 디스크가 1급(Pull, 매우 큼)이고 Redis 는 메모리가 1급이라 성능·내구성 사이에서 트레이드오프가 있어요.
파티셔닝은 Kafka 가 토픽당 N 파티션을 자동 분산해주는데, Redis 는 키 분산을 직접 해야 해요. 데이터 보존 한계도 Kafka 는 TB 단위까지 가지만 Redis 는 메모리 한계 때문에 MAXLEN 이 필수. 생태계도 Kafka 는 Connect·Streams API·Schema Registry 까지 갖추고 있는데 Redis 는 그 정도로 풍부하지는 않아요. 분산 모델 자체도 Kafka 는 브로커 클러스터 기반의 본질적 분산인 반면, Redis 는 단일 인스턴스 또는 Cluster 입니다.
다른 점 — Redis Stream 이 더 강한 영역
대신 단순함은 Redis 가 압도적이에요. Redis 한 인스턴스로 끝나는데 Kafka 는 Zookeeper(또는 KRaft, Kafka 자체 메타데이터 관리자) + Broker + ZK quorum 운영 부담을 같이 안아야 해요. 지연 시간도 Redis 는 μs, Kafka 는 ms 라 둘 다 충분히 빠르지만 Redis 가 더 낮아요. 이미 Redis 가 인프라에 있다면 새 도구 도입 없이 큐를 얹을 수 있고, XADD·XREADGROUP·XACK 만 알면 개발이 시작되니 손에 익는 속도도 빠릅니다.
선택 가이드
소~중규모이면서 이미 Redis 가 있다면 Redis Stream 이 답이에요. 반대로 대규모에 처리량이 결정적이고 영구 데이터 보존까지 필요하면 Kafka. 이벤트 소싱으로 도메인 이벤트를 100% 보존해야 한다면 그것도 Kafka 쪽. 그냥 간단한 작업 큐라면 사실 List 로 충분하고, 보장이 필요한 만큼만 Stream 으로 올리면 됩니다.
여기까지 따라오셨다면 한 가지 의문이 들 거예요 — "그럼 Kafka 시리즈는 왜 51편?". 답은 Kafka 가 큰 데이터·복잡한 운영을 다룸 — 80~130편 (Part 5) 에서 깊이 풀어 가요. Stream 은 가볍게 시작할 때, Kafka 는 본격으로 갈 때.
시험 직전 한 번 더 — Redis Stream 함정 압축 노트
- Stream = 추가 전용 로그 (Redis 5.0+), Kafka 와 가장 가까운 자료구조
- 각 항목 = ID(
<ms>-<seq>) + field-value 쌍 여러 개 - ID 자동 =
*박으면 timestamp 기반 자동 부여 - 시간 순서 = 자동 보장 (ID 증가 순)
- XADD = 추가 (O(1) amortized)
- XREAD = 읽기,
$= tail follow (새 메시지만),BLOCK 0= 영원히 대기 - XRANGE = 범위 조회,
-= 시작,+= 끝 - XLEN = 항목 수 (O(1))
- Stream vs Pub/Sub vs List — 영속성·재처리·다중 소비자 그룹 차이
- Pub/Sub = 휘발 fire-and-forget
- List = 영속 + 1:1 단순 큐
- Stream = 영속 + 재처리 + Consumer Group + ACK
- Consumer Group 4단계 =
XGROUP CREATE→XREADGROUP→ 처리 →XACK XGROUP CREATE ... $= 생성 시점 이후 메시지만XREADGROUP ... >= 아직 그룹의 누구도 안 받은 메시지만- 읽은 항목은 자동으로 PEL(Pending Entries List) 에 등록
- ACK 안 하면 = PEL 에 영원히 남음 (메모리 증가)
XAUTOCLAIM= 워커 사망 시 미ACK 항목을 다른 워커로 회수 → at-least-once 보장XPENDING= 그룹의 미ACK 항목 확인 (모니터링·디버깅)- 무한 증가 방지 =
MAXLEN(정확) 또는MAXLEN ~(근사, 권장) MINID= ID 기반 자르기 (시간 retention)XTRIM= 나중에 자르기- Kafka 와 닮음 = log·Consumer Group·재처리·ACK
- Kafka 와 다름 = 처리량(Kafka 더 많음) · 분산(Kafka 본질적) · 운영 부담(Kafka 큼)
- Redis Stream 선택 = 소~중규모 + 이미 Redis 있음 + 단순함 우선
- Kafka 선택 = 대규모 + 처리량 결정적 + 영구 보존 필요
공식 문서: Redis Streams 에서 Stream 명령어 전체 reference 를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 49편 — Redis String 깊이 + 분산 락 패턴
- 50편 — Redis Hash + 객체 캐싱 패턴
- 51편 — Redis List + 큐·캡 리스트 패턴
- 52편 — Redis Set + 집합 연산 패턴
- 53편 — Redis Sorted Set + 랭킹·Sliding Window Rate Limiter 패턴
다음 글: