<?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>Read-Through on jyukki's Blog</title><link>https://jyukki.com/tags/read-through/</link><description>Recent content in Read-Through on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Sat, 02 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/read-through/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Cache-Aside, Read-Through, Write-Through, Write-Behind를 workload 기준으로 고르는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-cache-pattern-selection-workload-playbook/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-cache-pattern-selection-workload-playbook/</guid><description>캐시 패턴은 취향이 아니라 workload와 데이터 신뢰도 요구에 맞춰 골라야 합니다. Cache-Aside, Read-Through, Write-Through, Write-Behind를 실무 숫자와 조건으로 비교합니다.</description><content:encoded><![CDATA[<p>캐시 설계에서 제일 흔한 실수는 &ldquo;우리 서비스는 Redis를 쓰니까 캐시 전략도 이미 정해졌다&quot;고 생각하는 것입니다. 실제로는 Redis를 붙였다는 사실보다 <strong>어떤 읽기/쓰기 패턴을 선택했는지</strong>가 장애 모양과 운영 비용을 더 크게 좌우합니다. 같은 상품 조회 서비스라도 <code>Cache-Aside</code>를 쓰느냐, <code>Write-Through</code>를 쓰느냐에 따라 stale read가 나는 시점, DB 부하가 튀는 방식, 장애 때 복구 순서가 전부 달라집니다.</p>
<p>특히 캐시는 성능 도구처럼 보이지만, 실무에서는 <strong>정합성, 운영 단순성, 장애 복구성</strong>까지 함께 건드립니다. 그래서 패턴 선택은 &ldquo;더 빠른가&rdquo; 한 줄로 끝나지 않습니다. 이 글은 <a href="/learning/deep-dive/deep-dive-cache-consistency-invalidation-playbook/">캐시 일관성 설계</a>, <a href="/learning/deep-dive/deep-dive-redis-caching/">Redis 캐싱</a>, <a href="/learning/deep-dive/deep-dive-request-coalescing-singleflight/">Request Coalescing/Singleflight</a>, <a href="/learning/deep-dive/deep-dive-cache-warmup-cold-start-playbook/">캐시 워밍과 콜드 스타트</a>에서 이어지는 관점으로, 패턴 자체를 <strong>workload 기준으로 고르는 프레임</strong>에 집중해 정리합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>Cache-Aside, Read-Through, Write-Through, Write-Behind가 각각 어떤 워크로드에서 유리한지 구분할 수 있습니다.</li>
<li>read/write 비율, stale 허용 시간, 쓰기 증폭, 장애 복구 방식 같은 실무 기준을 숫자로 잡을 수 있습니다.</li>
<li>패턴을 하나만 고집하지 않고, 읽기 경로와 쓰기 경로를 분리해서 조합하는 방법을 이해할 수 있습니다.</li>
<li>결제, 재고, 피드, 분석 이벤트처럼 성격이 다른 데이터를 같은 캐시 규칙으로 다루면 왜 사고가 나는지 설명할 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-캐시-패턴은-자료구조가-아니라-데이터-계약이다">1) 캐시 패턴은 자료구조가 아니라 데이터 계약이다</h3>
<p>패턴 이름만 보면 구현 스타일 차이처럼 보이지만, 실제로는 데이터 계약에 가깝습니다. 예를 들어 <code>Cache-Aside</code>는 &ldquo;읽기 시점에 없으면 애플리케이션이 채운다&quot;는 계약이고, <code>Write-Through</code>는 &ldquo;쓰기 성공 시 캐시도 함께 최신화된다&quot;는 계약입니다. <code>Write-Behind</code>는 더 강하게, &ldquo;사용자 쓰기 성공과 원본 영속화 시점이 분리될 수 있다&quot;는 계약을 받아들이는 패턴입니다.</p>
<p>그래서 먼저 정해야 할 것은 캐시 기술이 아니라 아래 네 가지입니다.</p>
<ol>
<li>이 데이터는 <strong>얼마나 오래 stale해도 되는가</strong></li>
<li>miss가 몰릴 때 <strong>원본 시스템이 얼마나 버틸 수 있는가</strong></li>
<li>쓰기 실패나 지연이 났을 때 <strong>복구 경로를 자동화할 수 있는가</strong></li>
<li>운영자가 문제를 봤을 때 <strong>어디부터 의심해야 하는가</strong></li>
</ol>
<p>이 네 질문에 답하지 않은 상태에서 패턴부터 고르면, 구현은 빨라도 운영은 자주 흔들립니다.</p>
<h3 id="2-cache-aside는-가장-무난하지만-miss-폭증과-무효화-비용을-숨긴다">2) Cache-Aside는 가장 무난하지만, miss 폭증과 무효화 비용을 숨긴다</h3>
<p><code>Cache-Aside</code>는 애플리케이션이 캐시를 먼저 읽고, miss면 DB를 읽어 캐시에 채우는 방식입니다. 구현이 단순하고 프레임워크 의존이 적어서 기본값으로 많이 씁니다. 실제로 아래 조건이면 아직도 가장 좋은 출발점인 경우가 많습니다.</p>
<ul>
<li>read/write 비율이 <strong>10:1 이상</strong></li>
<li>stale 허용 시간이 <strong>수 초~수 분</strong></li>
<li>miss 시 원본 조회가 <strong>수십 ms~수백 ms</strong> 안에서 버틴다</li>
<li>키 무효화 규칙을 도메인 팀이 직접 이해하고 관리할 수 있다</li>
</ul>
<p>문제는 miss 폭증입니다. TTL이 같은 키에 몰려 있거나 핫키 비중이 높으면 캐시가 비는 순간 DB가 직접 충격을 받습니다. 그래서 <code>Cache-Aside</code>를 고를 때는 <a href="/learning/deep-dive/deep-dive-request-coalescing-singleflight/">Request Coalescing/Singleflight</a>나 TTL 지터, soft TTL 같은 보조 장치를 같이 봐야 합니다. 운영 기준으로는 <code>cache_miss_burst_qps / steady_state_qps</code>가 <strong>3배 이상</strong> 반복되면 단순 Cache-Aside만으로는 부족한 신호로 보는 편이 안전합니다.</p>
<h3 id="3-read-through는-읽기-경로를-단순화하지만-플랫폼-책임이-필요하다">3) Read-Through는 읽기 경로를 단순화하지만, 플랫폼 책임이 필요하다</h3>
<p><code>Read-Through</code>는 애플리케이션이 miss 처리 로직을 직접 들고 있지 않고, 캐시 계층이 원본 조회와 채우기를 대신합니다. 이 패턴의 장점은 읽기 경로를 한곳으로 모을 수 있다는 점입니다. 여러 서비스가 같은 키를 읽는데 miss 처리 규칙도 같다면, 중복 구현을 줄이기 좋습니다.</p>
<p>다만 이 방식이 잘 맞으려면 전제가 있습니다.</p>
<ul>
<li>캐시 계층 또는 플랫폼 팀이 <strong>원본 조회 규칙</strong>을 소유할 수 있어야 함</li>
<li>key schema, TTL, fallback, timeout 정책이 서비스마다 크게 다르지 않아야 함</li>
<li>miss 처리 실패가 났을 때 애플리케이션이 &ldquo;왜 비었는지&rdquo; 추적할 관측 지표가 있어야 함</li>
</ul>
<p>즉 Read-Through는 기술적으로 더 고급이라기보다, <strong>조직적으로 공통 읽기 계층을 운영할 준비가 된 팀</strong>에게 맞습니다. 서비스별 비즈니스 규칙 차이가 큰데도 억지로 중앙화하면, 공통 레이어가 오히려 병목이 됩니다.</p>
<h3 id="4-write-through는-stale-read를-줄이지만-쓰기-경로의-비용을-숨기지-못한다">4) Write-Through는 stale read를 줄이지만, 쓰기 경로의 비용을 숨기지 못한다</h3>
<p><code>Write-Through</code>는 DB나 원본 저장소에 쓰는 시점에 캐시도 함께 갱신합니다. 그래서 읽기 일관성이 중요한 서비스에서 자주 검토합니다. 예를 들어 상품 가격, 사용자 권한, 설정 플래그처럼 &ldquo;방금 바꾼 값이 바로 보여야 한다&quot;는 요구가 강하면 Cache-Aside보다 예측 가능성이 좋습니다.</p>
<p>하지만 이 패턴은 읽기 성능을 위해 쓰기 경로를 무겁게 만듭니다. 아래 조건이 동시에 보이면 신중해야 합니다.</p>
<ul>
<li>write QPS가 평시 <strong>1,000/sec 이상</strong>으로 높다</li>
<li>쓰기 payload가 커서 캐시 직렬화 비용이 무시되지 않는다</li>
<li>같은 쓰기가 여러 secondary key를 갱신해야 한다</li>
<li>캐시 업데이트 실패 시 재시도 또는 보정 파이프라인이 없다</li>
</ul>
<p>실무에선 <code>Write-Through</code>를 쓴다고 해도 &ldquo;DB commit + cache set + invalidation event&quot;를 완전 동기 경로로 몰아넣지 않는 편이 많습니다. 핵심은 <strong>즉시 최신성</strong>이 필요한 키만 좁게 적용하고, 나머지는 <a href="/learning/deep-dive/deep-dive-cache-consistency-invalidation-playbook/">캐시 일관성 설계</a>나 <a href="/learning/deep-dive/deep-dive-transactional-outbox-cdc/">Transactional Outbox + CDC</a>로 보정하는 것입니다. 권장 출발선으로는 <code>write_latency_p95</code> 증가 허용치를 기존 대비 <strong>20% 이내</strong>로 두고, 그 이상이면 전면 Write-Through 확대를 멈추는 편이 낫습니다.</p>
<h3 id="5-write-behind는-처리량에는-유리하지만-정합성-예산이-없는-도메인에는-위험하다">5) Write-Behind는 처리량에는 유리하지만, 정합성 예산이 없는 도메인에는 위험하다</h3>
<p><code>Write-Behind</code>는 애플리케이션 또는 캐시 계층이 먼저 쓰기를 받아들이고, 원본 영속화는 뒤로 미루는 방식입니다. 배치성 집계, 랭킹 점수, 비핵심 활동 로그처럼 burst가 크고 약한 eventual consistency를 받아들일 수 있는 곳에서는 꽤 강합니다. 원본 저장소에 직접 쓰기 압력을 덜 주고, burst 흡수력이 좋아지기 때문입니다.</p>
<p>반대로 아래 데이터에는 보수적으로 봐야 합니다.</p>
<ul>
<li>결제, 포인트, 재고처럼 <strong>유실 비용이 큰 데이터</strong></li>
<li>사용자에게 즉시 보이는 상태 변경</li>
<li>쓰기 순서 역전이 비즈니스 오류로 이어지는 데이터</li>
<li>운영자가 replay reason 없이 수동 재처리하면 사고가 나는 경로</li>
</ul>
<p>실무 기준으로는 <code>durable queue</code>, <code>idempotency key</code>, <code>dead letter/quarantine</code>, <code>reconciliation</code>이 없으면 Write-Behind를 운영 패턴으로 부르기 어렵습니다. 그냥 &ldquo;나중에 몰아서 저장&quot;은 패턴이 아니라 리스크입니다. 적어도 <code>write_behind_backlog_age_p95</code>를 <strong>30초 이하</strong>, <code>flush_fail_rate</code>를 <strong>0.1% 이하</strong>로 유지할 자신이 없으면 핵심 데이터엔 적용 범위를 좁혀야 합니다.</p>
<h3 id="6-대부분의-시스템은-패턴-하나가-아니라-조합으로-굴러간다">6) 대부분의 시스템은 패턴 하나가 아니라 조합으로 굴러간다</h3>
<p>현실 서비스는 하나의 패턴만으로 닫히지 않습니다. 예를 들어 상품 상세는 <code>Cache-Aside</code>, 가격 변경은 <code>Write-Through</code>, 분석 집계는 <code>Write-Behind</code>, 공통 사용자 프로필은 <code>Read-Through</code>처럼 섞이는 경우가 더 흔합니다. 중요한 건 &ldquo;한 서비스는 한 패턴&quot;이 아니라 <strong>한 데이터 계약은 한 패턴</strong>에 가깝다는 점입니다.</p>
<p>그래서 팀이 먼저 분리해야 할 것은 테이블이 아니라 데이터 클래스입니다.</p>
<ul>
<li>강한 최신성이 필요한가</li>
<li>읽기 폭증이 큰가</li>
<li>쓰기 폭증이 큰가</li>
<li>재처리와 보정이 가능한가</li>
<li>운영 설명 가능성이 중요한가</li>
</ul>
<p>이 다섯 축을 먼저 나누면 패턴 선택이 훨씬 덜 감정적이 됩니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-빠른-선택-매트릭스">1) 빠른 선택 매트릭스</h3>
<table>
  <thead>
      <tr>
          <th>조건</th>
          <th>우선 검토 패턴</th>
          <th>이유</th>
          <th>주의점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>읽기 압도적, stale 수 초 허용</td>
          <td>Cache-Aside</td>
          <td>구현 단순, 확장 쉬움</td>
          <td>stampede 방지 필수</td>
      </tr>
      <tr>
          <td>여러 서비스가 같은 조회 규칙 공유</td>
          <td>Read-Through</td>
          <td>공통 miss 처리 중앙화</td>
          <td>플랫폼 관측성 필요</td>
      </tr>
      <tr>
          <td>방금 쓴 값이 바로 보여야 함</td>
          <td>Write-Through</td>
          <td>최신성 예측 가능</td>
          <td>쓰기 지연 증가</td>
      </tr>
      <tr>
          <td>burst write 흡수, 약한 eventual consistency 허용</td>
          <td>Write-Behind</td>
          <td>쓰기 압력 완화</td>
          <td>queue/replay 없으면 위험</td>
      </tr>
  </tbody>
