fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • langgraph 페이지로 이동
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code 메모리: CLAUDE.md와 .claude/rules를 규칙으로 쓰는 법
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • Docling — IBM Research 의 문서 파싱 toolkit 상세 정리
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
    • 스킬 문서를 신경망처럼 학습시킨다 — Microsoft SkillOpt 분석
  • ai 페이지로 이동
    • agent 페이지로 이동
    • [초안] AI 제품 백엔드 안정성 — 지연·비용·권한·관측·도구 실패·폴백/재시도/사람 에스컬레이션
    • [초안] LLM 평가 프레임워크: 골든셋, 회귀 테스트, LLM-as-a-judge, 사람 피드백 루프
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • apartment 페이지로 이동
    • 구리 럭키아파트 24평 인테리어 레퍼런스 모음
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] CJ푸드빌 디지털 채널 면접: 슬롯 도메인 경험을 커머스 도메인 설계 능력으로 번역하기
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 도메인 모델링: 주문·재고·노출의 세 축을 분리해서 설계하기
    • 커머스 주문 상태와 데이터 정합성 기본기 — CJ푸드빌 면접 대비
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리 — CJ푸드빌 디지털 채널 백엔드 면접 대비
    • [초안] F&B 주문/매장/픽업 상태머신 설계 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • [초안] DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
    • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
  • finance 페이지로 이동
    • industry-cycle 페이지로 이동
    • investing 페이지로 이동
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS는 어떻게 안전한가 — TLS, 인증서, 그리고 termination
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • [초안] 커머스/F&B 면접 답변집 — 슬롯 도메인 경험을 주문·결제·쿠폰·매장 설계로 매핑하기
    • [초안] F&B / e-Commerce 운영 모니터링과 장애 대응 인터뷰 정리
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS 가 안 줄어드는 이유 — gc.collect 의 한계와 malloc_trim
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
    • [초안] Spring Security 6.x OAuth2 + JWT 상용 인증 설계 — Grant 선택, Resource Server, Refresh Rotation, 로그아웃
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/database/[초안] Redis Streams 소비자 그…
db

[초안] Redis Streams 소비자 그룹 신뢰성 — PEL, 재할당, 멱등성까지

> 이 문서는 Redis Streams의 소비자 그룹(Consumer Group)이 어떻게 메시지 유실 없이 분산 처리를 보장하는가를 운영·장애 관점에서 정리한다. Stream의 기본 명령어와 Pub/Sub과의 비교는 pub-sub.md에 이미 있으므로, 본 문서는 그 위에서 한 단계 더 들어간다 — 소비자가 죽었을 때 메시지는 어디에 남고, 누가 다시 처...

2026.06.11·7 min read·5 views

이 문서는 Redis Streams의 소비자 그룹(Consumer Group)이 어떻게 메시지 유실 없이 분산 처리를 보장하는가를 운영·장애 관점에서 정리한다. Stream의 기본 명령어와 Pub/Sub과의 비교는 pub-sub.md에 이미 있으므로, 본 문서는 그 위에서 한 단계 더 들어간다 — 소비자가 죽었을 때 메시지는 어디에 남고, 누가 다시 처리하며, 중복을 어떻게 막는가. 결론부터 말하면 Streams는 "메시지를 영속한다"가 아니라 at-least-once + PEL(Pending Entries List) + 명시적 ACK라는 세 부품의 조합으로 신뢰성을 만든다. 이 세 가지를 분리해서 설명할 수 있어야 한다.

학습 목표는 다음 세 가지다.

  • XREADGROUP이 메시지를 PEL에 등록하는 순간과 XACK로 비우는 순간의 의미를 구분한다.
  • 소비자가 죽은 뒤 남은 메시지를 XPENDING → XCLAIM / XAUTOCLAIM으로 회수하는 흐름을 설계한다.
  • at-least-once 전달 위에서 멱등 소비(idempotent consumer)와 독약 메시지(poison message) 처리를 어떻게 얹는지 판단한다.

왜 소비자 그룹이 신뢰성의 핵심인가

