<?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>Log Compaction on jyukki's Blog</title><link>https://jyukki.com/tags/log-compaction/</link><description>Recent content in Log Compaction on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-KR</language><lastBuildDate>Mon, 31 Aug 2026 10:06:00 +0900</lastBuildDate><atom:link href="https://jyukki.com/tags/log-compaction/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Kafka Log Compaction·Tombstone·키 수명주기 운영 플레이북</title><link>https://jyukki.com/learning/deep-dive/deep-dive-kafka-log-compaction-tombstone-key-lifecycle-playbook/</link><pubDate>Mon, 31 Aug 2026 10:06:00 +0900</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-kafka-log-compaction-tombstone-key-lifecycle-playbook/</guid><description>Kafka compacted topic을 단순한 저장 공간 절감 옵션으로 보지 않고, key 설계·tombstone 보존·consumer bootstrap·재처리·스키마 진화까지 포함한 상태 동기화 계약으로 운영하는 기준을 정리합니다.</description><content:encoded><![CDATA[<p>Kafka의 <code>cleanup.policy=compact</code>는 흔히 저장 공간을 줄이는 옵션으로 소개됩니다. 하지만 운영에서 compacted topic의 진짜 용도는 디스크 절감이 아닙니다. 새 consumer가 오래된 이벤트를 전부 이해하지 않아도 <strong>key별 현재 상태를 복구할 수 있게 만드는 상태 배포 경로</strong>입니다. 이 전제가 빠지면 팀은 compacted topic에 감사 이력, 임시 이벤트, 불안정한 식별자를 섞어 넣고, 장애 뒤에 &ldquo;토픽에는 데이터가 있는데 왜 삭제된 사용자가 다시 살아났지?&rdquo; 같은 문제를 만납니다.</p>
<p>이 글은 <a href="/learning/deep-dive/deep-dive-kafka-idempotence-ordering/">Kafka 멱등·정렬 처리 전략</a>, <a href="/learning/deep-dive/deep-dive-event-schema-registry-compatibility-playbook/">이벤트 스키마 레지스트리와 호환성 운영</a>, <a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">Transactional Outbox와 CDC</a>, <a href="/learning/deep-dive/deep-dive-projection-lag-read-model-rebuild-playbook/">Projection Lag와 Read Model Rebuild</a>을 연결합니다. 앞선 글이 이벤트의 전달·호환성·읽기 모델 복구를 다뤘다면, 여기서는 <strong>최신 상태만 남기는 토픽이 어떤 계약을 가져야 안전한지</strong>를 다룹니다.</p>
<p>참고 자료:</p>
<ul>
<li><a href="https://kafka.apache.org/documentation/#compaction">Apache Kafka: Log Compaction</a></li>
<li><a href="https://kafka.apache.org/documentation/#topicconfigs">Apache Kafka: Topic Configuration</a></li>
</ul>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>Log compaction이 보장하는 것과 보장하지 않는 것을 구분합니다.</li>
<li>key, value, tombstone을 상태 머신 관점에서 설계하는 방법을 배웁니다.</li>
<li>consumer bootstrap, key migration, 재처리에서 삭제 상태가 되살아나는 사고를 막는 기준을 얻습니다.</li>
<li>compaction 설정을 storage 튜닝이 아니라 data correctness와 복구 시간의 운영 계약으로 측정합니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-compaction은-즉시-정리가-아니라-나중에-최신-상태를-남기는-정책이다">1) Compaction은 즉시 정리가 아니라 &ldquo;나중에 최신 상태를 남기는&rdquo; 정책이다</h3>
<p>compacted topic에서 Kafka는 같은 key를 가진 레코드 중 오래된 레코드를 cleaner가 나중에 제거할 수 있게 합니다. 중요한 단어는 <strong>나중에</strong>입니다. produce 직후에 과거 레코드가 사라지는 것도 아니고, consumer가 중복 레코드를 절대 보지 않는 것도 아닙니다. segment 크기, cleaner 처리량, <code>min.cleanable.dirty.ratio</code>, 브로커 부하에 따라 한 key의 과거 값이 꽤 오래 남을 수 있습니다.</p>
<p>따라서 consumer는 다음 두 규칙을 동시에 따라야 합니다.</p>
<ol>
<li>같은 key가 여러 번 와도 마지막 offset의 상태가 이긴다고 처리합니다.</li>
<li>오래된 레코드가 남아 있다는 사실로 audit history가 보존된다고 판단하지 않습니다.</li>
</ol>
<p>예를 들어 <code>customer-preference</code> 토픽에 <code>customerId=42</code>의 언어 설정이 <code>ko → en → ko</code>로 세 번 들어왔다면, compaction 뒤에는 마지막 <code>ko</code>만 남을 수 있습니다. 그러나 cleaner가 돌기 전에는 세 레코드를 모두 읽을 수 있고, 파티션이 다르면 서로 다른 key 사이의 시간 순서는 보장되지 않습니다. compaction은 데이터베이스의 unique constraint도, sync 호출의 deduplication도 아닙니다. producer 중복과 consumer 중복의 경계는 <a href="/learning/deep-dive/deep-dive-kafka-idempotence-ordering/">Kafka 멱등·정렬 처리 전략</a>에서 정리한 것처럼 별도 방어선이 필요합니다.</p>
<table>
  <thead>
      <tr>
          <th>질문</th>
          <th>compacted topic의 답</th>
          <th>운영상 필요한 보완</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>같은 key의 과거 값이 즉시 사라지는가</td>
          <td>아니다</td>
          <td>consumer는 반복 레코드를 허용해야 한다</td>
      </tr>
      <tr>
          <td>새 consumer가 최종 상태를 만들 수 있는가</td>
          <td>가능하다</td>
          <td>key·tombstone·bootstrap 계약이 올바를 때만 가능</td>
      </tr>
      <tr>
          <td>모든 과거 사실을 보존하는가</td>
          <td>아니다</td>
          <td>audit event는 append-only topic 또는 원장에 둔다</td>
      </tr>
      <tr>
          <td>null value는 빈 상태인가</td>
          <td>아니다</td>
          <td>일반적으로 tombstone으로 해석되므로 별도 의미를 정해야 한다</td>
      </tr>
  </tbody>