</table>
<p>이 매트릭스에서 중요한 것은 &ldquo;더 현대적&quot;인 패턴이 있는가가 아니라, <strong>문제의 중심이 읽기인지 쓰기인지</strong> 먼저 가르는 것입니다. 읽기 장애가 반복되면 Cache-Aside 계열을 먼저 보고, 쓰기 최신성이 문제면 Write-Through를, 쓰기 burst가 본체면 Write-Behind를 보는 식이 더 낫습니다.</p>
<h3 id="2-숫자로-시작하는-의사결정-기준">2) 숫자로 시작하는 의사결정 기준</h3>
<p>초기 기준은 아래처럼 잡으면 실무에서 크게 벗어나지 않습니다.</p>
<ul>
<li><code>read/write ratio &gt;= 20:1</code>, stale 허용 <code>&gt;= 3초</code> → Cache-Aside 우선</li>
<li>공통 조회 서비스 3개 이상, miss 처리 로직 중복이 잦음 → Read-Through 검토</li>
<li>사용자 체감 stale 허용 <code>&lt;= 1초</code>, write QPS가 감당 가능 → Write-Through 부분 적용</li>
<li>write burst가 평시 대비 <code>5배 이상</code>, backlog replay 자동화 가능 → Write-Behind 제한 적용</li>
<li>cache miss burst 때 DB CPU가 <strong>15%p 이상</strong> 튄다 → Cache-Aside만으로 운영 금지, coalescing 보강</li>
<li>cache update 실패가 사용자 오류로 직결된다 → 비동기 보정보다 동기 보장 또는 강한 invalidate 우선</li>
</ul>
<p>우선순위는 보통 이 순서가 안전합니다.</p>
<ol>
<li>데이터 오류 방지</li>
<li>원본 저장소 보호</li>
<li>운영 복구 가능성 확보</li>
<li>평균 지연 최적화</li>
</ol>
<p>많은 팀이 4번부터 건드리다가 나중에 1~3번 비용을 더 크게 냅니다.</p>
<h3 id="3-도메인별-권장-출발점">3) 도메인별 권장 출발점</h3>
<ul>
<li><strong>상품/콘텐츠 조회</strong>: Cache-Aside + TTL 지터 + singleflight</li>
<li><strong>사용자 권한/설정</strong>: Write-Through 또는 강한 invalidate + 짧은 TTL</li>
<li><strong>피드/랭킹</strong>: Cache-Aside + precompute, 경우에 따라 Write-Behind 집계</li>
<li><strong>정산/원장성 데이터</strong>: 캐시 범위 최소화, 보여주기용 read model만 별도 캐시</li>
<li><strong>분석 이벤트/비핵심 카운터</strong>: Write-Behind 가능, 단 durable queue 전제</li>
</ul>
<p>핵심은 &ldquo;Redis를 어디에 붙일까&quot;가 아니라, <strong>오류가 나도 어떤 데이터는 늦어도 되고 어떤 데이터는 틀리면 안 되는가</strong>를 분리하는 것입니다.</p>
<h3 id="4-운영-대시보드-최소-구성">4) 운영 대시보드 최소 구성</h3>
<p>패턴을 골랐다면 아래 지표는 같이 봐야 합니다.</p>
<ul>
<li><code>cache_hit_ratio</code></li>
<li><code>cache_miss_burst_qps</code></li>
<li><code>stale_read_detected_rate</code></li>
<li><code>cache_fill_latency_p95</code></li>
<li><code>write_latency_p95</code></li>
<li><code>write_behind_backlog_age_p95</code></li>
<li><code>invalidation_lag_p95</code></li>
</ul>
<p>패턴별로 보는 포인트도 다릅니다. Cache-Aside는 miss burst와 fill latency, Write-Through는 write latency와 cache update failure, Write-Behind는 backlog age와 replay success rate를 우선 봐야 합니다. 같은 대시보드라도 어디를 먼저 읽을지가 달라야 운영 판단이 빨라집니다.</p>
<h3 id="5-2주-파일럿-권장-순서">5) 2주 파일럿 권장 순서</h3>
<p><strong>1주차</strong><br>
현재 캐시 대상 10개를 골라 <code>stale 허용 시간</code>, <code>read/write 비율</code>, <code>원본 조회 비용</code>, <code>쓰기 유실 비용</code>을 표로 적습니다.</p>
<p><strong>2주차</strong><br>
상위 2개 데이터 클래스에만 패턴을 다시 매깁니다. 예를 들어 상품 조회는 Cache-Aside를 유지하되 singleflight를 붙이고, 사용자 설정은 Write-Through 또는 짧은 invalidate 전략으로 바꾸는 식입니다.</p>
<p>중요한 건 한 번에 전면 교체하지 않는 것입니다. 캐시 패턴은 기능보다 운영 관성에 더 깊게 붙어 있어서, 한 구간씩 바꾸고 지표를 비교하는 편이 사고가 적습니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, <strong>Cache-Aside는 쉬워 보여도 무효화가 본체</strong>입니다. TTL만으로 버티려 하면 인기 키와 변경 빈도 높은 키에서 사고가 납니다.</p>
<p>둘째, <strong>Read-Through는 공통화의 이점만큼 공통 장애점도 키웁니다.</strong> 중앙 miss 처리 계층이 흔들리면 여러 서비스가 같이 느려질 수 있습니다.</p>
<p>셋째, <strong>Write-Through는 읽기 최신성을 얻는 대신 쓰기 경로 예산을 써버립니다.</strong> 특히 다중 키 갱신이 붙으면 write amplification이 빠르게 커집니다.</p>
<p>넷째, <strong>Write-Behind는 queue와 replay가 없으면 거의 항상 과소설계</strong>입니다. 나중에 저장한다는 말은 결국 지연, 유실, 순서 역전 리스크를 같이 받겠다는 뜻입니다.</p>
<p>다섯째, <strong>패턴 선택과 일관성 설계를 분리해서 보면 안 됩니다.</strong> 어떤 패턴을 쓰든 <a href="/learning/deep-dive/deep-dive-cache-consistency-invalidation-playbook/">캐시 일관성 설계</a>, <a href="/learning/deep-dive/deep-dive-cache-warmup-cold-start-playbook/">캐시 워밍과 콜드 스타트</a>, <a href="/learning/deep-dive/deep-dive-redis-caching/">Redis 캐싱</a>까지 같이 봐야 운영이 닫힙니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 데이터별 stale 허용 시간을 초 단위로 적었다.</li>
<li><input disabled="" type="checkbox"> read/write 비율과 miss burst 크기를 측정했다.</li>
<li><input disabled="" type="checkbox"> 캐시 패턴을 서비스 단위가 아니라 데이터 클래스 단위로 나눴다.</li>
<li><input disabled="" type="checkbox"> Write-Behind 후보에는 durable queue, idempotency, replay 경로가 있다.</li>
<li><input disabled="" type="checkbox"> Write-Through 후보에는 write latency 증가 허용치가 정해져 있다.</li>
<li><input disabled="" type="checkbox"> Cache-Aside 후보에는 singleflight, TTL 지터, soft TTL 중 최소 1개 이상 보호 장치가 있다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>현재 서비스의 캐시 대상 5개를 골라 <code>stale 허용 시간</code>, <code>read/write 비율</code>, <code>오류 비용</code>을 표로 적어 보세요.</li>
<li>그중 1개는 Cache-Aside 유지, 1개는 Write-Through 후보, 1개는 캐시 제외 대상으로 나눠 보고 이유를 써 보세요.</li>
<li>miss burst가 심한 키 1개를 골라 <a href="/learning/deep-dive/deep-dive-request-coalescing-singleflight/">Request Coalescing/Singleflight</a>를 붙였을 때 줄일 수 있는 DB QPS를 대략 계산해 보세요.</li>
<li>Write-Behind를 쓰고 싶다면 backlog age, flush fail rate, replay SLA를 숫자로 먼저 적어 보세요.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-cache-consistency-invalidation-playbook/">캐시 일관성 설계(Write-Through/Invalidate/CDC)</a></li>
<li><a href="/learning/deep-dive/deep-dive-redis-caching/">Redis 캐싱</a></li>
<li><a href="/learning/deep-dive/deep-dive-request-coalescing-singleflight/">Request Coalescing/Singleflight</a></li>
<li><a href="/learning/deep-dive/deep-dive-cache-warmup-cold-start-playbook/">캐시 워밍과 콜드 스타트 완화</a></li>
<li><a href="/learning/modules/backend-data-system-phase/">백엔드 데이터 시스템 단계</a></li>
</ul>
]]></content:encoded></item></channel></rss>