단일 소비자가 XREAD로 스트림을 읽는 것만으로는 신뢰성이 없다. 읽고 나서 처리 중에 프로세스가 죽으면 그 메시지가 처리됐는지 아무도 모른다. 다시 읽으려면 마지막으로 읽은 ID를 소비자가 직접 어딘가에 저장해야 하는데, 그 저장 자체가 또 하나의 장애 지점이 된다.

소비자 그룹은 이 "어디까지 읽었나"와 "무엇을 아직 처리 못 했나"를 서버 측 상태로 들고 있다. 그래서 소비자가 재시작해도 자기가 받았지만 ACK하지 않은 메시지를 그대로 다시 받을 수 있다. Kafka의 컨슈머 그룹 + 오프셋 커밋과 같은 역할이지만, Redis는 오프셋 하나가 아니라 **개별 메시지 단위의 미처리 목록(PEL)**을 들고 있다는 점이 결정적으로 다르다.


핵심 작동 원리: PEL과 두 개의 시점

소비자 그룹의 신뢰성은 단 두 개의 시점으로 요약된다.

bash
# 시점 1: XREADGROUP — 메시지를 받는 순간, PEL에 등록된다
XREADGROUP GROUP workers consumer-1 COUNT 10 STREAMS orders >
 
# 시점 2: XACK — 처리 완료를 알리는 순간, PEL에서 제거된다
XACK orders workers 1711500000000-0

> 는 "이 그룹의 누구에게도 아직 전달되지 않은 새 메시지"를 뜻한다. 이 호출이 성공하면 해당 메시지는 그 소비자 이름으로 PEL에 기록된다. PEL은 그룹별로 유지되는 "전달은 됐지만 아직 ACK 안 된 메시지" 목록이다.

여기서 가장 자주 틀리는 부분 — XACK를 호출하기 전까지 메시지는 PEL에 영원히 남는다. 소비자가 처리 도중 죽어도 메시지는 사라지지 않는다. 다시 살아난 소비자는 > 대신 자기 ID 범위를 지정해 자기 PEL을 다시 읽을 수 있다.

bash
# 0 부터 읽으면 = 내가 받았지만 아직 ACK 안 한 메시지를 다시 가져온다 (재처리)
XREADGROUP GROUP workers consumer-1 COUNT 10 STREAMS orders 0

>는 새 메시지, 0(또는 특정 ID)은 자기 PEL 재조회. 이 둘을 섞으면 안 된다.

ACK를 처리 전에 할 것인가, 후에 할 것인가

이게 전달 시멘틱을 결정한다.

  • 처리 후 ACK (권장 기본값): 처리 성공이 확인된 다음 XACK. 처리 중 죽으면 PEL에 남아 재처리된다 → at-least-once. 중복이 발생할 수 있으므로 멱등성이 필수다.
  • 처리 전 ACK: 받자마자 XACK 후 처리. 처리 중 죽으면 메시지는 영영 사라진다 → at-most-once. 유실을 감수하는 게 맞는 비핵심 이벤트에만.

Redis Streams는 구조적으로 exactly-once를 보장하지 않는다. "정확히 한 번"은 at-least-once 전달 + 멱등 소비로 결과적으로 만드는 것이지, 브로커가 주는 게 아니다.


죽은 소비자의 메시지 회수: XPENDING과 XCLAIM

소비자 하나가 영영 돌아오지 않으면, 그 소비자의 PEL에 갇힌 메시지를 다른 소비자가 가져와야 한다. 이걸 자동으로 해주는 장치는 없다 — 직접 구현해야 하는 운영 책임이다.

bash
# 1. 그룹 전체의 미처리 현황 요약 (총 개수, 최소/최대 ID, 소비자별 분포)
XPENDING orders workers
 
# 2. 상세 — idle 시간이 60초(60000ms) 넘은 미처리 메시지 최대 10건
XPENDING orders workers IDLE 60000 - + 10
 
# 3. 특정 메시지를 consumer-2 소유로 강제 이전 (3600000ms 이상 idle인 것만)
XCLAIM orders workers consumer-2 3600000 1711500000000-0