</table>
<h3 id="2-key는-partitioning용-문자열이-아니라-상태의-신원이다">2) key는 partitioning용 문자열이 아니라 상태의 신원이다</h3>
<p>compaction은 key가 같다고 판단한 레코드를 하나의 상태 계열로 묶습니다. 그래서 key 선택은 &ldquo;파티션을 고르게 나누는가&quot;보다 먼저 <strong>이 두 레코드는 실제로 같은 현재 상태를 표현하는가</strong>를 묻는 일입니다. <code>orderId</code>는 주문 상태의 좋은 key가 될 수 있지만, 결제 승인 시도 하나하나를 표현하는 topic에 <code>orderId</code>를 쓰면 여러 시도가 서로 덮어쓸 수 있습니다. 그 경우 <code>paymentAttemptId</code>가 더 적합하거나, 애초에 compacted topic이 맞지 않을 수 있습니다.</p>
<p>좋은 key에는 네 가지 성질이 있습니다.</p>
<ul>
<li><strong>안정성</strong>: 이메일·표시명처럼 바뀔 수 있는 값 대신 내부 ID나 명시적인 business key를 쓴다.</li>
<li><strong>단일성</strong>: 서로 다른 두 state가 같은 key로 합쳐지지 않는다.</li>
<li><strong>해석 가능성</strong>: consumer가 key만 보고 상태 범위와 소유 도메인을 알 수 있다.</li>
<li><strong>수명 명시성</strong>: 생성, 갱신, 삭제, 재활성화 때 같은 key를 계속 쓸지 문서화한다.</li>
</ul>
<p><code>tenantId:userId</code>처럼 복합 key를 만들 때도 delimiter 관습만 믿지 말고 canonical encoding을 정합니다. 숫자와 문자열 normalization, 대소문자, UUID format, tenant 이동 시 key 의미를 통일하지 않으면 같은 사람의 상태가 여러 key에 쌓입니다. <a href="/learning/deep-dive/deep-dive-identifier-normalization-security-playbook/">식별자 정규화와 보안</a>에서 다룬 것처럼 표기 차이는 단순한 문자열 문제가 아니라 접근 범위와 데이터 병합 오류가 될 수 있습니다.</p>
<h3 id="3-tombstone은-삭제-이벤트가-아니라-최신-상태의-부정이다">3) Tombstone은 &ldquo;삭제 이벤트&quot;가 아니라 최신 상태의 부정이다</h3>
<p>Kafka에서 일반적으로 key는 있고 value가 <code>null</code>인 레코드는 tombstone입니다. compaction은 이 tombstone을 일정 기간 보존한 뒤 key의 이전 값과 tombstone을 함께 제거할 수 있습니다. 늦게 시작한 consumer가 이전 값을 읽은 뒤 tombstone도 읽어야 해당 key가 더 이상 존재하지 않는다는 상태를 만들 수 있기 때문입니다.</p>
<p>여기서 자주 나는 사고는 <code>null</code>을 &ldquo;선호도 없음&quot;이나 &ldquo;값을 아직 계산하지 않음&quot;으로 재사용하는 것입니다. 그러면 consumer는 필드가 비어 있는 정상 상태와 삭제 상태를 구분할 수 없습니다. 값이 비어 있음을 표현하려면 명시적인 value를 씁니다.</p>
<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-json" data-lang="json"><span style="display:flex;"><span><span style="color:#6272a4">// 상태는 존재하지만 알림 채널이 없다
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>{ <span style="color:#ff79c6">&#34;customerId&#34;</span>: <span style="color:#f1fa8c">&#34;42&#34;</span>, <span style="color:#ff79c6">&#34;notificationChannels&#34;</span>: [] }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// customerId=42 상태 자체를 제거한다
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>key = <span style="color:#f1fa8c">&#34;42&#34;</span>, value = <span style="color:#ff79c6">null</span>
</span></span></code></pre></div><p><code>delete.retention.ms</code>는 정리 편의값이 아니라 <strong>가장 느린 정상 consumer가 tombstone을 볼 수 있는 창</strong>입니다. 출발점은 다음처럼 잡을 수 있습니다.</p>
<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-text" data-lang="text"><span style="display:flex;"><span>delete.retention.ms &gt;= 최대 planned downtime
</span></span><span style="display:flex;"><span>                     + 가장 긴 bootstrap 시간
</span></span><span style="display:flex;"><span>                     + incident 복구 여유
</span></span></code></pre></div><p>예를 들어 주말 포함 최대 72시간 중단이 가능한 consumer가 있고, full bootstrap이 6시간이며, 복구 확인에 6시간을 잡는다면 7일보다 짧은 보존값은 보수적이지 않습니다. 이 수치는 절대 규칙이 아니라 workload 계약입니다. 장기간 오프라인이 허용되는 모바일 동기화나 외부 파트너 consumer가 있다면 토픽 bootstrap이 아니라 별도 snapshot API를 제공할지 먼저 결정해야 합니다.</p>
<h3 id="4-value-schema와-key-schema의-진화는-속도가-다르다">4) Value schema와 key schema의 진화는 속도가 다르다</h3>
<p>값 필드 하나를 추가하는 일은 <a href="/learning/deep-dive/deep-dive-event-schema-registry-compatibility-playbook/">이벤트 스키마 레지스트리와 호환성 운영</a>의 backward/forward compatibility 규칙으로 관리할 수 있습니다. 반면 key 변경은 더 위험합니다. compaction의 동치 관계 자체가 바뀌기 때문입니다. <code>email</code> key를 <code>userId</code> key로 옮기는 순간, Kafka는 두 key가 같은 사람이라는 사실을 모릅니다.</p>
<p>안전한 key migration은 보통 네 단계입니다.</p>
<ol>
<li><strong>mapping 확정</strong>: old key와 new key를 연결하는 source of truth와 충돌 처리 기준을 정합니다.</li>
<li><strong>새 key publish</strong>: 상태 snapshot을 new key로 발행하고 new consumer가 이를 읽게 합니다.</li>
<li><strong>소비자 전환 검증</strong>: checksum과 key 수를 비교해 새 상태가 완전한지 확인합니다.</li>
<li><strong>이전 key tombstone</strong>: 전환 window가 끝난 뒤 old key에 tombstone을 발행하고 보존 창 동안 관측합니다.</li>
</ol>
<p>dual publish 기간에는 한 도메인 변경이 두 key에 서로 다른 revision으로 기록되지 않게 <code>stateVersion</code> 또는 source event offset을 넣는 편이 좋습니다. &ldquo;두 토픽에 비슷한 값을 보냈다&quot;는 것은 consistency 증거가 아닙니다. old/new state count, checksum, tombstone count를 같은 release gate에서 봐야 합니다.</p>
<h3 id="5-bootstrap은-consumer-onboarding이-아니라-복구-테스트다">5) Bootstrap은 consumer onboarding이 아니라 복구 테스트다</h3>
<p>compacted topic의 가장 큰 장점은 새 consumer가 <code>earliest</code>부터 읽어 현재 상태를 만들 수 있다는 점입니다. 그러나 이 절차를 실제로 실행하지 않으면 장점은 가정일 뿐입니다. 늦은 tombstone, serializer 변경, null key, 오래된 consumer bug, 잘못된 offset reset policy는 production 사고가 난 뒤에야 드러납니다.</p>
<p>최소한 주 1회 또는 schema/key 변경 때 다음 검증을 실행합니다.</p>
<ol>
<li>빈 state store를 준비하고 consumer를 earliest offset에서 시작합니다.</li>
<li>bootstrap 종료 offset, 처리한 key 수, tombstone 수, 소요 시간을 기록합니다.</li>
<li>기준 DB 또는 신뢰할 수 있는 snapshot과 key count·sample checksum을 비교합니다.</li>
<li>불일치가 있으면 lag만 보지 말고 tombstone age, key normalization, schema decode failure부터 분리합니다.</li>
</ol>
<p>이 과정은 <a href="/learning/deep-dive/deep-dive-projection-lag-read-model-rebuild-playbook/">Projection Lag와 Read Model Rebuild</a>의 rebuild 원칙을 현재 상태 topic에 적용한 것입니다. 복원 비용이 너무 커서 이 테스트를 못 돌린다면, cleaner 설정을 조정하기 전에 state의 범위와 snapshot 전략을 다시 설계해야 합니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-상태-topic-계약을-한-장으로-고정한다">1) 상태 topic 계약을 한 장으로 고정한다</h3>
<p>새 compacted topic을 만들 때 아래 표를 ADR 또는 repository 문서에 넣습니다. 설정만 남기고 데이터 의미를 남기지 않으면 몇 달 뒤 <code>null</code>이 삭제였는지, retry signal이었는지 아무도 알 수 없습니다.</p>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>예시</th>
          <th>결정 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>topic</td>
          <td><code>catalog.product-state.v1</code></td>
          <td>append-only event와 구분되는 이름</td>
      </tr>
      <tr>
          <td>key</td>
          <td>immutable <code>productId</code></td>
          <td>한 key가 하나의 현재 상품 상태</td>
      </tr>
      <tr>
          <td>value</td>
          <td>displayable, sellable, revision</td>
          <td>null은 사용하지 않고 빈 상태는 명시 표현</td>
      </tr>
      <tr>
          <td>tombstone owner</td>
          <td>catalog service</td>
          <td>실제 삭제 또는 state ownership 종료만 발행</td>
      </tr>
      <tr>
          <td>retention</td>
          <td><code>delete.retention.ms=7d</code></td>
          <td>downtime 72시간 + bootstrap 6시간 + 여유</td>
      </tr>
      <tr>
          <td>bootstrap SLO</td>
          <td>10M key를 90분 이내</td>
          <td>새 consumer/recovery 목표에 맞춤</td>
      </tr>
      <tr>
          <td>correctness gate</td>
          <td>sampled checksum mismatch 0건</td>
          <td>disk size가 아닌 상태 정합성 우선</td>
      </tr>
  </tbody>
