{
    "componentChunkName": "component---src-templates-blog-template-js",
    "path": "/Music_Pipeline_02/",
    "result": {"data":{"cur":{"id":"c5d7d09a-9e82-5065-a87f-08c6c0216000","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>음원 메타데이터 파일 하나에 Release 5,000건, Track 50,000건이 들어온다.\nMyBatis 시절엔 <code class=\"language-text\">&lt;foreach></code> 로 multi-value insert 를 쓰면 끝이었는데,\nJPA 로 오니 <code class=\"language-text\">saveAll()</code> 이 건별 INSERT 를 날리고 있었다.</p>\n<p><code class=\"language-text\">hibernate.jdbc.batch_size: 1000</code> 을 넣어도 달라지지 않았다. <strong>경고도 로그도 없이.</strong></p>\n<h2 id=\"원인--identity-는-batch-를-조용히-끈다\" style=\"position:relative;\"><a href=\"#%EC%9B%90%EC%9D%B8--identity-%EB%8A%94-batch-%EB%A5%BC-%EC%A1%B0%EC%9A%A9%ED%9E%88-%EB%81%88%EB%8B%A4\" aria-label=\"원인  identity 는 batch 를 조용히 끈다 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>원인 — IDENTITY 는 batch 를 조용히 끈다</h2>\n<p>영속성 컨텍스트는 엔티티를 <strong>ID 를 키로</strong> 관리한다. 그런데 <code class=\"language-text\">GenerationType.IDENTITY</code> 는\nAUTO_INCREMENT 라서 <strong>INSERT 를 실제로 실행해야</strong> ID 를 알 수 있다.</p>\n<ul>\n<li>Hibernate 의 batch 는 write-behind(쓰기 지연) 위에서 동작한다 — INSERT 를 모았다가 flush 에서 묶어 보낸다</li>\n<li>IDENTITY 는 <code class=\"language-text\">persist()</code> 시점에 ID 가 필요하므로 즉시 INSERT 할 수밖에 없다</li>\n<li>그래서 Hibernate 는 IDENTITY 엔티티의 batch 를 <strong>예외 없이 포기한다</strong></li>\n</ul>\n<p>코드 차이는 애노테이션 한 줄인데, 이 동작 차이는 어디에도 표시되지 않는다.\nMyBatis 는 SQL 이 XML 에 그대로 보이니 이런 종류의 “조용한 비활성화”가 없다 —\nJPA 로 넘어올 때 가장 조심해야 할 지점이 바로 이 <strong>암묵성</strong>이라고 느꼈다.</p>\n<h2 id=\"5가지-전략을-같은-조건에서-측정\" style=\"position:relative;\"><a href=\"#5%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5%EC%9D%84-%EA%B0%99%EC%9D%80-%EC%A1%B0%EA%B1%B4%EC%97%90%EC%84%9C-%EC%B8%A1%EC%A0%95\" aria-label=\"5가지 전략을 같은 조건에서 측정 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>5가지 전략을 같은 조건에서 측정</h2>\n<p>같은 데이터(Release 1,000 / Track 10,000), 같은 트랜잭션 경계에서 쓰기 방식만 갈아끼웠다.\nTestcontainers MariaDB, 3회 반복 중앙값.</p>\n<table>\n<thead>\n<tr>\n<th>전략</th>\n<th align=\"right\">중앙값(ms)</th>\n<th align=\"right\">처리량(건/초)</th>\n<th>수행 작업</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>JPA <code class=\"language-text\">IDENTITY</code></td>\n<td align=\"right\">17,225</td>\n<td align=\"right\">581</td>\n<td>순수 INSERT</td>\n</tr>\n<tr>\n<td>JPA <code class=\"language-text\">SEQUENCE</code> (increment 1000)</td>\n<td align=\"right\"><strong>418</strong></td>\n<td align=\"right\"><strong>23,923</strong></td>\n<td>순수 INSERT</td>\n</tr>\n<tr>\n<td>JPA 앱할당 TSID</td>\n<td align=\"right\">490</td>\n<td align=\"right\">20,408</td>\n<td>순수 INSERT</td>\n</tr>\n<tr>\n<td><strong>JDBC batchUpdate + UPSERT</strong></td>\n<td align=\"right\">1,205</td>\n<td align=\"right\">8,299</td>\n<td>UPSERT + 사후 SELECT</td>\n</tr>\n<tr>\n<td>TSID + 사전 SELECT + UPSERT</td>\n<td align=\"right\">1,254</td>\n<td align=\"right\">7,974</td>\n<td>사전 SELECT + UPSERT</td>\n</tr>\n</tbody>\n</table>\n<p>IDENTITY 와 SEQUENCE 의 차이가 <strong>41배</strong>다. 애노테이션 한 줄의 값이다.</p>\n<h2 id=\"기각된-가설--이-표에서-제일-배운-것\" style=\"position:relative;\"><a href=\"#%EA%B8%B0%EA%B0%81%EB%90%9C-%EA%B0%80%EC%84%A4--%EC%9D%B4-%ED%91%9C%EC%97%90%EC%84%9C-%EC%A0%9C%EC%9D%BC-%EB%B0%B0%EC%9A%B4-%EA%B2%83\" 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>측정 전 예측은 “JDBC 직접 제어가 제일 빠를 것”이었다. 결과는 SEQUENCE 의 1/3.</p>\n<p>이유를 파 보니 표의 함정이 보였다. <strong>전략마다 수행하는 작업이 다르다.</strong>\nJDBC 행은 “재전송 시 UPSERT” 라는 요구사항을 만족하는 비용까지 포함한 수치고,\nSEQUENCE 행은 순수 INSERT 만 한 수치다. 이 표는 “같은 SQL 의 속도 비교”가 아니라\n<strong>“각 전략이 만족하는 요구사항의 비용 비교”</strong> 였던 것이다.</p>\n<p>후속 가설 “사후 SELECT 를 사전 SELECT 로 바꾸면 싸질 것”도 측정으로 기각됐다 —\n이런 조회는 결과셋 크기가 아니라 <strong>인덱스 탐색 횟수</strong>가 비용을 지배한다.</p>\n<h2 id=\"최종-선택\" style=\"position:relative;\"><a href=\"#%EC%B5%9C%EC%A2%85-%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<p>유통사가 같은 앨범을 정정 재전송하는 요구사항(UPSERT 멱등성)이 있어서\n<strong>JDBC batchUpdate + <code class=\"language-text\">ON DUPLICATE KEY UPDATE</code></strong> 를 선택했다. 최고 속도 전략이 아니라\n<strong>요구사항을 가장 싸게 만족하는</strong> 전략이다. 순수 신규 적재만 있는 시스템이라면 SEQUENCE 가 맞다.</p>\n<p>부수 팁: MariaDB JDBC 는 <code class=\"language-text\">rewriteBatchedStatements=true</code> 로 multi-value 재작성이 가능한데,\n이때 affected rows 가 <code class=\"language-text\">SUCCESS_NO_INFO(-2)</code> 로 뭉개질 수 있다.\n반환값에 로직을 걸면 안 되는 이유이며, 이 성질은 뒤의 Kafka 멱등성 글에서 다시 등장한다.</p>\n<p>상세: <a href=\"https://github.com/Shim8934/media-project/blob/main/docs/adr/0001-batch-insert-strategy.md\">ADR-0001</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=\"#%EC%9B%90%EC%9D%B8--identity-%EB%8A%94-batch-%EB%A5%BC-%EC%A1%B0%EC%9A%A9%ED%9E%88-%EB%81%88%EB%8B%A4\">원인 — IDENTITY 는 batch 를 조용히 끈다</a></li>\n<li><a href=\"#5%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5%EC%9D%84-%EA%B0%99%EC%9D%80-%EC%A1%B0%EA%B1%B4%EC%97%90%EC%84%9C-%EC%B8%A1%EC%A0%95\">5가지 전략을 같은 조건에서 측정</a></li>\n<li><a href=\"#%EA%B8%B0%EA%B0%81%EB%90%9C-%EA%B0%80%EC%84%A4--%EC%9D%B4-%ED%91%9C%EC%97%90%EC%84%9C-%EC%A0%9C%EC%9D%BC-%EB%B0%B0%EC%9A%B4-%EA%B2%83\">기각된 가설 — 이 표에서 제일 배운 것</a></li>\n<li><a href=\"#%EC%B5%9C%EC%A2%85-%EC%84%A0%ED%83%9D\">최종 선택</a></li>\n</ul>\n</div>","excerpt":"문제 음원 메타데이터 파일 하나에 Release 5,000건, Track 50,000건이 들어온다.\nMyBatis 시절엔  로 multi-value insert 를 쓰면 끝이었는데,\nJPA 로 오니  이 건별 INSERT 를 날리고 있었다.  을 넣어도 달라지지 않았다. 경고도 로그도 없이. 원인 — IDENTITY 는 batch 를 조용히 끈다 영속성 컨텍스트는 엔티티를 ID 를 키로 관리한다. 그런데  는\nAUTO_INCREMENT 라서 INSERT 를 실제로 실행해야 ID 를 알 수 있다. Hibernate 의 batch 는 write-behind(쓰기 지연) 위에서 동작한다 — INSERT 를 모았다가 flush 에서 묶어 보낸다 IDENTITY 는  시점에 ID 가 필요하므로 즉시 INSERT 할 수밖에 없다 그래서 Hibernate 는 IDENTITY 엔티티의 batch 를 예외 없이 포기한다 코드 차이는 애노테이션 한 줄인데, 이 동작 차이는 어디에도 표시되지 않는다.…","frontmatter":{"date":"July 24, 2026","title":"음악 스트리밍 파이프라인 만들기 - 02. JPA 배치 insert, IDENTITY가 41배 느린 이유","categories":"Music_Pipeline","author":"shim8934","emoji":"🚀"},"fields":{"slug":"/Music_Pipeline_02/"}},"next":{"id":"cb71b72b-21f2-5639-868c-b331fd519ed5","html":"<h2 id=\"왜-이-프로젝트인가\" style=\"position:relative;\"><a href=\"#%EC%99%9C-%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%B8%EA%B0%80\" 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>MyBatis/MariaDB 중심으로 4~5년을 일했고, JPA·Kafka·Redis 는 “써봤다”와 “설계할 수 있다” 사이 어딘가에 있었다.\n그 간극을 메우려고 음악 스트리밍 서비스의 백엔드 데이터 파이프라인을 축소 구현했다.</p>\n<p>두 개의 큰 흐름으로 구성된다.</p>\n<ol>\n<li><strong>음원 메타데이터 입수</strong> — 유통사가 보내는 DDEX 스타일 XML 을 스트리밍 파싱·검증해 배치 적재</li>\n<li><strong>재생 이벤트 처리</strong> — Kafka 로 초당 수백 건의 재생 이벤트를 수집, 멱등하게 집계해 실시간 차트 제공</li>\n</ol>\n<p>이 도메인을 고른 이유는 두 흐름의 <strong>실패 모델이 정반대</strong>라서다.\n입수는 “파일 하나가 크고 실패가 부분적” — 트랜잭션 경계 문제.\n이벤트는 “건수가 많고 중복이 정상” — 멱등성 문제.\n한 프로젝트 안에서 서로 다른 정합성 전략을 비교할 수 있다.</p>\n<ul>\n<li>저장소: <a href=\"https://github.com/Shim8934/media-project\">github.com/Shim8934/media-project</a></li>\n<li>스택: Java 21, Spring Boot 3.4, JPA + JdbcTemplate, MariaDB, Kafka, Redis, Testcontainers, k6, ffmpeg</li>\n</ul>\n<h2 id=\"마일스톤-지도\" style=\"position:relative;\"><a href=\"#%EB%A7%88%EC%9D%BC%EC%8A%A4%ED%86%A4-%EC%A7%80%EB%8F%84\" 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<th>시리즈 글</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>M1</td>\n<td>도메인 모델링, 배치 insert 최적화, 실패 기록·재처리</td>\n<td>02, 03</td>\n</tr>\n<tr>\n<td>M2</td>\n<td>Kafka 파이프라인 — 멱등 집계, DLQ, poison pill</td>\n<td>04, 05</td>\n</tr>\n<tr>\n<td>M3</td>\n<td>실시간 TOP 100 차트 + Redis 캐싱</td>\n<td>06</td>\n</tr>\n<tr>\n<td>M4</td>\n<td>성능 검증 — N+1 수치 비교, 트랜잭션 반증, k6 부하</td>\n<td>07</td>\n</tr>\n<tr>\n<td>M5</td>\n<td>HLS 오디오 스트리밍 (선택 모듈)</td>\n<td>08</td>\n</tr>\n<tr>\n<td>번외</td>\n<td>로컬 Docker DB 가 랜섬 공격당한 이야기</td>\n<td>09</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"프로젝트를-관통한-세-가지-원칙\" style=\"position:relative;\"><a href=\"#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EA%B4%80%ED%86%B5%ED%95%9C-%EC%84%B8-%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99\" 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-결정은-adr-로-남긴다\" style=\"position:relative;\"><a href=\"#1-%EA%B2%B0%EC%A0%95%EC%9D%80-adr-%EB%A1%9C-%EB%82%A8%EA%B8%B4%EB%8B%A4\" aria-label=\"1 결정은 adr 로 남긴다 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. 결정은 ADR 로 남긴다</h3>\n<p>ADR(Architecture Decision Record, 아키텍처 의사결정 기록)은 Michael Nygard 가 제안한\n문서 형식이다. 아키텍처에 영향을 주는 결정 하나마다 “상황(Context) → 결정(Decision) →\n결과(Consequences)” 구조의 짧은 문서를 번호 붙여 남기고, 결정이 바뀌면 문서를 고치는 게\n아니라 새 ADR 이 이전 것을 대체(supersede)한다고 기록한다 — 코드에 깃 히스토리가 있듯\n결정에도 이력을 남기는 것이다.</p>\n<p>이 프로젝트는 여기에 “검토한 대안들”과 “재검토 트리거”를 더해 6편을 썼다.\n코드가 보여주지 못하는 것은 “왜 그렇게 했나”보다 <strong>“무엇을 하지 않기로 했나”</strong> 다.\n기각안의 기각 사유가 없으면 6개월 뒤에 같은 논의를 반복한다.</p>\n<h3 id=\"2-측정-없이-믿지-않는다--기각된-가설도-기록한다\" style=\"position:relative;\"><a href=\"#2-%EC%B8%A1%EC%A0%95-%EC%97%86%EC%9D%B4-%EB%AF%BF%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4--%EA%B8%B0%EA%B0%81%EB%90%9C-%EA%B0%80%EC%84%A4%EB%8F%84-%EA%B8%B0%EB%A1%9D%ED%95%9C%EB%8B%A4\" aria-label=\"2 측정 없이 믿지 않는다  기각된 가설도 기록한다 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. 측정 없이 믿지 않는다 — 기각된 가설도 기록한다</h3>\n<p>이 프로젝트에서 사전 예측이 측정으로 뒤집힌 것이 네 번이다.</p>\n<ol>\n<li>“JDBC batch 가 제일 빠를 것” → 아니었다 (수행하는 작업이 다르다)</li>\n<li>“fetch join 이 N+1 의 정답” → 쿼리는 줄지만 메모리 페이징이라는 숨은 비용</li>\n<li>“DB 가 느려서 캐시가 필요” → 저부하에선 캐시 없이도 p95 5ms</li>\n<li>“온디맨드 패키징도 1초쯤” → 실측 5.4초</li>\n</ol>\n<p>틀린 예측이 왜 틀렸는지가 맞은 결정보다 배운 게 많았다. 각 글에서 자세히 다룬다.</p>\n<h3 id=\"3-깨뜨려서-증명한다\" style=\"position:relative;\"><a href=\"#3-%EA%B9%A8%EB%9C%A8%EB%A0%A4%EC%84%9C-%EC%A6%9D%EB%AA%85%ED%95%9C%EB%8B%A4\" 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>“두 번 호출해도 결과가 같더라”는 함수의 성질 확인일 뿐이다. 실제로 문제 상황을 만들어야 증명이 된다.</p>\n<ul>\n<li>멱등성 → Kafka 오프셋을 0으로 되감아 전량 재소비시킨다</li>\n<li>트랜잭션 경계 → 기각한 설계(REQUIRED)를 일부러 재현해서 실패 기록이 사라지는 걸 보여준다</li>\n<li>캐시 stale → 30초를 기다리는 대신 생성 시각을 과거로 조작한 캐시를 주입한다</li>\n<li>Redis 장애 → 컨테이너를 멈추는 게 아니라 “아무도 안 듣는 포트”를 바라보는 클라이언트를 조립한다</li>\n</ul>\n<p>다음 글부터 마일스톤 순서로 간다. 첫 주제는 JPA 로 넘어온 MyBatis 개발자가\n가장 먼저 부딪히는 벽 — <strong>배치 insert 가 41배 느려지는 이유</strong>다.</p>\n<hr>\n<div class=\"table-of-contents\">\n<ul>\n<li>\n<p><a href=\"#%EC%99%9C-%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%B8%EA%B0%80\">왜 이 프로젝트인가</a></p>\n</li>\n<li>\n<p><a href=\"#%EB%A7%88%EC%9D%BC%EC%8A%A4%ED%86%A4-%EC%A7%80%EB%8F%84\">마일스톤 지도</a></p>\n</li>\n<li>\n<p><a href=\"#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EA%B4%80%ED%86%B5%ED%95%9C-%EC%84%B8-%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99\">프로젝트를 관통한 세 가지 원칙</a></p>\n<ul>\n<li><a href=\"#1-%EA%B2%B0%EC%A0%95%EC%9D%80-adr-%EB%A1%9C-%EB%82%A8%EA%B8%B4%EB%8B%A4\">1. 결정은 ADR 로 남긴다</a></li>\n<li><a href=\"#2-%EC%B8%A1%EC%A0%95-%EC%97%86%EC%9D%B4-%EB%AF%BF%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4--%EA%B8%B0%EA%B0%81%EB%90%9C-%EA%B0%80%EC%84%A4%EB%8F%84-%EA%B8%B0%EB%A1%9D%ED%95%9C%EB%8B%A4\">2. 측정 없이 믿지 않는다 — 기각된 가설도 기록한다</a></li>\n<li><a href=\"#3-%EA%B9%A8%EB%9C%A8%EB%A0%A4%EC%84%9C-%EC%A6%9D%EB%AA%85%ED%95%9C%EB%8B%A4\">3. 깨뜨려서 증명한다</a></li>\n</ul>\n</li>\n</ul>\n</div>","frontmatter":{"date":"July 24, 2026","title":"음악 스트리밍 파이프라인 만들기 - 01. 프로젝트 소개와 세 가지 원칙","categories":"Music_Pipeline","author":"shim8934","emoji":"🎵"},"fields":{"slug":"/Music_Pipeline_01/"}},"prev":{"id":"d0d7ea0e-12e8-527a-af70-666f74d2ac39","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>5,000건짜리 입수 파일 처리 중 3,001번째에서 실패하면 어떻게 해야 하나.</p>\n<p>가장 쉬운 답은 “파일 전체를 한 트랜잭션으로 묶고 실패하면 전량 롤백”이다.\n깨끗해 보이지만, 이 프로젝트에서 이 답을 <strong>일부러 구현해서 깨뜨렸다.</strong> 결론부터:</p>\n<blockquote>\n<p>전량 롤백은 데이터만 되돌리는 게 아니라 <strong>실패 원인 기록까지 지운다.</strong>\n그 파일을 다시 넣으면 같은 지점에서 같은 실패를 반복한다.</p>\n</blockquote>\n<h2 id=\"경계-설계-4가지\" style=\"position:relative;\"><a href=\"#%EA%B2%BD%EA%B3%84-%EC%84%A4%EA%B3%84-4%EA%B0%80%EC%A7%80\" aria-label=\"경계 설계 4가지 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>경계 설계 4가지</h2>\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>Delivery 등록</td>\n<td><code class=\"language-text\">REQUIRES_NEW</code> (즉시 커밋)</td>\n<td>messageId UNIQUE 가 중복 실행 차단 장치 — 커밋돼야 다른 트랜잭션에 보인다</td>\n</tr>\n<tr>\n<td>검증 실패 기록</td>\n<td><code class=\"language-text\">REQUIRES_NEW</code></td>\n<td><strong>롤백에 휩쓸리면 안 된다.</strong> 실패 기록은 재처리의 입력이다</td>\n</tr>\n<tr>\n<td>청크(500건) 적재</td>\n<td><code class=\"language-text\">REQUIRED</code></td>\n<td>커밋 단위. 건별 커밋은 fsync 비용, 파일 전체는 undo/락 비용</td>\n</tr>\n<tr>\n<td>사후 검증 실패</td>\n<td>보상 삭제</td>\n<td>이미 커밋된 것은 롤백으로 못 되돌린다</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"보상-삭제가-필요한-구조적-이유\" style=\"position:relative;\"><a href=\"#%EB%B3%B4%EC%83%81-%EC%82%AD%EC%A0%9C%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B5%AC%EC%A1%B0%EC%A0%81-%EC%9D%B4%EC%9C%A0\" 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>보상 삭제가 필요한 구조적 이유</h3>\n<p>스트리밍 파싱(StAX)이라 헤더의 <code class=\"language-text\">ReleaseCount</code> 와 실제 개수가 맞는지는 <strong>파일 끝까지\n읽어야</strong> 안다. 그때는 이미 여러 청크가 커밋된 뒤다. 그래서 앨범마다 <code class=\"language-text\">delivery_id</code> 를\n추적 컬럼으로 남기고, 사후 검증 실패 시 “이 파일이 넣은 것만” 지운다.\n트랜잭션으로 못 되돌리는 것을 보상 트랜잭션으로 되돌리는 패턴이다.</p>\n<h2 id=\"self-invocation--선언적-트랜잭션의-1번-지뢰\" style=\"position:relative;\"><a href=\"#self-invocation--%EC%84%A0%EC%96%B8%EC%A0%81-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-1%EB%B2%88-%EC%A7%80%EB%A2%B0\" aria-label=\"self invocation  선언적 트랜잭션의 1번 지뢰 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>self-invocation — 선언적 트랜잭션의 1번 지뢰</h2>\n<p><code class=\"language-text\">@Transactional</code> 은 Spring 프록시가 가로채서 동작한다. 같은 클래스 안에서\n<code class=\"language-text\">this.recordFailure()</code> 로 부르면 프록시를 안 거치므로 <strong>애노테이션이 조용히 무시된다.</strong>\nREQUIRES_NEW 를 붙여놨는데 실패 기록이 롤백에 휩쓸려 사라지는, 원인 찾기 어려운 버그가 된다.</p>\n<p>해결은 <strong>트랜잭션 경계를 소유하는 별도 빈</strong>(<code class=\"language-text\">IngestTransactionSupport</code>)을 두는 것.\nself-injection 이나 AopContext 도 있지만, 별도 클래스가 “여기가 경계다”를 구조로 드러낸다.\nMyBatis 시절엔 트랜잭션을 한 곳에서 명시적으로 열고 닫아 이 함정을 밟을 일이 적었다 —\n선언적 트랜잭션의 편리함은 이런 암묵성과 맞바꾼 것이다.</p>\n<p>이 패턴은 프로젝트에서 세 번 재사용됐다 (입수, TTL 정리 배치, HLS 자산 준비).</p>\n<h2 id=\"깨뜨려서-증명--반증-테스트-4종\" style=\"position:relative;\"><a href=\"#%EA%B9%A8%EB%9C%A8%EB%A0%A4%EC%84%9C-%EC%A6%9D%EB%AA%85--%EB%B0%98%EC%A6%9D-%ED%85%8C%EC%8A%A4%ED%8A%B8-4%EC%A2%85\" aria-label=\"깨뜨려서 증명  반증 테스트 4종 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>깨뜨려서 증명 — 반증 테스트 4종</h2>\n<p>주장만으로는 부족해서, 기각한 설계를 일부러 재현하는 테스트를 만들었다.</p>\n<table>\n<thead>\n<tr>\n<th>시나리오</th>\n<th>결과</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>실패 기록을 REQUIRED 로 + 호출자 롤백</td>\n<td>기록 소멸 — “왜 실패했는지 모르는 채 데이터만 없는” 상태</td>\n</tr>\n<tr>\n<td>실제 구현(REQUIRES_NEW) + 같은 롤백</td>\n<td>기록 생존</td>\n</tr>\n<tr>\n<td>청크1 커밋 후 청크2 실패</td>\n<td>청크1 잔존(전부도 0도 아님) → 보상 삭제가 마무리</td>\n</tr>\n<tr>\n<td>파일 전체 단일 트랜잭션</td>\n<td>원자성은 완벽, <strong>실패 기록까지 소멸</strong></td>\n</tr>\n</tbody>\n</table>\n<p>네 번째 줄이 이 글의 요지다. “깨끗한 롤백”의 실체는 <strong>디버깅 근거의 소실</strong>이었다.</p>\n<p>상세: <a href=\"https://github.com/Shim8934/media-project/blob/main/docs/adr/0002-transaction-boundary.md\">ADR-0002</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=\"#%EA%B2%BD%EA%B3%84-%EC%84%A4%EA%B3%84-4%EA%B0%80%EC%A7%80\">경계 설계 4가지</a></p>\n<ul>\n<li><a href=\"#%EB%B3%B4%EC%83%81-%EC%82%AD%EC%A0%9C%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B5%AC%EC%A1%B0%EC%A0%81-%EC%9D%B4%EC%9C%A0\">보상 삭제가 필요한 구조적 이유</a></li>\n</ul>\n</li>\n<li>\n<p><a href=\"#self-invocation--%EC%84%A0%EC%96%B8%EC%A0%81-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-1%EB%B2%88-%EC%A7%80%EB%A2%B0\">self-invocation — 선언적 트랜잭션의 1번 지뢰</a></p>\n</li>\n<li>\n<p><a href=\"#%EA%B9%A8%EB%9C%A8%EB%A0%A4%EC%84%9C-%EC%A6%9D%EB%AA%85--%EB%B0%98%EC%A6%9D-%ED%85%8C%EC%8A%A4%ED%8A%B8-4%EC%A2%85\">깨뜨려서 증명 — 반증 테스트 4종</a></p>\n</li>\n</ul>\n</div>","frontmatter":{"date":"July 24, 2026","title":"음악 스트리밍 파이프라인 만들기 - 03. 트랜잭션 경계, 전량 롤백이 정답이 아닌 이유","categories":"Music_Pipeline","author":"shim8934","emoji":"🧱"},"fields":{"slug":"/Music_Pipeline_03/"}},"site":{"siteMetadata":{"siteUrl":"https://shim8934.github.io","comments":{"utterances":{"repo":"Shim8934/shim8934.github.io"}}}}},"pageContext":{"slug":"/Music_Pipeline_02/","nextSlug":"/Music_Pipeline_01/","prevSlug":"/Music_Pipeline_03/"}},
    "staticQueryHashes": ["1073350324","1956554647","2938748437"]}