<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Idempotent Consumer on jyukki's Blog</title><link>https://jyukki.com/tags/idempotent-consumer/</link><description>Recent content in Idempotent Consumer on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Tue, 21 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/idempotent-consumer/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Transactional Inbox 패턴, 중복 소비와 재처리 비용을 줄이는 실전 기준</title><link>https://jyukki.com/learning/deep-dive/deep-dive-transactional-inbox-idempotent-consumer-playbook/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-transactional-inbox-idempotent-consumer-playbook/</guid><description>메시지 중복 소비와 재처리 상황에서 Transactional Inbox 패턴을 어떻게 적용하고, 언제 단순 멱등 처리로 충분한지 실무 기준으로 정리합니다.</description><content:encoded><![CDATA[<p>이벤트 기반 시스템을 운영하다 보면 결국 같은 질문으로 돌아옵니다. &ldquo;producer 쪽에서 outbox를 넣었는데 왜 consumer 쪽에서 또 사고가 나지?&rdquo; 이유는 간단합니다. <strong>Outbox는 발행의 일관성을 보장하고, Inbox는 소비의 일관성을 줄이는 장치</strong>이기 때문입니다. 브로커가 at-least-once 전달을 하는 순간, 장애 복구나 재처리 과정에서 중복 소비는 정상 동작의 일부가 됩니다. 문제는 많은 팀이 이 사실을 알면서도 consumer 쪽 중복 효과를 로직 곳곳에서 임시 if문으로 막다가, 결국 환불 중복, 포인트 두 번 적립, 상태 전이 꼬임 같은 비용을 치른다는 점입니다.</p>
<p>Transactional Inbox 패턴은 이 문제를 정면으로 다룹니다. 핵심은 메시지를 받자마자 비즈니스 로직부터 실행하는 것이 아니라, <strong>메시지의 처리 이력과 비즈니스 효과를 같은 트랜잭션 경계 안에서 묶는 것</strong>입니다. 이 구조를 잡아 두면 장애가 나도 &ldquo;이미 처리한 메시지인지&rdquo;, &ldquo;처리 중이던 메시지인지&rdquo;, &ldquo;재시도해도 되는 메시지인지&quot;를 판단하기 쉬워집니다. 이 글에서는 <a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">트랜잭션 아웃박스 + CDC</a>, <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>, <a href="/learning/deep-dive/deep-dive-kafka-retry-dlq/">Kafka Retry/DLQ 패턴</a>과 연결해서, consumer 쪽 일관성을 운영 기준으로 정리해 보겠습니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>Transactional Inbox가 단순 dedupe 테이블과 어떻게 다른지 설명할 수 있습니다.</li>
<li>언제는 단순 멱등 키 저장으로 충분하고, 언제는 별도 inbox 상태 머신이 필요한지 구분할 수 있습니다.</li>
<li>중복 소비, 순서 뒤집힘, 재처리, DLQ 복구까지 포함한 실무 의사결정 기준을 숫자 중심으로 잡을 수 있습니다.</li>
<li>inbox 테이블 스키마, 트랜잭션 경계, 보존 기간, 운영 지표를 한 번에 설계할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-outbox가-있어도-consumer-중복-효과는-사라지지-않는다">1) Outbox가 있어도 consumer 중복 효과는 사라지지 않는다</h3>
<p>Outbox를 넣으면 producer 입장에서는 &ldquo;DB 커밋은 됐는데 이벤트는 유실됨&rdquo; 같은 이중 쓰기 문제가 크게 줄어듭니다. 하지만 broker와 consumer 사이에는 여전히 아래 상황이 남습니다.</p>
<ul>
<li>consumer가 처리 완료 직후 offset commit 전에 죽음</li>
<li>broker 재전송으로 같은 메시지가 다시 도착함</li>
<li>운영자가 DLQ 메시지를 수동 재적재함</li>
<li>배치 재생성 또는 replay 작업으로 과거 이벤트를 다시 흘림</li>
</ul>
<p>이때 consumer 로직이 &ldquo;주문 적립금 지급&rdquo;, &ldquo;결제 상태 변경&rdquo;, &ldquo;외부 시스템 전송&rdquo; 같은 side effect를 만들면, 단순히 &ldquo;같은 이벤트가 한 번 더 왔다&quot;가 아니라 <strong>같은 업무 효과가 한 번 더 발생</strong>할 수 있습니다. 그래서 consumer 쪽에는 &ldquo;전달 보장&quot;보다 &ldquo;효과 보장&rdquo; 관점이 필요합니다.</p>
<p>여기서 핵심 질문은 하나입니다.</p>
<blockquote>
<p>이 메시지가 다시 와도 같은 결과로 닫히는가?</p></blockquote>
<p>닫히지 않는다면 inbox 계층을 별도로 두는 편이 맞습니다. 특히 금전, 재고, 상태 전이, 외부 API 호출처럼 복구 비용이 큰 영역은 <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>를 로직 단위로 흩뿌리기보다 inbox에서 한 번 묶는 편이 운영이 훨씬 단순해집니다.</p>
<h3 id="2-transactional-inbox는-읽음-표시가-아니라-처리-상태-계약이다">2) Transactional Inbox는 &ldquo;읽음 표시&quot;가 아니라 처리 상태 계약이다</h3>
<p>가장 흔한 오해는 inbox를 <code>message_id</code> 한 줄 저장하는 dedupe 테이블 정도로 보는 것입니다. 하지만 실무에서는 그 정도로는 부족한 경우가 많습니다. 이유는 장애 시점에 따라 상태가 세 갈래로 나뉘기 때문입니다.</p>
<ul>
<li>아직 처리 전</li>
<li>처리 중이었으나 완료 불명확</li>
<li>처리 완료</li>
</ul>
<p>그래서 inbox는 보통 아래 정보를 가집니다.</p>
<ul>
<li><code>message_id</code> 또는 <code>(producer_id, sequence_no)</code></li>
<li><code>aggregate_key</code> 또는 <code>business_key</code></li>
<li><code>status</code> (<code>RECEIVED</code>, <code>PROCESSING</code>, <code>DONE</code>, <code>FAILED</code>, <code>DEAD</code>)</li>
<li><code>processed_at</code></li>
<li><code>payload_hash</code></li>
<li><code>error_code</code>, <code>retry_count</code></li>
<li>필요하면 <code>result_snapshot</code> 또는 <code>effect_ref</code></li>
</ul>
<p>즉 inbox는 단순 중복 체크 저장소가 아니라 <strong>처리 상태 계약서</strong>입니다. 이 구조가 있어야 운영자는 &ldquo;같은 메시지라서 무시했는지&rdquo;, &ldquo;실패해서 재시도 대기인지&rdquo;, &ldquo;이미 효과가 반영됐는지&quot;를 로그가 아니라 데이터로 확인할 수 있습니다.</p>
<p>실무 기준으로는 아래 구분이 유용합니다.</p>
<ul>
<li><strong>단순 멱등 저장이면 충분한 경우</strong>: 조회성 집계, 캐시 갱신, 이미 upsert로 닫히는 작업</li>
<li><strong>상태 머신형 inbox가 필요한 경우</strong>: 금전 반영, 상태 전이, 외부 시스템 호출, 사람 승인 큐 적재</li>
</ul>
<p>후자라면 <code>DONE/FAILED/DEAD</code>를 명시하지 않으면 복구 시 판단이 매우 어려워집니다.</p>
<h3 id="3-트랜잭션-경계는-inbox-기록과-비즈니스-효과를-같이-묶어야-한다">3) 트랜잭션 경계는 &ldquo;inbox 기록&quot;과 &ldquo;비즈니스 효과&quot;를 같이 묶어야 한다</h3>
<p>Transactional Inbox의 핵심은 이름 그대로 <strong>같은 데이터 저장소 트랜잭션 안에서 inbox 업데이트와 도메인 변경을 같이 커밋</strong>하는 것입니다. 순서를 잘못 잡으면 dedupe는 됐는데 효과가 빠지거나, 효과는 반영됐는데 inbox는 미기록인 상태가 생깁니다.</p>
<p>권장 흐름은 아래와 같습니다.</p>
<ol>
<li><code>message_id</code> 기준으로 inbox row 조회 또는 생성</li>
<li>이미 <code>DONE</code>면 즉시 skip</li>
<li><code>PROCESSING</code> 또는 lease 만료 상태면 재진입 규칙 확인</li>
<li>비즈니스 로직 수행</li>
<li>도메인 변경과 inbox <code>DONE</code> 업데이트를 <strong>같은 트랜잭션</strong>으로 커밋</li>
<li>커밋 후 offset ack/commit</li>
</ol>
<p>이 순서가 중요한 이유는 ack를 먼저 하면 재처리 근거가 사라지고, 비즈니스 변경만 먼저 커밋하면 중복 방지 근거가 늦어지기 때문입니다.</p>
<p>숫자 기준도 필요합니다.</p>
<ul>
<li>메시지 처리 시간이 보통 200ms 이내면 <code>PROCESSING</code> lease는 <strong>3~5배 수준</strong>, 예를 들어 1초 전후로 시작</li>
<li>재시도 횟수는 일반 비즈니스 consumer 기준 <strong>3~5회</strong>에서 닫고, 그 이상은 DLQ 또는 운영 검토로 승격</li>
<li>같은 <code>message_id</code> 재도착 비율이 <strong>0.5% 이상</strong>이면 장애 복구나 commit 지연 구조를 의심</li>
<li><code>PROCESSING</code> 상태 체류가 p95 기준 <strong>평시의 2배 이상</strong> 늘면 consumer hang 또는 downstream 지연을 먼저 확인</li>
</ul>
<p>이 기준이 있어야 inbox가 단순 저장이 아니라 운영 판단 도구가 됩니다.</p>
<h3 id="4-순서-보장은-inbox가-해결하지-않는다-대신-피해를-줄인다">4) 순서 보장은 inbox가 해결하지 않는다, 대신 피해를 줄인다</h3>
<p>중복과 순서는 비슷해 보여도 다른 문제입니다. Transactional Inbox는 중복 효과 방지에는 강하지만, 메시지가 뒤집혀 도착했을 때 업무 의미까지 자동 보정해 주지는 않습니다. 예를 들어 <code>ORDER_CONFIRMED</code> 뒤에 <code>ORDER_CREATED</code>가 늦게 도착하면, 둘 다 중복이 아니므로 inbox만으로는 막기 어렵습니다.</p>
<p>그래서 순서 민감한 도메인은 추가 기준이 필요합니다.</p>
<ul>
<li>aggregate별 monotonic version 저장</li>
<li><code>event_time</code>보다 <code>version</code> 또는 <code>sequence_no</code> 우선 사용</li>
<li>늦게 온 이벤트는 drop, merge, 보류 중 어떤 정책인지 명확히 문서화</li>
</ul>
<p>실무에서는 아래 우선순위가 무난합니다.</p>
<ol>
<li>같은 aggregate에서 버전 비교 가능하면 버전 우선</li>
<li>버전이 없으면 상태 전이 허용표(state transition matrix) 사용</li>
<li>둘 다 없으면 inbox만으로 안전하지 않으므로 producer 계약 수정 우선</li>
</ol>
<p>즉 inbox는 <strong>중복 방지층</strong>이지 <strong>순서 복구 엔진</strong>은 아닙니다. 순서를 다뤄야 하면 <a href="/learning/deep-dive/deep-dive-event-schema-registry-compatibility-playbook/">이벤트 스키마 호환성</a>이나 <a href="/learning/deep-dive/deep-dive-kafka-idempotence-ordering/">Kafka 멱등성·순서 보장 설계</a>까지 같이 봐야 합니다.</p>
<h3 id="5-retention과-재처리-창을-안-정하면-inbox-테이블이-새-병목이-된다">5) retention과 재처리 창을 안 정하면 inbox 테이블이 새 병목이 된다</h3>
<p>inbox는 시간이 지나면 계속 커집니다. 그래서 보존 기간을 &ldquo;일단 오래&quot;로 잡는 팀이 많지만, 이 방식은 테이블 크기와 인덱스 비용을 빠르게 키웁니다. 반대로 너무 짧게 잡으면 늦은 재전송이나 수동 replay를 dedupe하지 못합니다.</p>
<p>보통 아래 기준이 실용적입니다.</p>
<ul>
<li>일반 주문/결제성 이벤트: <strong>7~30일</strong> 보존</li>
<li>정산/감사 중요 이벤트: <strong>30~90일</strong> 보존 후 아카이브</li>
<li>대규모 replay 가능성이 있는 도메인: replay window와 동일하거나 더 길게</li>
</ul>
<p>의사결정 기준은 &ldquo;메시지가 얼마나 늦게 다시 올 수 있는가&quot;와 &ldquo;중복 효과 비용이 얼마나 큰가&quot;입니다. 예를 들어 운영자가 최대 14일치 DLQ를 다시 흘릴 수 있다면, inbox dedupe window를 3일로 두는 건 거의 무의미합니다.</p>
<p>또한 inbox 테이블은 아래 조건이면 샤딩 또는 파티셔닝을 검토할 만합니다.</p>
<ul>
<li>일일 insert가 <strong>1천만 건 이상</strong></li>
<li><code>message_id</code> 조회 p95가 <strong>20ms 이상</strong>으로 상승</li>
<li>purge 작업이 write latency를 눈에 띄게 흔듦</li>
</ul>
<p>이 시점부터는 도메인별 inbox 분리, 날짜 파티션, TTL 아카이브 전략을 함께 봐야 합니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-가장-안전한-도입-순서">1) 가장 안전한 도입 순서</h3>
<p>처음부터 모든 consumer에 inbox를 붙이기보다, <strong>중복 효과 비용이 큰 상위 10~20% consumer부터</strong> 시작하는 편이 낫습니다.</p>
<ol>
<li>금전 반영, 상태 전이, 외부 API 호출 consumer 식별</li>
<li>현재 중복 사고 사례를 <code>중복 반영</code>, <code>순서 꼬임</code>, <code>재처리 불명확</code>으로 분류</li>
<li>inbox 스키마 도입 후 <code>DONE</code> skip만 먼저 적용</li>
<li>이후 <code>FAILED</code>, <code>DEAD</code>, <code>retry_count</code>를 추가해 운영 흐름 확장</li>
</ol>
<p>이 순서가 좋은 이유는 초기에 상태 머신을 너무 크게 설계하면 복잡도만 늘고, 반대로 dedupe만 넣으면 운영 가시성이 부족하기 때문입니다.</p>
<h3 id="2-최소-스키마-예시">2) 최소 스키마 예시</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> consumer_inbox (
</span></span><span style="display:flex;"><span>  message_id        <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">120</span>) <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>  consumer_name     <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">80</span>)  <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  business_key      <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">120</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  status            <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">20</span>)  <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  payload_hash      <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">128</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  retry_count       <span style="color:#8be9fd;font-style:italic">INT</span>          <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span> <span style="color:#ff79c6">DEFAULT</span> <span style="color:#bd93f9">0</span>,
</span></span><span style="display:flex;"><span>  processed_at      <span style="color:#ff79c6">TIMESTAMP</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  lease_until       <span style="color:#ff79c6">TIMESTAMP</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  error_code        <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">80</span>) <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  created_at        <span style="color:#ff79c6">TIMESTAMP</span>    <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span> <span style="color:#ff79c6">DEFAULT</span> <span style="color:#ff79c6">CURRENT_TIMESTAMP</span>,
</span></span><span style="display:flex;"><span>  updated_at        <span style="color:#ff79c6">TIMESTAMP</span>    <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span> <span style="color:#ff79c6">DEFAULT</span> <span style="color:#ff79c6">CURRENT_TIMESTAMP</span>
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_consumer_inbox_business_key <span style="color:#ff79c6">ON</span> consumer_inbox (consumer_name, business_key);
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_consumer_inbox_status_updated <span style="color:#ff79c6">ON</span> consumer_inbox (status, updated_at);
</span></span></code></pre></div><p>여기서 중요한 것은 <code>message_id</code>만이 아닙니다. <code>business_key</code>를 남겨야 운영자가 &ldquo;같은 주문에 어떤 이벤트가 몇 번 들어왔는지&quot;를 묶어서 볼 수 있습니다. <code>payload_hash</code>를 두면 같은 ID인데 payload가 달라진 이상 케이스도 탐지할 수 있습니다.</p>
<h3 id="3-운영-지표와-알람-기준">3) 운영 지표와 알람 기준</h3>
<p>최소 아래 지표는 대시보드에 올리는 편이 좋습니다.</p>
<ul>
<li><code>inbox_duplicate_skip_rate</code>: 중복으로 skip된 비율</li>
<li><code>inbox_processing_stuck</code>: <code>PROCESSING</code> 상태 장기 체류 건수</li>
<li><code>inbox_done_latency_p95</code>: 수신부터 <code>DONE</code>까지 걸린 시간</li>
<li><code>inbox_dead_total</code>: <code>DEAD</code> 상태 누적 건수</li>
<li><code>payload_hash_mismatch_total</code>: 같은 ID, 다른 payload 감지 건수</li>
</ul>
<p>권장 알람 기준 예시:</p>
<ul>
<li><code>inbox_processing_stuck &gt; 50</code>가 5분 이상 지속되면 경고</li>
<li><code>payload_hash_mismatch_total &gt; 0</code>이면 즉시 확인</li>
<li><code>duplicate_skip_rate</code>가 평시 대비 <strong>3배 이상</strong> 상승하면 broker 재전송 또는 consumer commit 이상 의심</li>
<li><code>done_latency_p95</code>가 SLO의 <strong>80% 이상</strong>을 10분 이상 점유하면 downstream 병목 점검</li>
</ul>
<h3 id="4-언제-inbox-대신-다른-해법이-나은가">4) 언제 inbox 대신 다른 해법이 나은가</h3>
<p>모든 문제를 inbox로 풀 필요는 없습니다.</p>
<ul>
<li>단순 upsert 집계면 DB <code>UPSERT</code>만으로 충분할 수 있습니다.</li>
<li>순서가 더 중요한 경우는 inbox보다 version check가 먼저입니다.</li>
<li>cross-service 보상 흐름이 핵심이면 <a href="/learning/deep-dive/deep-dive-distributed-transactions/">분산 트랜잭션/Saga</a>가 더 맞습니다.</li>
<li>producer와 consumer를 모두 통제할 수 있으면 아예 event contract를 바꾸는 것이 더 저렴할 수도 있습니다.</li>
</ul>
<p>실무 우선순위는 보통 이렇습니다.</p>
<ol>
<li>도메인 로직 자체가 자연 멱등인지 확인</li>
<li>안 되면 간단한 key dedupe로 닫히는지 확인</li>
<li>그래도 복구/운영이 어렵다면 Transactional Inbox 적용</li>
<li>순서/보상까지 얽히면 별도 상태 기계와 계약 수정 검토</li>
</ol>
<p>즉 inbox는 강력하지만, <strong>비용이 더 싼 단순화 경로를 먼저 배제한 뒤</strong> 들어가는 편이 맞습니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, inbox를 넣으면 write path가 하나 늘어납니다. TPS가 아주 높은 consumer에서는 DB write 부담과 인덱스 비용이 생깁니다. 그래서 정말 필요한 consumer부터 적용해야 합니다.</p>
<p>둘째, &ldquo;중복 skip&quot;만 믿고 순서 문제를 무시하면 더 큰 사고가 납니다. 상태 전이 도메인은 반드시 version 또는 transition rule이 필요합니다.</p>
<p>셋째, inbox 상태를 남기되 purge 전략이 없으면 저장소가 새 병목이 됩니다. retention과 아카이브를 도입 초기부터 같이 잡아야 합니다.</p>
<p>넷째, 외부 API 호출이 트랜잭션 안에 길게 묶이면 lease 만료와 중복 재진입 위험이 커집니다. 이 경우는 outbox 또는 비동기 단계 분리를 먼저 검토하는 편이 낫습니다.</p>
<p>다섯째, 운영자가 수동 replay를 자주 하는 팀이라면 inbox 설계에 <code>operator_reason</code>, <code>replayed_by</code>, <code>replay_batch_id</code> 같은 감사 필드도 생각보다 중요합니다. 나중에 원인 추적 비용을 크게 줄여 줍니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 중복 효과 비용이 큰 consumer를 상위 위험도 기준으로 식별했다.</li>
<li><input disabled="" type="checkbox"> <code>message_id</code>, <code>business_key</code>, <code>status</code>, <code>payload_hash</code>를 inbox에 저장한다.</li>
<li><input disabled="" type="checkbox"> 비즈니스 효과와 inbox <code>DONE</code> 업데이트를 같은 트랜잭션으로 커밋한다.</li>
<li><input disabled="" type="checkbox"> 순서 민감 도메인에 version 또는 상태 전이 규칙이 있다.</li>
<li><input disabled="" type="checkbox"> DLQ replay 최대 기간에 맞춰 inbox retention을 정했다.</li>
<li><input disabled="" type="checkbox"> <code>duplicate_skip_rate</code>, <code>processing_stuck</code>, <code>payload_hash_mismatch</code>를 모니터링한다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>현재 운영 중인 consumer 하나를 골라, &ldquo;같은 메시지가 두 번 오면 어떤 side effect가 두 번 실행되는지&quot;를 표로 적어 보세요. 이 단계만 해도 inbox 필요성이 훨씬 선명해집니다.</li>
<li><code>DONE</code>만 있는 단순 dedupe 설계와 <code>RECEIVED/PROCESSING/DONE/FAILED/DEAD</code> 상태 머신 설계를 비교하고, 복구 시 어떤 질문에 각각 답할 수 있는지 정리해 보세요.</li>
<li>DLQ에서 7일치 메시지를 다시 넣는 시나리오를 가정해, 현재 retention과 인덱스 전략이 버틸지 계산해 보세요.</li>
</ol>
<h2 id="함께-보면-좋은-글">함께 보면 좋은 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">트랜잭션 아웃박스 + CDC</a></li>
<li><a href="/learning/deep-dive/deep-dive-idempotency/">멱등성: 안전한 재시도를 위한 API 설계</a></li>
<li><a href="/learning/deep-dive/deep-dive-kafka-retry-dlq/">Kafka Retry/DLQ 패턴</a></li>
<li><a href="/learning/deep-dive/deep-dive-kafka-idempotence-ordering/">Kafka 멱등성·순서 보장 설계</a></li>
<li><a href="/learning/deep-dive/deep-dive-distributed-transactions/">분산 트랜잭션: 2PC에서 SAGA까지</a></li>
</ul>
]]></content:encoded></item></channel></rss>