</table>
<p>DB 변경에서 상태 topic을 갱신한다면 outbox 또는 CDC로 source transaction과 publish 경계를 정합니다. 단순히 application code에서 DB update 뒤 <code>send()</code>를 호출하면 실패 순서에 따라 DB만 바뀌거나 Kafka만 바뀌는 빈틈이 생깁니다. 그 경계는 <a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">Transactional Outbox와 CDC</a>의 문제이지만, compacted topic에서는 그 빈틈이 오래 지속되는 stale state로 보인다는 점이 다릅니다.</p>
<h3 id="2-삭제-경로를-createupdate와-같은-수준으로-테스트한다">2) 삭제 경로를 create/update와 같은 수준으로 테스트한다</h3>
<p>일반적인 happy path 테스트는 <code>create → update → read</code>까지만 확인합니다. 상태 topic에서는 <code>create → update → delete → empty bootstrap</code>이 최소 경로입니다. 특히 delete 직후 live consumer가 state를 지우는지, 오프라인 consumer가 tombstone을 지나 재시작했을 때도 같은 결과인지 둘 다 확인해야 합니다.</p>
<p>다음 기준으로 canary를 시작할 수 있습니다.</p>
<ul>
<li>tombstone 처리 지연 p95: <strong>5분 이하</strong> 또는 도메인의 삭제 SLO 이하</li>
<li>tombstone decode/처리 오류: <strong>0건</strong></li>
<li>bootstrap 뒤 기준 snapshot과 sampled checksum mismatch: <strong>0건</strong></li>
<li>key count 차이: <strong>0.1% 미만</strong>, 초과 시 자동 확대 중단</li>
<li>bootstrap duration: baseline 대비 <strong>20% 이내</strong> 증가</li>
<li>compaction cleaner lag: 일시 spike가 아니라 <strong>30분 이상 지속</strong> 시 조사</li>
</ul>
<p>여기서 0.1%는 삭제 상태 누락을 허용한다는 뜻이 아닙니다. 큰 dataset에서 기준 snapshot의 시점 차이, eventual consistency window를 분리하기 위한 조사 기준입니다. 삭제·권한·가격처럼 stale state가 곧 위험으로 이어지는 도메인은 checksum mismatch를 한 건도 허용하지 않는 편이 맞습니다.</p>
<h3 id="3-key-migration은-평상시-배포보다-느리게-rollback은-더-빠르게-설계한다">3) key migration은 평상시 배포보다 느리게, rollback은 더 빠르게 설계한다</h3>
<p>key migration 중 rollback 대상은 code revision만이 아닙니다. new key로 이미 발행한 state와 old key tombstone의 조합이 남습니다. 따라서 이전 key를 지우기 전에 new consumer rollout을 충분히 검증해야 합니다. 권장 순서는 <code>new topic 또는 new-key view 생성 → 5% consumer canary → full consumer 전환 → old key tombstone → retention 창 관찰 → producer 단순화</code>입니다.</p>
<p>old key tombstone을 너무 빨리 쓰면 rollback consumer가 과거 key state를 복원하지 못합니다. 반대로 tombstone을 영원히 미루면 한 상태가 두 identity로 살아 있습니다. migration 종료 조건을 &ldquo;배포가 끝남&quot;이 아니라 다음 세 개로 둡니다.</p>
<ul>
<li>new key consumer adoption 100%</li>
<li>state checksum과 business count가 7일 연속 기준 안</li>
<li>old key read 또는 write가 0인 기간이 delete retention보다 김</li>
</ul>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<ol>
<li>
<p><strong>Compacted topic은 audit log가 아닙니다.</strong> 현재 상태를 빠르게 배포하려는 목적과 모든 변경 사실을 보존하려는 목적은 분리합니다. 결제 원장, 보안 감사, 규제 보존은 append-only event와 별도 보존 정책이 필요합니다.</p>
</li>
<li>
<p><strong>삭제 보존을 무한히 늘린다고 자동으로 안전해지지 않습니다.</strong> bootstrap은 길어지고 disk 비용도 커집니다. 장기 오프라인 consumer가 정상 요구사항이라면 retention만 늘리기보다 snapshot delivery 또는 재동기화 API를 검토합니다.</p>
</li>
<li>
<p><strong>compaction은 key 없는 레코드를 구해 주지 않습니다.</strong> null key 레코드는 일반적인 compaction 대상으로 취급할 수 없습니다. 원인별 metric을 두고 producer validation에서 차단하는 편이 낫습니다.</p>
</li>
<li>
<p><strong>key 변경은 schema field rename보다 위험합니다.</strong> key가 바뀌면 cleanup의 동치 관계가 달라집니다. 새 key publish와 이전 key tombstone을 원자적이라고 가정하지 말고 reconciliation을 둡니다.</p>
</li>
<li>
<p><strong>cleaner lag만 보고 장애를 판단하지 않습니다.</strong> broker I/O, segment, retention, traffic 급증에 따라 lag는 일시적으로 커질 수 있습니다. 실제 사용자 영향은 bootstrap 시간, stale state, tombstone 누락과 함께 판단합니다.</p>
</li>
</ol>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> compacted topic의 key가 immutable business identity이며, 한 key가 하나의 최신 state만 대표한다.</li>
<li><input disabled="" type="checkbox"> <code>null</code> value의 의미를 tombstone으로 고정하고, 빈 상태는 명시 value로 표현한다.</li>
<li><input disabled="" type="checkbox"> <code>delete.retention.ms</code>가 planned downtime, full bootstrap, incident 여유를 합친 값보다 길다.</li>
<li><input disabled="" type="checkbox"> earliest bootstrap과 기준 snapshot checksum 비교를 정기적으로 실행한다.</li>
<li><input disabled="" type="checkbox"> key migration에 old/new mapping, dual publish, tombstone, rollback consumer 기간이 있다.</li>
<li><input disabled="" type="checkbox"> audit event와 state topic을 분리하고 각각의 retention·owner·복구 목적을 문서화했다.</li>
</ul>
<h3 id="연습-사용자-설정-상태-topic을-복구-가능하게-만들기">연습: 사용자 설정 상태 topic을 복구 가능하게 만들기</h3>
<ol>
<li><code>user-preference</code>처럼 최신 상태만 필요한 데이터 하나를 고르고, key가 실제 immutable ID인지 확인합니다.</li>
<li>create, update, empty value, delete를 각각 어떤 value 또는 tombstone으로 표현할지 표로 적습니다.</li>
<li>새 consumer가 earliest부터 읽어 state store를 만든 뒤 원본 DB와 key count·100개 샘플 checksum을 비교합니다.</li>
<li>consumer를 72시간 멈췄다고 가정하고, 현재 <code>delete.retention.ms</code>로 모든 tombstone을 볼 수 있는지 계산합니다.</li>
<li>key를 <code>email</code>에서 <code>userId</code>로 옮기는 migration runbook을 작성하되, old key tombstone 전에 어떤 rollback 증거가 필요한지 명시합니다.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-kafka-idempotence-ordering/">Kafka 멱등·정렬 처리 전략</a></li>
<li><a href="/learning/deep-dive/deep-dive-event-schema-registry-compatibility-playbook/">이벤트 스키마 레지스트리와 호환성 운영</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-projection-lag-read-model-rebuild-playbook/">Projection Lag와 Read Model Rebuild</a></li>
</ul>
]]></content:encoded></item></channel></rss>