{
    "componentChunkName": "component---src-templates-blog-template-js",
    "path": "/Music_Pipeline_05/",
    "result": {"data":{"cur":{"id":"7154d496-001f-5747-805c-4587277d4d14","html":"<h2 id=\"문제\" style=\"position:relative;\"><a href=\"#%EB%AC%B8%EC%A0%9C\" aria-label=\"문제 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>문제</h2>\n<p>깨진 JSON 메시지 하나가 파티션에 들어오면 무슨 일이 벌어지나.</p>\n<p>무한 재시도 설정이라면 그 메시지가 <strong>파티션 전체를 막는다</strong>(head-of-line blocking).\n뒤에 쌓인 정상 메시지 수만 건이 한 건 때문에 처리되지 않는다. 이게 poison pill 이다.</p>\n<h2 id=\"설계--실패를-두-종류로-가른다\" style=\"position:relative;\"><a href=\"#%EC%84%A4%EA%B3%84--%EC%8B%A4%ED%8C%A8%EB%A5%BC-%EB%91%90-%EC%A2%85%EB%A5%98%EB%A1%9C-%EA%B0%80%EB%A5%B8%EB%8B%A4\" aria-label=\"설계  실패를 두 종류로 가른다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>설계 — 실패를 두 종류로 가른다</h2>\n<p>M1 입수에서 “재시도 가능(DB 장애) vs 불가(검증 실패)“를 갈랐던 원칙을 그대로 재적용했다.</p>\n<table>\n<thead>\n<tr>\n<th>실패 유형</th>\n<th>판단</th>\n<th>처리</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>역직렬화 실패</td>\n<td>백 번 다시 읽어도 같은 바이트는 같은 실패</td>\n<td><strong>재시도 없이 즉시 DLQ</strong></td>\n</tr>\n<tr>\n<td>처리 실패 (DB 등)</td>\n<td>일시적일 수 있다</td>\n<td>지수 backoff(0.5s→1s→2s) 3회 → DLQ</td>\n</tr>\n</tbody>\n</table>\n<ul>\n<li><code class=\"language-text\">ErrorHandlingDeserializer</code> 가 역직렬화 실패를 예외가 아니라 “값”(null + 원인 헤더)으로\n바꿔준다 — 리스너까지 도달해야 우리가 제어권을 갖는다.</li>\n<li>고정 간격 재시도는 기각 — DB 가 과부하로 느려진 상황에서 같은 속도로 계속 두드리면\n회복을 방해한다. 지수 backoff 는 시스템에 숨 쉴 틈을 준다.</li>\n<li>DLQ 토픽은 원본과 같은 파티션 수, 보관은 4배 — 재처리 판단에 시간 여유를 준다.</li>\n</ul>\n<h2 id=\"삽질-기록--문서엔-없고-구현에만-있는-것들\" style=\"position:relative;\"><a href=\"#%EC%82%BD%EC%A7%88-%EA%B8%B0%EB%A1%9D--%EB%AC%B8%EC%84%9C%EC%97%94-%EC%97%86%EA%B3%A0-%EA%B5%AC%ED%98%84%EC%97%90%EB%A7%8C-%EC%9E%88%EB%8A%94-%EA%B2%83%EB%93%A4\" aria-label=\"삽질 기록  문서엔 없고 구현에만 있는 것들 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>삽질 기록 — 문서엔 없고 구현에만 있는 것들</h2>\n<h3 id=\"1-dlq-메시지가-base64-로-변질\" style=\"position:relative;\"><a href=\"#1-dlq-%EB%A9%94%EC%8B%9C%EC%A7%80%EA%B0%80-base64-%EB%A1%9C-%EB%B3%80%EC%A7%88\" aria-label=\"1 dlq 메시지가 base64 로 변질 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>1. DLQ 메시지가 base64 로 변질</h3>\n<p>DLQ 발행에 기존 JsonSerializer 템플릿을 재사용했더니, 깨진 원본 <code class=\"language-text\">byte[]</code> 가\n<strong>base64 문자열로 이중 인코딩</strong>되어 실렸다. 원본 보존이 DLQ 의 존재 이유인데 증거를\n훼손한 셈. <code class=\"language-text\">DeadLetterPublishingRecoverer</code> 에 <code class=\"language-text\">Map&lt;Class&lt;?>, KafkaOperations></code> 로\nbyte[] 전용 템플릿(ByteArraySerializer)을 따로 매핑해야 한다.</p>\n<h3 id=\"2-classcastexception-string-cannot-be-cast-to-b\" style=\"position:relative;\"><a href=\"#2-classcastexception-string-cannot-be-cast-to-b\" aria-label=\"2 classcastexception string cannot be cast to b permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>2. ClassCastException: String cannot be cast to [B</h3>\n<p>1번을 고치며 DLQ 템플릿의 <strong>key 직렬화까지</strong> ByteArray 로 바꿨더니 이번엔 키에서 터졌다.\n역직렬화가 실패한 것은 value 뿐이고 <strong>key 는 여전히 String</strong> 이다. 이 실패가 연쇄를\n일으켰다 — DLQ 발행 실패 → 에러 핸들러 실패 → 배치 전체 재시도. DLQ 경로 자체가\npoison pill 이 될 수 있다는 것을 몸으로 배웠다.</p>\n<h3 id=\"3-배치-리스너의-예외-의미론\" style=\"position:relative;\"><a href=\"#3-%EB%B0%B0%EC%B9%98-%EB%A6%AC%EC%8A%A4%EB%84%88%EC%9D%98-%EC%98%88%EC%99%B8-%EC%9D%98%EB%AF%B8%EB%A1%A0\" aria-label=\"3 배치 리스너의 예외 의미론 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>3. 배치 리스너의 예외 의미론</h3>\n<p>배치 리스너에서 예외를 그냥 던지면 <strong>배치 전체</strong>가 실패 처리된다.\n<code class=\"language-text\">BatchListenerFailedException</code> 으로 실패 인덱스를 지정하지 않으면, 깨진 메시지\n앞의 정상 메시지까지 도매금으로 묶인다. “깨진 메시지가 섞인 배치에서 정상 메시지는\n살아남는다”를 테스트로 고정해 두었다.</p>\n<h2 id=\"검증\" style=\"position:relative;\"><a href=\"#%EA%B2%80%EC%A6%9D\" aria-label=\"검증 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>검증</h2>\n<ul>\n<li>poison pill 을 정상 메시지 사이에 주입 → DLQ 로 이동, 뒤 메시지 전부 처리됨</li>\n<li>DLQ 메시지에 원인 헤더(원본 토픽/파티션/오프셋/예외) 동봉 확인\n— 참고로 Spring 은 발행 시점에 헤더를 <code class=\"language-text\">KafkaHeaders.DLT_*</code> 로 바꿔 단다. 검증할 때 이걸 몰라서 또 한 번 헤맸다</li>\n<li>재처리 runbook 문서화: DLQ 는 “버리는 곳”이 아니라 “고쳐서 되돌리는 대기열”</li>\n</ul>\n<p>상세: <a href=\"https://github.com/Shim8934/media-project/blob/main/docs/adr/0004-dlq-and-poison-pill.md\">ADR-0004</a></p>\n<hr>\n<div class=\"table-of-contents\">\n<ul>\n<li>\n<p><a href=\"#%EB%AC%B8%EC%A0%9C\">문제</a></p>\n</li>\n<li>\n<p><a href=\"#%EC%84%A4%EA%B3%84--%EC%8B%A4%ED%8C%A8%EB%A5%BC-%EB%91%90-%EC%A2%85%EB%A5%98%EB%A1%9C-%EA%B0%80%EB%A5%B8%EB%8B%A4\">설계 — 실패를 두 종류로 가른다</a></p>\n</li>\n<li>\n<p><a href=\"#%EC%82%BD%EC%A7%88-%EA%B8%B0%EB%A1%9D--%EB%AC%B8%EC%84%9C%EC%97%94-%EC%97%86%EA%B3%A0-%EA%B5%AC%ED%98%84%EC%97%90%EB%A7%8C-%EC%9E%88%EB%8A%94-%EA%B2%83%EB%93%A4\">삽질 기록 — 문서엔 없고 구현에만 있는 것들</a></p>\n<ul>\n<li><a href=\"#1-dlq-%EB%A9%94%EC%8B%9C%EC%A7%80%EA%B0%80-base64-%EB%A1%9C-%EB%B3%80%EC%A7%88\">1. DLQ 메시지가 base64 로 변질</a></li>\n<li><a href=\"#2-classcastexception-string-cannot-be-cast-to-b\">2. ClassCastException: String cannot be cast to [B</a></li>\n<li><a href=\"#3-%EB%B0%B0%EC%B9%98-%EB%A6%AC%EC%8A%A4%EB%84%88%EC%9D%98-%EC%98%88%EC%99%B8-%EC%9D%98%EB%AF%B8%EB%A1%A0\">3. 배치 리스너의 예외 의미론</a></li>\n</ul>\n</li>\n<li>\n<p><a href=\"#%EA%B2%80%EC%A6%9D\">검증</a></p>\n</li>\n</ul>\n</div>","excerpt":"문제 깨진 JSON 메시지 하나가 파티션에 들어오면 무슨 일이 벌어지나. 무한 재시도 설정이라면 그 메시지가 파티션 전체를 막는다(head-of-line blocking).\n뒤에 쌓인 정상 메시지 수만 건이 한 건 때문에 처리되지 않는다. 이게 poison pill 이다. 설계 — 실패를 두 종류로 가른다 M1 입수에서 “재시도 가능(DB 장애) vs 불가(검증 실패)“를 갈랐던 원칙을 그대로 재적용했다. 실패 유형 판단 처리 역직렬화 실패 백 번 다시 읽어도 같은 바이트는 같은 실패 재시도 없이 즉시 DLQ 처리 실패 (DB 등) 일시적일 수 있다 지수 backoff(0.5s→1s→2s) 3회 → DLQ  가 역직렬화 실패를 예외가 아니라 “값”(null + 원인 헤더)으로\n바꿔준다 — 리스너까지 도달해야 우리가 제어권을 갖는다. 고정 간격 재시도는 기각 — DB 가 과부하로 느려진 상황에서 같은 속도로 계속 두드리면\n회복을 방해한다. 지수 backoff 는 시스템에 숨 쉴 틈을…","frontmatter":{"date":"July 24, 2026","title":"음악 스트리밍 파이프라인 만들기 - 05. DLQ와 poison pill, 그리고 세 번의 삽질","categories":"Music_Pipeline","author":"shim8934","emoji":"☠️"},"fields":{"slug":"/Music_Pipeline_05/"}},"next":{"id":"e92214b5-54b4-50b0-ac8b-0c9e19a48bf5","html":"<h2 id=\"문제\" style=\"position:relative;\"><a href=\"#%EB%AC%B8%EC%A0%9C\" aria-label=\"문제 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>문제</h2>\n<p>재생 이벤트(초당 수백 건)를 소비해 곡별 재생수를 집계한다. 재생수는 <strong>카운터</strong>다 —\n두 번 더하면 그냥 틀리고, 틀렸다는 사실조차 알기 어렵다.</p>\n<p>그리고 Kafka 는 at-least-once 다. 리밸런싱, 오프셋 커밋 전 종료, 운영자의 오프셋 리셋,\n<code class=\"language-text\">max.poll.interval.ms</code> 초과 — 컨슈머는 <strong>정상 동작 중에도</strong> 같은 메시지를 다시 받는다.\n중복 소비는 장애가 아니라 전제 조건이다.</p>\n<h2 id=\"흔한-오해부터\" style=\"position:relative;\"><a href=\"#%ED%9D%94%ED%95%9C-%EC%98%A4%ED%95%B4%EB%B6%80%ED%84%B0\" aria-label=\"흔한 오해부터 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>흔한 오해부터</h2>\n<p>”<code class=\"language-text\">enable.idempotence=true</code> 켰으니 안전하다”는 말이 자주 나온다. 아니다.</p>\n<table>\n<thead>\n<tr>\n<th>설정</th>\n<th>막는 것</th>\n<th>못 막는 것</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>프로듀서 멱등성</td>\n<td>프로듀서 재시도로 인한 브로커 측 중복 저장</td>\n<td><strong>컨슈머 재소비</strong></td>\n</tr>\n<tr>\n<td>Kafka 트랜잭션(EOS)</td>\n<td>Kafka→Kafka 경로의 중복</td>\n<td><strong>외부 DB 쓰기</strong></td>\n</tr>\n</tbody>\n</table>\n<p>컨슈머가 DB 에 쓰는 순간, Kafka 가 주는 보장은 끝난다. 멱등성은 우리 몫이다.</p>\n<h2 id=\"대안-비교와-선택\" style=\"position:relative;\"><a href=\"#%EB%8C%80%EC%95%88-%EB%B9%84%EA%B5%90%EC%99%80-%EC%84%A0%ED%83%9D\" aria-label=\"대안 비교와 선택 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>대안 비교와 선택</h2>\n<table>\n<thead>\n<tr>\n<th>대안</th>\n<th>판단</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>A. eventId UNIQUE + 신규분만 집계</strong></td>\n<td>✅ 채택</td>\n</tr>\n<tr>\n<td>B. 오프셋을 DB 에 저장, 집계와 한 트랜잭션</td>\n<td>이론상 가장 정확하지만 기각</td>\n</tr>\n<tr>\n<td>C. Kafka EOS</td>\n<td>외부 DB 라 적용 불가 (오해 방지용으로 문서에 남김)</td>\n</tr>\n<tr>\n<td>D. 중복 허용 + 주기 재집계</td>\n<td>규모가 커지면 A 와 조합 재검토</td>\n</tr>\n</tbody>\n</table>\n<p>B 를 기각한 결정타는 성능이 아니라 <strong>검증 가능성</strong>이었다. A 는 중복을 일부러\n만들어(오프셋 되감기) 테스트로 증명할 수 있다. B 는 리밸런싱 시나리오 재현이 훨씬\n어려워 “아마 맞을 것”에 기대게 된다. 덤으로 B 는 <code class=\"language-text\">kafka-consumer-groups.sh</code> 로\nlag 을 보는 표준 운영 도구를 포기해야 한다.</p>\n<h2 id=\"채택한-알고리즘\" style=\"position:relative;\"><a href=\"#%EC%B1%84%ED%83%9D%ED%95%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98\" aria-label=\"채택한 알고리즘 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>채택한 알고리즘</h2>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">1. 배치 내부 중복 제거          (LinkedHashMap)\n2. 기처리 eventId 선조회        SELECT ... WHERE event_id IN (...)\n3. 신규분만 원본 적재           INSERT IGNORE batch\n4. 트랙별 델타 합산 후 UPSERT   play_count = play_count + VALUES(...)</code></pre></div>\n<ul>\n<li>INSERT IGNORE 의 <strong>반환값으로 판정하지 않는다</strong> — 02편의 <code class=\"language-text\">rewriteBatchedStatements</code>\n때문에 affected rows 가 <code class=\"language-text\">-2</code> 로 뭉개질 수 있다. 집계 정확성이 드라이버 설정에\n의존하는 것은 수용 불가. 선조회가 SELECT 한 번 비싸지만 결정적이다.</li>\n<li>커밋 순서는 <strong>DB 커밋 → 오프셋 커밋.</strong> 사이에서 죽으면 재소비되지만 멱등이 흡수한다.\n반대 순서면 이벤트가 조용히 유실된다. at-least-once + 멱등 = 사실상 exactly-once.</li>\n<li>파티션 키 = trackId. 같은 트랙이 항상 같은 파티션 → 같은 카운터 행을 두 컨슈머가\n동시에 때리는 상황이 <strong>구조적으로 없다.</strong> 대가는 인기곡 파티션 스큐고,\n시뮬레이터에 Zipf 분포를 넣어 그 스큐를 일부러 재현했다.</li>\n</ul>\n<h2 id=\"ttl-은-설정값이-아니라-안전성-조건이다\" style=\"position:relative;\"><a href=\"#ttl-%EC%9D%80-%EC%84%A4%EC%A0%95%EA%B0%92%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%95%88%EC%A0%84%EC%84%B1-%EC%A1%B0%EA%B1%B4%EC%9D%B4%EB%8B%A4\" aria-label=\"ttl 은 설정값이 아니라 안전성 조건이다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>TTL 은 설정값이 아니라 안전성 조건이다</h2>\n<p>원본 이벤트는 초당 500건이면 하루 4,300만 행이라 TTL 로 지워야 한다.\n그런데 원본 행이 곧 멱등성 판정 근거다 — 지운 뒤 도착한 중복은 신규로 집계된다.</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">TTL(play_event) > retention.ms(play-events)</code></pre></div>\n<p>브로커가 이미 지운 메시지는 재소비될 수 없으므로, 이 부등식이 성립하면 중복이 도착할 수\n있는 기간 동안 판정 근거가 반드시 남아 있다. 처음엔 문서에 “두 값을 같이 움직여야 한다”고\n적었는데, <strong>사람이 기억해야 하는 제약은 언젠가 깨진다.</strong> 그래서 기동 시점에 검증해\n어기면 애플리케이션이 뜨지 않게 만들었다(fail fast).</p>\n<h2 id=\"증명--멱등성은-깨뜨려서\" style=\"position:relative;\"><a href=\"#%EC%A6%9D%EB%AA%85--%EB%A9%B1%EB%93%B1%EC%84%B1%EC%9D%80-%EA%B9%A8%EB%9C%A8%EB%A0%A4%EC%84%9C\" aria-label=\"증명  멱등성은 깨뜨려서 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>증명 — 멱등성은 깨뜨려서</h2>\n<p><code class=\"language-text\">AdminClient.alterConsumerGroupOffsets</code> 로 오프셋을 0으로 되감아 120건을 전량\n재소비시키고 카운터 불변을 단언했다. 핵심은 <strong>duplicate 메트릭을 함께 단언</strong>한 것 —\n이게 없으면 “재소비가 안 일어나서 그대로”인 경우와 구분되지 않는다.\n테스트가 실제로 문제를 만들었는지부터 확인해야 다음 단언이 의미를 갖는다.</p>\n<p>덤으로 얻은 교훈: <code class=\"language-text\">@BeforeEach</code> 에서 DB 를 비워도 <strong>토픽의 메시지는 남는다.</strong>\n오프셋을 되감으면 다른 테스트가 발행한 이벤트까지 살아난다. 이벤트 기반 시스템에서\n“DB 정리 = 깨끗한 상태”는 성립하지 않는다 — 운영에서도 똑같다.</p>\n<p>상세: <a href=\"https://github.com/Shim8934/media-project/blob/main/docs/adr/0003-consumer-idempotency.md\">ADR-0003</a></p>\n<hr>\n<div class=\"table-of-contents\">\n<ul>\n<li><a href=\"#%EB%AC%B8%EC%A0%9C\">문제</a></li>\n<li><a href=\"#%ED%9D%94%ED%95%9C-%EC%98%A4%ED%95%B4%EB%B6%80%ED%84%B0\">흔한 오해부터</a></li>\n<li><a href=\"#%EB%8C%80%EC%95%88-%EB%B9%84%EA%B5%90%EC%99%80-%EC%84%A0%ED%83%9D\">대안 비교와 선택</a></li>\n<li><a href=\"#%EC%B1%84%ED%83%9D%ED%95%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98\">채택한 알고리즘</a></li>\n<li><a href=\"#ttl-%EC%9D%80-%EC%84%A4%EC%A0%95%EA%B0%92%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%95%88%EC%A0%84%EC%84%B1-%EC%A1%B0%EA%B1%B4%EC%9D%B4%EB%8B%A4\">TTL 은 설정값이 아니라 안전성 조건이다</a></li>\n<li><a href=\"#%EC%A6%9D%EB%AA%85--%EB%A9%B1%EB%93%B1%EC%84%B1%EC%9D%80-%EA%B9%A8%EB%9C%A8%EB%A0%A4%EC%84%9C\">증명 — 멱등성은 깨뜨려서</a></li>\n</ul>\n</div>","frontmatter":{"date":"July 24, 2026","title":"음악 스트리밍 파이프라인 만들기 - 04. Kafka 컨슈머 멱등성, 중복은 버그가 아니라 전제다","categories":"Music_Pipeline","author":"shim8934","emoji":"🔁"},"fields":{"slug":"/Music_Pipeline_04/"}},"prev":{"id":"d3715462-713f-5c22-89ad-4fd2525e72a6","html":"<h2 id=\"문제\" style=\"position:relative;\"><a href=\"#%EB%AC%B8%EC%A0%9C\" aria-label=\"문제 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>문제</h2>\n<p>집계 테이블 기반 “실시간 TOP 100 차트” API. 모든 사용자가 같은 것을 본다 —\n전형적인 읽기 편중 + <strong>핫키 하나</strong> 워크로드다. 결정이 세 겹이었다.</p>\n<h2 id=\"결정-1--차트를-어디서-만드나-redis-sorted-set-기각\" style=\"position:relative;\"><a href=\"#%EA%B2%B0%EC%A0%95-1--%EC%B0%A8%ED%8A%B8%EB%A5%BC-%EC%96%B4%EB%94%94%EC%84%9C-%EB%A7%8C%EB%93%9C%EB%82%98-redis-sorted-set-%EA%B8%B0%EA%B0%81\" aria-label=\"결정 1  차트를 어디서 만드나 redis sorted set 기각 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>결정 1 — 차트를 어디서 만드나: Redis Sorted Set 기각</h2>\n<p>“Redis 차트면 ZINCRBY + ZREVRANGE 지” 싶었는데, 기각했다. 결정타는 <strong>멱등성</strong>이다.</p>\n<p>컨슈머가 같은 배치를 재처리하면 DB 는 <code class=\"language-text\">event_id UNIQUE</code> 로 걸러내지만\n<strong>ZINCRBY 는 그대로 두 번 더해진다.</strong> 04편에서 세운 멱등성 체계 밖에 비원자\n이중 쓰기 경로를 만드는 구조다. Redis 에도 중복 판정을 만들면 되지만, 그건 같은\n체계를 두 번 구현하는 것이다. DB 집계(TOP 100 쿼리) + 캐시로 갔다.\n설계 결정은 독립적이지 않다 — <strong>앞의 결정이 뒤의 선택지를 제약한다.</strong></p>\n<h2 id=\"결정-2--look-aside\" style=\"position:relative;\"><a href=\"#%EA%B2%B0%EC%A0%95-2--look-aside\" aria-label=\"결정 2  look aside permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>결정 2 — Look-aside</h2>\n<p>Write-through 는 기각: 쓰기 주체(컨슈머, 초당 수백 건)와 읽기 주체(차트 API)가 달라서,\n아무도 안 읽는 사이에도 집계마다 TOP 100 재계산을 강요하는 꼴이다.\n차트는 엔티티 캐시가 아니라 <strong>쿼리 결과 캐시</strong>라 읽기 쪽에서 채우는 게 자연스럽다.</p>\n<h2 id=\"결정-3--스탬피드-논리물리-ttl-이원화--단일-비행\" style=\"position:relative;\"><a href=\"#%EA%B2%B0%EC%A0%95-3--%EC%8A%A4%ED%83%AC%ED%94%BC%EB%93%9C-%EB%85%BC%EB%A6%AC%EB%AC%BC%EB%A6%AC-ttl-%EC%9D%B4%EC%9B%90%ED%99%94--%EB%8B%A8%EC%9D%BC-%EB%B9%84%ED%96%89\" aria-label=\"결정 3  스탬피드 논리물리 ttl 이원화  단일 비행 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>결정 3 — 스탬피드: 논리/물리 TTL 이원화 + 단일 비행</h2>\n<p>물리 TTL 하나만 쓰면 만료 순간 동시 요청 전부가 DB 로 몰린다.\n캐시가 가장 필요한 순간에 캐시가 없다.</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">soft TTL 30s  : 논리 만료. 지나면 stale — 즉시 응답하고 갱신은 백그라운드 1개 스레드만\nhard TTL 120s : 물리 만료. Redis 가 실제로 지운다\n완전 미스      : ReentrantLock 단일 비행 — 1개 요청만 DB, 나머지는 대기 후 재확인\nRedis 장애    : DB 직접 조회 폴백 — 캐시는 성능 장치지 가용성 의존점이 아니다</code></pre></div>\n<ul>\n<li><code class=\"language-text\">soft >= hard</code> 설정은 기동 실패로 막았다 — stale 서빙 창이 없으면 이 구조가 무의미해진다.\n04편의 TTL 부등식과 같은 패턴: <strong>설정값을 안전성 조건으로 다룬다.</strong></li>\n<li>jitter 는 키가 하나라 무의미, 분산 락은 단일 인스턴스에서 인프라만 느는 거래라 보류.</li>\n<li>캐시 키도 하나 — TOP 100 전체를 캐싱하고 요청 size 는 잘라서 응답한다.</li>\n</ul>\n<h2 id=\"테스트-기법이-절반이다\" style=\"position:relative;\"><a href=\"#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%B2%95%EC%9D%B4-%EC%A0%88%EB%B0%98%EC%9D%B4%EB%8B%A4\" aria-label=\"테스트 기법이 절반이다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>테스트 기법이 절반이다</h2>\n<table>\n<thead>\n<tr>\n<th>검증 대상</th>\n<th>방법</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>캐시 히트</td>\n<td><strong>DB 를 바꾼 뒤 응답이 안 바뀌는 것</strong>으로 증명 (호출 횟수 카운팅보다 견고)</td>\n</tr>\n<tr>\n<td>stale 동작</td>\n<td>30초 대기 대신 <code class=\"language-text\">generatedAt</code> 을 과거로 조작한 캐시를 주입</td>\n</tr>\n<tr>\n<td>단일 비행</td>\n<td>빈 캐시에 10 스레드 동시 진입 → DB 호출 <strong>정확히 1회</strong> 단언</td>\n</tr>\n<tr>\n<td>Redis 장애</td>\n<td>컨테이너를 멈추지 않는다 — <strong>빈 포트를 바라보는 커넥션 팩토리를 조립.</strong> 장애는 기다리는 게 아니라 만드는 것</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"부하를-걸어-보니--예상이-뒤집혔다\" style=\"position:relative;\"><a href=\"#%EB%B6%80%ED%95%98%EB%A5%BC-%EA%B1%B8%EC%96%B4-%EB%B3%B4%EB%8B%88--%EC%98%88%EC%83%81%EC%9D%B4-%EB%92%A4%EC%A7%91%ED%98%94%EB%8B%A4\" aria-label=\"부하를 걸어 보니  예상이 뒤집혔다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>부하를 걸어 보니 — 예상이 뒤집혔다</h2>\n<p>k6 로 캐시 ON/OFF 를 비교했다 (별도 글 예정인 M4 부하 테스트의 일부).</p>\n<table>\n<thead>\n<tr>\n<th>시나리오</th>\n<th align=\"right\">처리량</th>\n<th align=\"right\">p95</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>50 VU, 캐시 ON</td>\n<td align=\"right\">479 rps</td>\n<td align=\"right\">4.2ms</td>\n</tr>\n<tr>\n<td>50 VU, 캐시 OFF</td>\n<td align=\"right\">478 rps</td>\n<td align=\"right\"><strong>5.0ms</strong></td>\n</tr>\n<tr>\n<td>200 VU 풀부하, 캐시 ON</td>\n<td align=\"right\"><strong>4,167 rps</strong></td>\n<td align=\"right\"><strong>55.7ms</strong></td>\n</tr>\n<tr>\n<td>200 VU 풀부하, 캐시 OFF</td>\n<td align=\"right\">3,562 rps</td>\n<td align=\"right\">99.3ms</td>\n</tr>\n<tr>\n<td>Redis 정지 (캐시 ON)</td>\n<td align=\"right\">81 rps</td>\n<td align=\"right\">523ms · <strong>실패율 0%</strong></td>\n</tr>\n</tbody>\n</table>\n<p>저부하에서는 <strong>캐시가 무의미했다.</strong> 인덱스 타고 버퍼 풀에 올라간 DB 도 p95 5ms 다.\n“DB 가 느려서 캐시를 얹는다”는 전제가 이 규모에선 측정으로 기각된 것.\n캐시의 진짜 역할은 고부하에서 드러났다 — 응답을 빠르게 하는 게 아니라\n<strong>DB 를 병목에서 제거</strong>한다 (p95 43% 감소, 남은 지연은 웹 계층으로 병목 이동).</p>\n<p>Redis 를 정지시킨 상태에서도 1,650 요청 실패율 0% — 다만 전 요청이 타임아웃\n500ms 를 태우고 폴백했다. “다음 개선은 서킷 브레이커”라는 결론까지가 측정의 산출물이다.</p>\n<p>상세: <a href=\"https://github.com/Shim8934/media-project/blob/main/docs/adr/0005-chart-cache-strategy.md\">ADR-0005</a></p>\n<hr>\n<div class=\"table-of-contents\">\n<ul>\n<li><a href=\"#%EB%AC%B8%EC%A0%9C\">문제</a></li>\n<li><a href=\"#%EA%B2%B0%EC%A0%95-1--%EC%B0%A8%ED%8A%B8%EB%A5%BC-%EC%96%B4%EB%94%94%EC%84%9C-%EB%A7%8C%EB%93%9C%EB%82%98-redis-sorted-set-%EA%B8%B0%EA%B0%81\">결정 1 — 차트를 어디서 만드나: Redis Sorted Set 기각</a></li>\n<li><a href=\"#%EA%B2%B0%EC%A0%95-2--look-aside\">결정 2 — Look-aside</a></li>\n<li><a href=\"#%EA%B2%B0%EC%A0%95-3--%EC%8A%A4%ED%83%AC%ED%94%BC%EB%93%9C-%EB%85%BC%EB%A6%AC%EB%AC%BC%EB%A6%AC-ttl-%EC%9D%B4%EC%9B%90%ED%99%94--%EB%8B%A8%EC%9D%BC-%EB%B9%84%ED%96%89\">결정 3 — 스탬피드: 논리/물리 TTL 이원화 + 단일 비행</a></li>\n<li><a href=\"#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%B2%95%EC%9D%B4-%EC%A0%88%EB%B0%98%EC%9D%B4%EB%8B%A4\">테스트 기법이 절반이다</a></li>\n<li><a href=\"#%EB%B6%80%ED%95%98%EB%A5%BC-%EA%B1%B8%EC%96%B4-%EB%B3%B4%EB%8B%88--%EC%98%88%EC%83%81%EC%9D%B4-%EB%92%A4%EC%A7%91%ED%98%94%EB%8B%A4\">부하를 걸어 보니 — 예상이 뒤집혔다</a></li>\n</ul>\n</div>","frontmatter":{"date":"July 24, 2026","title":"음악 스트리밍 파이프라인 만들기 - 06. Redis 캐시와 스탬피드, 그리고 캐시가 무의미했던 순간","categories":"Music_Pipeline","author":"shim8934","emoji":"⚡"},"fields":{"slug":"/Music_Pipeline_06/"}},"site":{"siteMetadata":{"siteUrl":"https://shim8934.github.io","comments":{"utterances":{"repo":"Shim8934/shim8934.github.io"}}}}},"pageContext":{"slug":"/Music_Pipeline_05/","nextSlug":"/Music_Pipeline_04/","prevSlug":"/Music_Pipeline_06/"}},
    "staticQueryHashes": ["1073350324","1956554647","2938748437"]}