XCLAIM의 idle 임계값이 안전장치다. "최소 N밀리초 동안 아무도 ACK 안 한 메시지만 뺏는다"는 조건이라, 아직 살아서 처리 중인 소비자의 메시지를 성급하게 빼앗지 않는다. 임계값을 너무 짧게 잡으면 느린 소비자의 메시지를 중복 처리하게 되고, 너무 길게 잡으면 장애 복구가 느려진다.

XAUTOCLAIM — 스캔과 클레임을 한 번에

Redis 6.2부터는 XPENDING으로 스캔하고 XCLAIM으로 옮기는 두 단계를 XAUTOCLAIM 하나로 합칠 수 있다.

bash
# idle 60초 넘은 미처리 메시지를 0번 커서부터 스캔해 consumer-2가 회수
XAUTOCLAIM orders workers consumer-2 60000 0 COUNT 10

반환값에 다음 커서가 포함되므로 커서를 이어가며 전체 PEL을 순회할 수 있다. 운영에서는 별도의 "회수 워커"가 주기적으로 XAUTOCLAIM을 돌려 고아 메시지를 흡수하게 만드는 패턴이 흔하다.


독약 메시지(poison message)와 데드레터

at-least-once의 그림자는 영원히 실패하는 메시지다. 처리할 때마다 예외가 터지는 메시지는 ACK되지 않으니 PEL에 남고, 회수 워커가 계속 다시 집어 처리를 시도한다 → 무한 재처리 루프.

XPENDING ... IDLE 상세 응답에는 각 메시지의 **전달 횟수(delivery count)**가 들어 있다. 이 값을 임계값으로 쓴다.

text
1) 1) "1711500000000-0"
   2) "consumer-1"
   3) (integer) 920000     # idle time (ms)
   4) (integer) 5          # delivery count — 5번째 전달

전달 횟수가 임계값(예: 5)을 넘으면 다음 중 하나를 선택한다.

  • 별도의 데드레터 스트림으로 XADD 후 원본은 XACK로 PEL에서 제거.
  • 알림을 띄우고 사람이 수동 개입할 때까지 격리.

Redis는 데드레터를 기본 제공하지 않으므로, "전달 횟수 임계값 + 데드레터 스트림 + 원본 ACK"를 직접 구성해야 한다. 이걸 빼먹으면 독약 메시지 하나가 회수 워커의 처리량을 통째로 갉아먹는다.


흔한 오해

  • "Stream에 넣으면 메시지가 안전하게 보관된다." → MAXLEN/MINID 트리밍이나 메모리 압박에 의한 제거는 ACK 여부를 보지 않는다. 아직 PEL에 남은 미처리 메시지도 트리밍으로 잘려나갈 수 있다. 보존 기간은 처리 SLA보다 넉넉해야 한다.
  • "소비자 그룹이 자동으로 재할당해 준다." → 아니다. 죽은 소비자의 PEL을 옮기는 것은 XCLAIM/XAUTOCLAIM을 호출하는 내 코드다. Kafka의 리밸런스 같은 자동 재분배는 없다.
  • "ACK는 처리 성공의 증거다." → XACK는 그저 PEL에서 빼는 명령일 뿐, 처리 결과를 검증하지 않는다. 처리 전에 ACK하면 그 메시지는 성공 여부와 무관하게 사라진다.
  • "consumer 이름이 많을수록 빨라진다." → PEL은 consumer 이름 단위로 쌓인다. 매번 랜덤 이름으로 접속하면 죽은 이름마다 고아 PEL이 남아 XINFO가 지저분해지고 회수 대상이 폭증한다. consumer 이름은 안정적으로 재사용한다.
  • "XLEN이 0이면 다 처리된 것이다." → XLEN은 스트림에 남은 엔트리 수일 뿐 PEL과 무관하다. 미처리 현황은 XPENDING과 XINFO GROUPS의 pending / lag로 본다.

