Engineering Note
Redis에도 Consumer Group이 있다: Streams를 캐시 너머로 보기
Redis Streams를 key-value 캐시 너머의 append-only event log, consumer group, pending message 처리 관점에서 살펴봅니다.
Redis라고 하면 보통 GET, SET, TTL, cache aside, distributed lock 같은 장면을 먼저 떠올립니다.
그 인상은 틀리지 않습니다. Redis는 빠른 key-value 저장소로 많이 쓰이고, 캐시 용도로 특히 강합니다. 그런데 Redis를 캐시로만 기억하면 놓치는 기능이 있습니다.
Redis에는 Streams가 있습니다.
이름 그대로 stream은 순서가 있는 event log처럼 동작합니다. producer는 stream에 entry를 추가하고, consumer는 그 entry를 읽습니다. 여기까지는 흔한 queue처럼 보입니다. 하지만 Redis Streams는 consumer group, pending entries, ack, claim 같은 메시지 처리에 필요한 꽤 중요한 개념도 함께 제공합니다.
그래서 질문을 바꿔볼 수 있습니다.
Redis는 캐시만 하는 도구일까?
아니면 작은 이벤트 파이프라인도 맡길 수 있을까?
이 글은 두 번째 질문에 대한 실무적인 답을 정리합니다.
먼저 가져갈 결론
- Redis Streams는 Redis 안에 있는 append-only event log에 가깝습니다.
XADD로 entry를 추가하고,XREAD또는XREADGROUP으로 읽습니다.- consumer group을 쓰면 여러 worker가 같은 stream을 나눠 처리할 수 있습니다.
XACK전까지 읽은 메시지는 pending 상태로 남습니다.- pending message는
XPENDING,XCLAIM,XAUTOCLAIM으로 확인하고 회수할 수 있습니다. - 작은 비동기 작업 큐, 내부 이벤트 처리, 짧은 이벤트 이력에는 좋은 선택지가 될 수 있습니다.
- Kafka나 RabbitMQ를 항상 대체하는 도구는 아닙니다.
핵심은 Redis Streams를 “캐시 기능의 부록”으로 보지 않는 것입니다.
Redis key-value = 현재 상태를 빠르게 읽고 쓰기
Redis Streams = 시간 순서대로 발생한 이벤트를 쌓고 처리하기
둘은 같은 Redis 안에 있지만 바라보는 문제가 다릅니다.
Stream은 무엇을 저장하나
Redis Stream은 entry들의 목록입니다. 각 entry에는 ID가 있고, field-value 쌍이 들어갑니다.
예를 들어 주문 후처리 이벤트를 넣는다면 이런 형태가 됩니다.
XADD order-events * orderId 1001 type paid amount 49000
여기서 *는 Redis가 stream entry ID를 생성하게 하겠다는 뜻입니다. 생성된 ID는 보통 시간 기반 형태를 갖습니다.
읽을 때는 XREAD로 새 entry를 가져올 수 있습니다.
XREAD BLOCK 5000 STREAMS order-events $
이 관점에서 stream은 단순한 queue보다 event log에 가깝습니다. entry는 stream에 남아 있고, 범위 조회도 가능합니다.
XRANGE order-events - +
물론 Redis는 메모리 기반 시스템입니다. stream이 계속 커지면 메모리도 계속 씁니다. 그래서 운영에서는 trimming 정책을 함께 설계해야 합니다.
XTRIM order-events MAXLEN ~ 100000
stream을 쓰기 시작하는 순간, “무엇을 얼마나 오래 보관할 것인가”가 설계 질문이 됩니다.
List, Pub/Sub, Streams는 어떻게 다른가
Redis에는 Streams 이전에도 메시징처럼 쓸 수 있는 기능이 있었습니다. 대표적으로 List와 Pub/Sub입니다.
List는 queue처럼 쓰기 쉽습니다.
LPUSH jobs payload
BRPOP jobs 0
단순 작업 큐라면 여전히 유용합니다. 하지만 여러 worker가 읽고, 처리 중 죽은 메시지를 추적하고, 재처리하는 구조를 만들려면 애플리케이션 코드가 많이 필요합니다.
Pub/Sub은 실시간 fan-out에 가깝습니다.
PUBLISH events payload
SUBSCRIBE events
가볍고 빠르지만, 구독자가 없을 때 발행된 메시지는 나중에 다시 읽을 수 없습니다. “지금 듣고 있는 사람에게만 전달”하는 모델입니다.
Streams는 그 사이에서 다른 선택지를 줍니다.
| 기능 | List | Pub/Sub | Streams |
|---|---|---|---|
| 메시지 저장 | 가능 | 약함 | 가능 |
| 여러 worker 분산 | 직접 설계 | subscriber별 | consumer group 지원 |
| 처리 완료 추적 | 직접 설계 | 없음 | XACK |
| 미처리 메시지 확인 | 직접 설계 | 없음 | pending entries |
| replay/range 조회 | 제한적 | 없음 | XRANGE, ID 기반 조회 |
Streams는 Redis 안에서 “저장되는 이벤트 흐름”을 다루고 싶을 때 훨씬 자연스럽습니다.
Consumer Group이 들어오면 달라지는 것
Redis Streams를 흥미롭게 만드는 부분은 consumer group입니다.
consumer group을 만들면 여러 consumer가 같은 stream을 나눠 읽을 수 있습니다.
XGROUP CREATE order-events workers $
그 다음 각 worker는 group 안의 consumer 이름을 가지고 메시지를 읽습니다.
XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS order-events >
마지막의 >는 “이 group에 아직 전달되지 않은 새 메시지”를 달라는 의미입니다.
처리가 끝나면 ack를 보냅니다.
XACK order-events workers 1718610000000-0
이 흐름에서 중요한 점은 “읽음”과 “처리 완료”가 다르다는 것입니다.
XREADGROUP으로 읽었다
-> worker에게 전달되었다
-> 아직 성공적으로 처리되었다는 뜻은 아니다
XACK을 보냈다
-> 처리 완료로 인정한다
이 차이가 pending entries를 만듭니다.
Pending Entries List가 중요한 이유
consumer가 메시지를 읽었지만 XACK을 보내기 전에 죽으면 어떻게 될까요?
Redis Streams는 그 메시지를 바로 잊지 않습니다. consumer group의 Pending Entries List, 줄여서 PEL에 남깁니다.
운영자는 pending 상태를 확인할 수 있습니다.
XPENDING order-events workers
특정 consumer가 오래 붙잡고 있는 메시지도 볼 수 있습니다.
XPENDING order-events workers - + 10 worker-1
오래 pending 상태인 메시지는 다른 consumer가 회수할 수 있습니다.
XAUTOCLAIM order-events workers worker-2 60000 0-0 COUNT 10
이 명령은 일정 시간 이상 idle 상태인 pending 메시지를 찾아 worker-2가 다시 소유하도록 만듭니다.
이 구조 덕분에 Redis Streams는 “worker가 읽고 죽으면 메시지가 사라진다”보다 한 단계 나은 운영 모델을 제공합니다.
하지만 이것이 정확히 한 번 처리된다는 뜻은 아닙니다. 재시도와 claim이 들어가면 같은 메시지가 다시 처리될 수 있습니다. 따라서 handler는 idempotent하게 설계해야 합니다.
at-least-once에 가깝게 생각한다
중복 처리는 애플리케이션이 견딘다
처리 완료 후 XACK한다
작은 이벤트 파이프라인 예시
예를 들어 주문 API가 결제 성공 후 여러 후처리를 해야 한다고 해보겠습니다.
- 알림 보내기
- 포인트 적립
- 추천 모델 업데이트
- 내부 감사 이벤트 기록
요청 경로에서 이 모든 일을 직접 처리하면 API latency가 길어집니다. Redis Streams를 쓰면 요청 경로는 이벤트를 남기고 빠르게 끝낼 수 있습니다.
Order API
-> XADD order-events ...
-> response
Worker group
-> XREADGROUP
-> process
-> XACK
이때 Redis Streams는 다음을 제공합니다.
- 이벤트 순서
- 여러 worker로 처리 분산
- 처리 완료 ack
- pending message 확인
- worker 장애 후 재처리
작은 서비스 내부의 비동기 후처리라면 이 정도로도 충분히 실용적일 수 있습니다.
언제 Redis Streams가 잘 맞나
Redis Streams는 이런 상황에서 특히 괜찮습니다.
- 이미 Redis를 운영하고 있습니다.
- 별도 Kafka나 RabbitMQ를 붙이기엔 시스템이 작습니다.
- 이벤트 보관 기간이 짧습니다.
- 처리량 요구가 Redis 한계 안에 있습니다.
- consumer group 기반의 worker 분산이 필요합니다.
- 재처리와 중복 처리를 애플리케이션에서 제어할 수 있습니다.
예를 들면:
- 웹훅 발송 큐
- 알림 발송 큐
- 이미지/문서 후처리 작업
- 내부 도메인 이벤트 fan-out
- 짧은 audit trail
- 실시간 이벤트를 잠깐 버퍼링하는 구조
핵심은 “Redis를 이미 신뢰하고 운영하고 있는가”입니다. 이미 Redis가 운영 표준 안에 있다면 Streams는 작은 이벤트 처리에 매우 빠르게 도입할 수 있습니다.
언제 다른 도구가 더 나은가
Redis Streams가 Kafka를 대체한다고 말하면 과합니다.
다음 조건에서는 Kafka, RabbitMQ, managed queue가 더 자연스러울 수 있습니다.
- 장기간 이벤트 보관이 중요합니다.
- 여러 팀이 공유하는 event platform이 필요합니다.
- topic partitioning, replay, schema governance가 중요합니다.
- 매우 높은 처리량과 장기 확장성이 필요합니다.
- 메시지 라우팅, dead letter exchange, 복잡한 delivery semantics가 필요합니다.
- Redis memory와 persistence 설정만으로는 위험이 큽니다.
Redis Streams는 “작고 빠른 이벤트 파이프라인”에는 좋지만, 조직 전체의 이벤트 기반 아키텍처를 책임지는 중심축으로 쓰기에는 별도 검토가 필요합니다.
운영할 때 꼭 봐야 할 것
Redis Streams를 운영에 넣는다면 기능 사용법보다 운영 기준이 더 중요합니다.
첫째, stream 길이를 관리해야 합니다.
XINFO STREAM order-events
XLEN과 memory 사용량을 보고, XTRIM 또는 MAXLEN 전략을 정해야 합니다.
둘째, pending이 쌓이는지 봐야 합니다.
XINFO GROUPS order-events
XPENDING order-events workers
pending이 계속 늘어난다면 worker가 느리거나, 실패가 반복되거나, ack가 누락되고 있을 수 있습니다.
셋째, poison message를 설계해야 합니다. 같은 메시지가 계속 실패하면 영원히 pending과 claim 사이를 돌 수 있습니다. retry count, dead-letter stream, 실패 이벤트 기록 같은 정책이 필요합니다.
넷째, idempotency가 필요합니다. worker가 같은 entry를 두 번 처리해도 주문 상태나 포인트가 깨지지 않아야 합니다.
다섯째, Redis persistence를 이해해야 합니다. RDB, AOF, replication, failover 설정에 따라 장애 시 손실 가능성과 복구 지점이 달라집니다. 캐시로만 쓸 때보다 훨씬 민감한 선택입니다.
이런 이유로 Redis Streams를 운영에 넣으면 단순한 up/down 모니터링만으로는 부족합니다. stream 길이, consumer group, pending message, 처리 지연, Redis 상태 이상을 함께 봐야 합니다. Pletor의 Konduo는 Redis plugin을 통해 Redis Stream과 consumer 상태를 확인하고, Redis 이상 징후를 진단하며, 필요한 장애 조치 흐름을 운영 화면 안에서 다룰 수 있도록 설계되어 있습니다.
캐시 너머의 Redis
Redis Streams를 알게 되면 Redis의 인상이 조금 달라집니다.
Redis는 단순히 “빠른 key-value cache”가 아닙니다. 적어도 작은 범위에서는 이벤트를 쌓고, 여러 worker가 나눠 처리하고, 처리 완료와 미처리 상태를 추적하는 도구가 될 수 있습니다.
물론 모든 이벤트 처리를 Redis Streams로 해결하자는 뜻은 아닙니다. Kafka가 필요한 문제는 Kafka가 더 잘 풉니다. RabbitMQ가 자연스러운 문제도 있습니다. 단순 queue가 충분한 경우도 많습니다.
다만 Redis를 이미 운영하고 있고, 작은 비동기 이벤트 파이프라인이 필요하다면 질문해볼 만합니다.
이 작업은 정말 Kafka까지 필요한가?
이미 있는 Redis Streams로 충분한가?
Redis Streams는 캐시 너머의 Redis를 보여주는 좋은 기능입니다.
함께 읽기 좋은 글
이 글의 관점과 이어지는 주제를 더 보려면 아래 글도 함께 읽어볼 수 있습니다.
- Consumer Group만으로는 Queue가 되지 않는다: Kafka Share Group의 의미 - Kafka share group이 기존 consumer group과 어떻게 다른지 정리합니다.
- Consumer Group인데 Consumer가 아니다: Kafka 안에서 노드들이 서로를 찾는 법 - consumer group을 소비가 아닌 노드 coordination 관점에서 봅니다.
- AI 애플리케이션에서 Redis가 맡는 다섯 가지 역할 - AI 애플리케이션에서 Redis가 맡는 여러 역할을 정리합니다.