<?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>Signature Verification on jyukki's Blog</title><link>https://jyukki.com/tags/signature-verification/</link><description>Recent content in Signature Verification on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/signature-verification/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Webhook Delivery Reliability 플레이북 (Signature·Retry·Idempotency·DLQ)</title><link>https://jyukki.com/learning/deep-dive/deep-dive-webhook-delivery-reliability-playbook/</link><pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-webhook-delivery-reliability-playbook/</guid><description>외부 시스템으로 webhook를 보내는 백엔드에서 중복 전송, 유실, 서명 검증 실패, 재시도 폭주를 줄이기 위한 실무 기준을 숫자와 우선순위 중심으로 정리합니다.</description><content:encoded><![CDATA[<p>실무에서 webhook는 생각보다 자주 사고를 만듭니다. 결제 승인 이벤트를 보냈는데 상대 서비스가 두 번 처리하기도 하고, 배송 상태 변경을 보냈는데 202 응답만 남고 실제 반영은 안 되기도 합니다. 더 까다로운 문제는 대부분의 장애가 &ldquo;전송 실패&rdquo; 한 줄로 끝나지 않는다는 점입니다. 서명 검증 실패, 수신 측의 일시 장애, 재시도 폭주, 순서 역전, 같은 이벤트의 중복 발행이 한꺼번에 얽힙니다.</p>
<p>그래서 webhook 설계의 핵심은 단순 HTTP POST가 아닙니다. <strong>전송 성공률보다 효과의 일관성</strong>을 보장하는 구조가 먼저입니다. 이 글에서는 발신 시스템 기준으로 webhook를 운영할 때 반드시 정해야 하는 기준을 정리합니다. 핵심은 네 가지입니다. <strong>서명 검증 가능성, 재시도 제어, 멱등성 보장, 실패 격리(DLQ)</strong> 입니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>webhook 전달 시스템을 설계할 때 &ldquo;보내면 끝&quot;이 아니라 어떤 상태 전이를 관리해야 하는지 이해할 수 있습니다.</li>
<li>서명, retry, idempotency, DLQ를 어떤 순서와 기준으로 붙여야 운영 사고를 줄일 수 있는지 알 수 있습니다.</li>
<li>실무에서 바로 적용 가능한 숫자 기준, 예를 들어 timeout, retry 횟수, dedupe window, DLQ 승격 조건을 잡을 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-webhook의-핵심-실패는-네트워크가-아니라-상태-불일치다">1) webhook의 핵심 실패는 네트워크가 아니라 상태 불일치다</h3>
<p>많은 팀이 webhook 장애를 &ldquo;상대 서버가 응답을 안 했다&rdquo; 정도로만 이해합니다. 하지만 실제로 더 위험한 건 <strong>발신 시스템과 수신 시스템이 서로 다른 진실을 믿는 상태</strong>입니다.</p>
<p>대표 시나리오는 아래와 같습니다.</p>
<ol>
<li>발신 시스템이 이벤트를 생성하고 webhook 전송 시도</li>
<li>수신 시스템은 실제 처리 완료</li>
<li>응답이 타임아웃 나서 발신 시스템은 실패로 기록</li>
<li>발신 시스템이 재시도</li>
<li>수신 시스템은 같은 이벤트를 다시 받아 중복 처리</li>
</ol>
<p>이 문제는 <a href="/learning/deep-dive/deep-dive-idempotency/">멱등성 설계</a>와 같은 관점으로 봐야 합니다. webhook는 단순 알림이 아니라 외부 효과를 유발하는 명령에 가깝기 때문에, 전송 성공보다 <strong>중복 효과 방지</strong>가 우선입니다.</p>
<h3 id="2-동기-요청으로-직접-보내기보다-비동기-전달-파이프라인이-안전하다">2) 동기 요청으로 직접 보내기보다 비동기 전달 파이프라인이 안전하다</h3>
<p>애플리케이션 트랜잭션 안에서 외부 webhook를 직접 호출하면 다음 문제가 생깁니다.</p>
<ul>
<li>DB 커밋과 외부 전송 성공 여부가 분리됨</li>
<li>외부 시스템 지연 때문에 사용자 요청 지연이 늘어남</li>
<li>같은 요청 안에서 재시도하면 thread와 connection을 오래 점유함</li>
</ul>
<p>그래서 실무에서는 보통 아래 구조가 안정적입니다.</p>
<ol>
<li>비즈니스 트랜잭션에서 이벤트 레코드 저장</li>
<li>Outbox 또는 전송 큐에 적재</li>
<li>별도 dispatcher가 webhook 전송</li>
<li>응답 상태와 재시도 스케줄 기록</li>
<li>반복 실패는 DLQ로 격리</li>
</ol>
<p>이 패턴은 <a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">Transactional Outbox + CDC</a>를 적용할 때 가장 자연스럽습니다. 핵심은 &ldquo;비즈니스 성공&quot;과 &ldquo;외부 전송 시도&quot;를 분리해, 사용자 요청 경로를 짧게 유지하는 것입니다.</p>
<h3 id="3-서명-검증은-보안-기능이면서-동시에-운영-디버깅-기능이다">3) 서명 검증은 보안 기능이면서 동시에 운영 디버깅 기능이다</h3>
<p>수신 시스템 입장에서는 webhook를 믿을 근거가 필요합니다. 이때 흔히 HMAC 기반 서명을 사용합니다. 그런데 서명은 단순 보안 체크로만 끝내면 절반만 구현한 것입니다.</p>
<p>서명 설계 시 필수 항목:</p>
<ul>
<li><code>event_id</code></li>
<li><code>timestamp</code></li>
<li><code>delivery_attempt</code></li>
<li>body 원문 해시 또는 body 자체</li>
<li>서명 알고리즘 버전(<code>v1</code>, <code>v2</code>)</li>
</ul>
<p>권장 검증 규칙:</p>
<ul>
<li>timestamp 허용 오차: 3~5분</li>
<li>동일 <code>event_id + signature_version</code> 재사용 허용 여부 명시</li>
<li>body canonicalization 규칙 고정(JSON key 정렬 여부 등)</li>
<li>key rotation 중에는 2개 키 동시 허용 기간 운영</li>
</ul>
<p>이 기준이 없으면 서명 실패 원인을 구분하기 어렵습니다. 실제 사고에서는 악성 요청보다도, 인코딩 차이·본문 재직렬화·시계 오차 때문에 정상 요청이 거부되는 경우가 더 많습니다.</p>
<h3 id="4-재시도는-많이-할수록-좋은-게-아니라-빨리-포기해야-할-대상과-끝까지-밀어야-할-대상을-나누는-게-중요하다">4) 재시도는 많이 할수록 좋은 게 아니라, 빨리 포기해야 할 대상과 끝까지 밀어야 할 대상을 나누는 게 중요하다</h3>
<p>재시도 전략을 잘못 잡으면 webhook 시스템이 장애 증폭기가 됩니다. 예를 들어 10초 timeout으로 8회 즉시 재시도하면, 상대 시스템이 이미 느린 상황에서 연결 수만 폭증합니다.</p>
<p>기본 원칙은 이렇습니다.</p>
<ul>
<li><strong>4xx 중 일부는 즉시 중지</strong>: 400, 401, 403, 404, 410은 보통 영구 실패 후보</li>
<li><strong>429/5xx는 제한된 재시도</strong>: backoff와 jitter 필수</li>
<li><strong>timeout/network error는 짧게 재시도 후 대기열로 회수</strong></li>
</ul>
<p>권장 초기값:</p>
<ul>
<li>connect timeout: 1초</li>
<li>read timeout: 3초</li>
<li>즉시 재시도: 0~1회</li>
<li>총 재시도 횟수: 5~8회</li>
<li>backoff: 1분, 5분, 15분, 1시간, 6시간</li>
<li>jitter: 10~20%</li>
</ul>
<p>이 기준은 <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff</a>와 같은 관점입니다. 성공률을 끌어올리려면 재시도 횟수보다 <strong>동시성 제한과 재시도 간격</strong>을 먼저 조정해야 합니다.</p>
<h3 id="5-dlq는-실패-저장소가-아니라-운영-우선순위-큐다">5) DLQ는 실패 저장소가 아니라 운영 우선순위 큐다</h3>
<p>DLQ를 &ldquo;나중에 보자&quot;는 창고처럼 쓰면 금방 쌓입니다. 중요한 건 DLQ로 보낸 후 무엇을 할지 미리 정하는 것입니다.</p>
<p>DLQ 승격 예시 조건:</p>
<ul>
<li>24시간 내 최대 재시도 횟수 초과</li>
<li>수신 엔드포인트가 410 Gone 반환</li>
<li>서명 검증 실패가 3회 연속 발생</li>
<li>payload 스키마 검증 실패</li>
</ul>
<p>DLQ 레코드 최소 필드:</p>
<ul>
<li>endpoint_id</li>
<li>event_id</li>
<li>event_type</li>
<li>final_status_code</li>
<li>last_error_reason</li>
<li>next_action(owner, due_at)</li>
<li>payload_hash</li>
</ul>
<p>메시지 재처리 관점은 <a href="/learning/deep-dive/deep-dive-kafka-retry-dlq/">Kafka Retry/DLQ</a>와 닮아 있습니다. 차이는 webhook DLQ는 단순 재적재보다 <strong>계약 변경 여부, endpoint 폐기 여부, 수신 팀 상태</strong>를 같이 봐야 한다는 점입니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-권장-아키텍처">1) 권장 아키텍처</h3>
<p>가장 무난한 기본형은 아래 구조입니다.</p>
<ol>
<li><strong>Event Producer</strong>: 주문/결제/회원 상태 변경 발생</li>
<li><strong>Outbox Table</strong>: 비즈니스 트랜잭션과 함께 이벤트 저장</li>
<li><strong>Dispatcher Worker</strong>: endpoint별 전송 정책에 따라 webhook 발송</li>
<li><strong>Delivery Log</strong>: attempt, 응답 코드, latency, error 기록</li>
<li><strong>Retry Scheduler</strong>: backoff 정책으로 재시도 예약</li>
<li><strong>DLQ + Ops Queue</strong>: 영구 실패 혹은 사람 판단이 필요한 건 별도 분리</li>
</ol>
<p>여기서 중요한 분리 기준은 세 가지입니다.</p>
<ul>
<li>이벤트 생성과 전송을 분리한다.</li>
<li>endpoint별 rate limit을 둔다.</li>
<li>운영자가 수동 재전송할 때도 같은 멱등성 키를 사용한다.</li>
</ul>
<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> webhook_endpoint (
</span></span><span style="display:flex;"><span>  endpoint_id         <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>  target_url          <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">500</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  secret_version      <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>  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 style="color:#6272a4">-- ACTIVE, PAUSED, DISABLED
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>  timeout_ms          <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">3000</span>,
</span></span><span style="display:flex;"><span>  max_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">6</span>,
</span></span><span style="display:flex;"><span>  rate_limit_per_min  <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">120</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></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></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">TABLE</span> webhook_delivery (
</span></span><span style="display:flex;"><span>  delivery_id         <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>  endpoint_id         <span style="color:#8be9fd;font-style:italic">BIGINT</span>       <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  event_id            <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>  event_type          <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>  idempotency_key     <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">160</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>  attempt_no          <span style="color:#8be9fd;font-style:italic">INT</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 style="color:#6272a4">-- PENDING, SENT, ACKED, RETRY, DLQ
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>  next_attempt_at     <span style="color:#ff79c6">TIMESTAMP</span>,
</span></span><span style="display:flex;"><span>  last_status_code    <span style="color:#8be9fd;font-style:italic">INT</span>,
</span></span><span style="display:flex;"><span>  last_error_reason   <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">200</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>  created_at          <span style="color:#ff79c6">TIMESTAMP</span>    <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</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></span><span style="display:flex;"><span>  <span style="color:#ff79c6">UNIQUE</span> (endpoint_id, event_id)
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p><code>UNIQUE (endpoint_id, event_id)</code>를 두는 이유는 같은 endpoint에 같은 이벤트가 중복 적재되는 것을 1차로 막기 위해서입니다. 다만 이것만으로는 충분하지 않으므로 수신 측도 <code>idempotency_key</code>를 소비해야 합니다.</p>
<h3 id="3-의사결정-기준숫자조건우선순위">3) 의사결정 기준(숫자·조건·우선순위)</h3>
<p>권장 우선순위는 <strong>데이터 무결성 &gt; 중복 효과 방지 &gt; 전송 지연 최소화 &gt; 처리 비용</strong> 입니다.</p>
<p>초기 운영 기준 예시:</p>
<ul>
<li>delivery success rate: 99% 이상</li>
<li>p95 delivery latency: 60초 이내</li>
<li>duplicate delivery rate: 0.5% 미만</li>
<li>DLQ 비율: 일간 0.3% 미만</li>
<li>signature verification failure: 0.1% 미만</li>
<li>endpoint별 동시 in-flight 요청: 3~10개로 제한</li>
</ul>
<p>자동 제어 기준 예시:</p>
<ul>
<li>5분 이동창에서 5xx 비율 20% 초과 시 해당 endpoint 일시 pause</li>
<li>429 비율 10% 초과 시 동시성 50% 감소</li>
<li>signature failure 3회 연속 발생 시 key mismatch 경보 + 자동 disable 후보 등록</li>
<li>24시간 내 DLQ 10건 초과 endpoint는 운영자 점검 전 수동 승인 모드 전환</li>
</ul>
<p>실무에서는 &ldquo;무조건 재전송&quot;보다 <strong>endpoint health에 따라 동작 모드를 바꾸는 것</strong>이 더 중요합니다. 이 부분은 <a href="/learning/deep-dive/deep-dive-api-rate-limit-backpressure/">API Rate Limit + Backpressure</a>와 연결해서 보면 좋습니다.</p>
<h3 id="4-실패-분류-기준을-먼저-정해야-운영이-빨라진다">4) 실패 분류 기준을 먼저 정해야 운영이 빨라진다</h3>
<p>실패를 전부 <code>delivery_failed</code>로만 남기면 원인 파악 시간이 길어집니다. 최소한 아래 수준으로는 나눠야 합니다.</p>
<ul>
<li><code>network_timeout</code></li>
<li><code>tls_handshake_failed</code></li>
<li><code>signature_invalid</code></li>
<li><code>http_4xx_permanent</code></li>
<li><code>http_5xx_retryable</code></li>
<li><code>rate_limited</code></li>
<li><code>payload_schema_invalid</code></li>
<li><code>endpoint_disabled</code></li>
</ul>
<p>분류 기준이 있으면 대응 자동화가 쉬워집니다.</p>
<ul>
<li><code>signature_invalid</code> → secret rotation 확인</li>
<li><code>rate_limited</code> → endpoint 동시성 낮춤</li>
<li><code>http_4xx_permanent</code> → 재시도 중단 + 계약 확인</li>
<li><code>payload_schema_invalid</code> → producer 배포 점검</li>
</ul>
<h3 id="5-4주-도입-플랜">5) 4주 도입 플랜</h3>
<p><strong>1주차: 전송 로그 표준화</strong><br>
<code>event_id</code>, <code>endpoint_id</code>, <code>attempt_no</code>, <code>status_code</code>, <code>latency_ms</code>, <code>payload_hash</code>를 전부 기록합니다.</p>
<p><strong>2주차: 비동기 전송 + 멱등성 키 도입</strong><br>
사용자 요청 경로에서 외부 호출을 분리하고, <code>idempotency_key</code>와 <code>UNIQUE(endpoint_id, event_id)</code>를 도입합니다.</p>
<p><strong>3주차: endpoint별 정책 분리</strong><br>
timeout, retry, concurrency, signing secret rotation 정책을 endpoint 단위로 나눕니다.</p>
<p><strong>4주차: DLQ 운영 룰과 대시보드 고정</strong><br>
DLQ 승격 조건, 자동 pause 조건, 수동 재처리 절차를 문서화합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<ol>
<li>
<p><strong>너무 강한 재시도는 성공률 대신 장애 반경을 키울 수 있다</strong><br>
특히 상대 시스템이 단일 DB에 묶여 있거나 rate limit이 약한 경우, 재시도 폭주는 webhook보다 더 큰 장애를 만들 수 있습니다.</p>
</li>
<li>
<p><strong>서명만 도입하고 canonicalization을 고정하지 않으면 정상 요청도 깨진다</strong><br>
JSON 직렬화 방식, 줄바꿈, charset 차이 때문에 운영 중 허무한 실패가 발생합니다.</p>
</li>
<li>
<p><strong>발신 측 멱등성만으로는 충분하지 않다</strong><br>
수신 측이 같은 <code>event_id</code>를 중복 처리하면 여전히 사고가 납니다. 계약 문서에 수신 측 dedupe 책임 범위를 명시해야 합니다.</p>
</li>
<li>
<p><strong>DLQ를 쌓아두기만 하면 기술 부채가 된다</strong><br>
DLQ는 실패 아카이브가 아니라 운영자 액션 큐여야 합니다. owner와 due time이 없는 DLQ는 거의 항상 방치됩니다.</p>
</li>
</ol>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> webhook 전송이 사용자 요청 경로와 분리돼 있다.</li>
<li><input disabled="" type="checkbox"> <code>event_id</code>, <code>idempotency_key</code>, <code>attempt_no</code>가 로그와 저장소에 모두 남는다.</li>
<li><input disabled="" type="checkbox"> endpoint별 timeout, retry, concurrency 정책이 숫자로 정의돼 있다.</li>
<li><input disabled="" type="checkbox"> 서명 버전과 key rotation 절차가 문서화돼 있다.</li>
<li><input disabled="" type="checkbox"> DLQ 승격 조건과 수동 재처리 owner가 정해져 있다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>현재 운영 중인 외부 연동 1개를 골라, <code>network_timeout</code>, <code>4xx</code>, <code>5xx</code>, <code>signature_invalid</code>로 실패를 나눴을 때 지난 2주 비율을 계산해 보세요.</li>
<li><code>event_id</code> 기준 중복 전송률과 실제 중복 처리율을 따로 측정해 보세요. 둘의 차이가 크면 수신 측 멱등성 계약이 약하다는 신호입니다.</li>
<li>endpoint 하나를 골라 <code>429 비율 10% 초과 시 동시성 50% 감축</code> 규칙을 적용했을 때 success rate와 p95 latency가 어떻게 변하는지 비교해 보세요.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-idempotency/">멱등성(Idempotency) 설계</a></li>
<li><a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">Transactional Outbox + CDC</a></li>
<li><a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout/Retry/Backoff 운영 기준</a></li>
<li><a href="/learning/deep-dive/deep-dive-api-rate-limit-backpressure/">API Rate Limit + Backpressure 설계</a></li>
<li><a href="/learning/deep-dive/deep-dive-kafka-retry-dlq/">Kafka Retry/DLQ 패턴</a></li>
</ul>
]]></content:encoded></item></channel></rss>