설계·운영 체크포인트

  • 그룹 생성 시작점: XGROUP CREATE orders workers $는 "지금 이후 새 메시지부터", 0은 "맨 처음부터"다. 이미 쌓인 메시지를 처리할지 결정해 시작 ID를 고른다. 스트림이 없을 수 있으면 MKSTREAM 옵션을 붙인다.
  • 멱등 소비: at-least-once이므로 같은 메시지가 두 번 올 수 있다. 메시지 ID나 비즈니스 키를 처리 완료 집합(예: Redis Set, DB unique 제약)에 기록해 중복 처리를 무력화한다.
  • 회수 워커 분리: 정상 소비 경로(>)와 고아 회수 경로(XAUTOCLAIM)를 분리하면, 회수 로직 장애가 정상 처리량에 영향을 덜 준다.
  • PEL 모니터링: XINFO GROUPS orders의 pending(미처리 수)과 lag(아직 전달 안 된 수)를 지표로 수집한다. pending이 단조 증가하면 소비자가 처리를 못 따라가거나 ACK를 빠뜨리고 있다는 신호다.
  • 트리밍 안전 마진: XADD orders MAXLEN ~ 100000 * ...처럼 근사 트리밍(~)으로 성능을 확보하되, 보존량은 최대 처리 지연 + 회수 지연을 견딜 만큼 잡는다.
  • 단일 인스턴스 한계: Redis Streams는 한 키가 한 노드에 산다. 클러스터에서 처리량을 늘리려면 스트림 키 자체를 샤딩해야 하고, Kafka 수준의 파티션 재분배·복제 보장이 필요하면 Stream이 맞는 도구인지 다시 본다.

Spring Data Redis에서의 ACK 제어

Spring의 StreamMessageListenerContainer는 기본이 자동 ACK다. 신뢰성 있는 소비를 하려면 자동 ACK를 끄고 처리 성공 뒤 직접 acknowledge해야 한다.

java
StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
    StreamMessageListenerContainerOptions.builder()
        .pollTimeout(Duration.ofSeconds(1))
        .build();
 
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container =
    StreamMessageListenerContainer.create(connectionFactory, options);
 
// autoAck = false 로 구독 → 처리 성공 후에만 명시적 ACK
container.receive(
    Consumer.from("workers", "consumer-1"),
    StreamOffset.create("orders", ReadOffset.lastConsumed()),
    message -> {
        try {
            handle(message.getValue());                       // 비즈니스 처리
            redisTemplate.opsForStream()
                .acknowledge("orders", "workers", message.getId());  // 성공 후 ACK
        } catch (Exception e) {
            // ACK하지 않음 → PEL에 남아 회수 대상이 된다
            log.warn("처리 실패, 재처리 대기: {}", message.getId(), e);
        }
    });
 
container.start();

receiveAutoAck(...)을 쓰면 받는 즉시 ACK되어 at-most-once가 된다. 핵심 이벤트라면 위처럼 receive(...) + 명시 ACK를 쓴다.


직접 해보기

로컬 Redis로 장애 시나리오를 재현해 본다.

bash
# 1. 스트림에 메시지 적재 + 그룹 생성
redis-cli XADD orders '*' userId 1001 amount 50000
redis-cli XGROUP CREATE orders workers 0
 
# 2. consumer-1이 읽기만 하고 ACK 안 함 (장애 흉내)
redis-cli XREADGROUP GROUP workers consumer-1 COUNT 1 STREAMS orders '>'
 
# 3. PEL에 갇힌 것을 확인
redis-cli XPENDING orders workers
 
# 4. idle 0ms 기준으로 consumer-2가 회수
redis-cli XAUTOCLAIM orders workers consumer-2 0 0
 
# 5. 처리 완료 가정 후 ACK → PEL 비워짐 확인
redis-cli XACK orders workers <message-id>
redis-cli XPENDING orders workers

점검 질문

