백엔드 데이터 인프라 54편 — Redis Stream + Consumer Group + Kafka 비교

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 54편. Redis Stream — 추가 전용 로그와 Consumer Group. XADD·XREAD·XRANGE·XGROUP·XREADGROUP·XACK·XPENDING 명령어와 event sourcing·durable message queue 패턴, Pub/Sub·List·Kafka 와의 비교까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 54편 — Redis Stream + Consumer Group + 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 CREATEXREADGROUP → 처리 → 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 를 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!