스스로 1분 안에 답할 수 있는지 확인한다.

  1. XREADGROUP에서 >와 0의 차이는 무엇이고, 각각 언제 쓰는가?
  2. 소비자가 처리 도중 죽으면 그 메시지는 어디에 남고, 누가 어떻게 다시 처리하는가?
  3. Redis Streams가 exactly-once를 보장하지 못하는 이유와, 그럼에도 중복 없는 결과를 만드는 방법은?
  4. 영원히 실패하는 메시지를 어떻게 감지하고 격리하는가?
  5. PEL에 미처리 메시지가 남아 있어도 트리밍으로 유실될 수 있는 이유는?

함께 보면 좋은 문서

  • pub-sub.md — Pub/Sub와 Stream 기본 명령어, 전달 시멘틱 비교
  • pub-sub-patterns.md — Pub/Sub 실전 패턴과 메시지 큐 경계
  • ../../kafka/message-delivery-semantics.md — at-least-once / exactly-once를 Kafka 관점에서 비교
on this page
  • 01왜 소비자 그룹이 신뢰성의 핵심인가
  • 02핵심 작동 원리: PEL과 두 개의 시점
  • ACK를 처리 전에 할 것인가, 후에 할 것인가
  • 03죽은 소비자의 메시지 회수: XPENDING과 XCLAIM
  • XAUTOCLAIM — 스캔과 클레임을 한 번에
  • 04독약 메시지(poison message)와 데드레터
  • 05흔한 오해
  • 06설계·운영 체크포인트
  • 07Spring Data Redis에서의 ACK 제어
  • 08직접 해보기
  • 09점검 질문
  • 10함께 보면 좋은 문서

이런 글도

  • [초안] MySQL 옵티마이저 힌트 — 인덱스 힌트와 optimizer hint로 실행 계획을 다루는 법
    이 문서는 MySQL이 고른 실행 계획이 마음에 들지 않을 때, 무엇을 어떻게 강제할 수 있는지를 정리한 학습 가이드다. 결론부터 말하면 힌트는 두 계열로 나뉜다. - 인덱스 힌트(USE / FORCE / IGNORE INDEX) — 오래된 문법, 인덱스 후보 집합만 손댄다. - 옵티마이저 힌트(/+ ... /) — MySQL 5.7+ 문법, 조인 방식·접...
    🗄️ db
    db
    2026.06.11
  • [초안] Redis Pub/Sub 패턴 심화 — 실전 활용과 메시지 큐와의 경계
    > 이 문서는 Redis Pub/Sub의 동작 원리와 실전 패턴(캐시 무효화, 실시간 이벤트 전파, 세션 클러스터링)을 백엔드 면접 관점에서 정리한다. Pub/Sub과 Stream의 비교는 pub-sub.md에 이미 있으므로 본 문서는 Pub/Sub 단일 채널을 패턴 수준에서 어떻게 쓰는가에 집중하고, Kafka·RabbitMQ와의 선택 기준까지 다룬다....
    🗄️ db
    db
    2026.05.19
  • [초안] MySQL 복제와 페일오버 심화: 운영 관점 deep-dive
    > 이 문서는 MySQL 복제와 샤딩의 후속 deep-dive다. binlog 포맷, GTID 개요, replica lag 원인 같은 기본 개념은 그 hub 문서에서 다루고, 여기서는 장애 시 어떻게 primary가 바뀌고 트래픽이 끊김 없이 이어지는가라는 한 가지 축만 깊게 본다. 읽기 부하 분산은 인덱스 + read replica + @Transacti...
    🗄️ db
    db
    2026.05.16
  • [초안] MySQL 옵티마이저와 실행 계획 생성 — 비용 모델·통계·optimizer_trace 실전 가이드
    대부분의 백엔드 개발자는 EXPLAIN 출력을 읽는 법은 알지만, 그 출력을 만들어내는 옵티마이저가 어떻게 동작하는지는 모른다. 면접에서 "왜 인덱스가 있는데 안 타죠?", "조인 순서는 누가 결정하나요?", "옵티마이저가 잘못된 선택을 할 때 어떻게 강제하나요?" 같은 질문을 받으면 막힌다. 옵티마이저는 SQL 한 문장을 수십\수백 개의 후보 실행 계획으...
    🗄️ db
    db
    2026.05.16

댓글 (0)