<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Cache-Aside on kastori</title><link>http://blog.kastori.dev/tags/cache-aside/</link><description>Recent content in Cache-Aside on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Sat, 18 Jul 2026 00:00:00 +0900</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/cache-aside/index.xml" rel="self" type="application/rss+xml"/><item><title>[Redis 실무 입문 #3] 캐시 구현하기 — Cache-Aside와 정합성</title><link>http://blog.kastori.dev/tech/2026-07-18-redis-03-caching/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-07-18-redis-03-caching/</guid><description>&lt;h2 id="캐시는-쉬워-보이지만-한-번씩은-데인다"&gt;&lt;a href="#%ec%ba%90%ec%8b%9c%eb%8a%94-%ec%89%ac%ec%9b%8c-%eb%b3%b4%ec%9d%b4%ec%a7%80%eb%a7%8c-%ed%95%9c-%eb%b2%88%ec%94%a9%ec%9d%80-%eb%8d%b0%ec%9d%b8%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;캐시는 쉬워 보이지만, 한 번씩은 데인다
&lt;/h2&gt;&lt;p&gt;내가 Redis를 처음 붙인 것도 캐시 때문이었다. 특정 조회가 느려서 &amp;ldquo;이거 Redis로 캐싱하면 되잖아&amp;rdquo; 하고 시작했다. 값을 넣고 빼는 것 자체는 &lt;a class="link" href="http://blog.kastori.dev/tech/2026-07-14-redis-01-getting-started/" &gt;#1&lt;/a&gt;에서 본 &lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;GET&lt;/code&gt;이 전부라 30분이면 붙인다.&lt;/p&gt;
&lt;p&gt;진짜 어려운 건 그 다음이었다. &lt;strong&gt;언제 채우고, 언제 비우고, 원본이 바뀌면 어떻게 맞출 것인가.&lt;/strong&gt; 캐시로 데이는 사람은 대부분 이 정합성에서 데인다. 이번 편은 그걸 순서대로 짚는다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="cache-aside--캐시의-기본기"&gt;&lt;a href="#cache-aside--%ec%ba%90%ec%8b%9c%ec%9d%98-%ea%b8%b0%eb%b3%b8%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;Cache-Aside — 캐시의 기본기
&lt;/h2&gt;&lt;p&gt;가장 널리 쓰는 패턴이다. 이름은 거창하지만 하는 일은 단순하다. &amp;ldquo;먼저 캐시를 보고, 없으면 DB에서 가져와 캐시에 채운다.&amp;rdquo;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. Redis에 있나? (GET)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 있으면 그대로 반환 (Cache Hit)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 없으면 ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. DB에서 조회
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 결과를 Redis에 저장 (SET, TTL 필수)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 반환
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;코드로도 이 흐름 그대로다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;user:&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 캐시 히트&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 캐시 미스 → DB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;user:&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// TTL 1시간&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;처음 캐시를 붙이면 딱 여기까지 짜고 &amp;ldquo;됐다&amp;rdquo; 한다. 나도 그랬다. 근데 여기엔 함정이 두 개 숨어 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="함정-1-ttl을-안-걸면-언젠가-터진다"&gt;&lt;a href="#%ed%95%a8%ec%a0%95-1-ttl%ec%9d%84-%ec%95%88-%ea%b1%b8%eb%a9%b4-%ec%96%b8%ec%a0%a0%ea%b0%80-%ed%84%b0%ec%a7%84%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;함정 1: TTL을 안 걸면 언젠가 터진다
&lt;/h2&gt;&lt;p&gt;위 코드에서 &lt;code&gt;Duration.ofHours(1)&lt;/code&gt;을 빼면 어떻게 될까. 캐시가 &lt;strong&gt;영원히&lt;/strong&gt; 남는다. 처음엔 아무 문제 없어 보인다. 그러다 서서히 두 가지가 온다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;메모리가 계속 찬다.&lt;/strong&gt; 캐시가 쌓이기만 하고 안 비워져서 결국 &lt;code&gt;maxmemory&lt;/code&gt;에 도달한다. 그때부터 Redis가 키를 쫓아내거나 쓰기를 거부하기 시작한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;낡은 데이터가 안 죽는다.&lt;/strong&gt; 원본이 바뀌어도 캐시는 옛 값을 계속 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 캐시엔 &lt;strong&gt;거의 항상 TTL을 건다&lt;/strong&gt;. 기준은 간단하다. &amp;ldquo;이 데이터, 최대 얼마나 낡아도 사용자가 괜찮은가?&amp;rdquo; 자주 안 바뀌면 길게(몇 시간), 자주 바뀌면 짧게(몇십 초~몇 분). 캐시는 원래 비워지라고 있는 거다. 안 비워지는 캐시는 캐시가 아니라 메모리 누수다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET user:1 &lt;span class="s2"&gt;&amp;#34;...&amp;#34;&lt;/span&gt; EX &lt;span class="m"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;# 저장과 동시에 TTL 1시간&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="함정-2-원본이-바뀌면-캐시는-어긋난다"&gt;&lt;a href="#%ed%95%a8%ec%a0%95-2-%ec%9b%90%eb%b3%b8%ec%9d%b4-%eb%b0%94%eb%80%8c%eb%a9%b4-%ec%ba%90%ec%8b%9c%eb%8a%94-%ec%96%b4%ea%b8%8b%eb%82%9c%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;함정 2: 원본이 바뀌면 캐시는 어긋난다
&lt;/h2&gt;&lt;p&gt;이게 진짜다. DB의 사용자 정보를 수정했는데 Redis엔 옛 값이 남아 있으면, 사용자는 방금 자기가 바꾼 값이 아니라 낡은 값을 본다. &amp;ldquo;분명 저장했는데 왜 그대로예요?&amp;rdquo; 문의가 이렇게 나온다.&lt;/p&gt;
&lt;p&gt;가장 단순하고 안전한 해법은 &lt;strong&gt;수정할 때 그 캐시를 지우는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1. DB 먼저 수정&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;user:&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 2. 캐시는 갱신(X) 삭제(O)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;여기서 두 가지가 포인트다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;첫째, 캐시를 새 값으로 &amp;ldquo;덮어쓰기&amp;quot;가 아니라 &amp;ldquo;삭제&amp;quot;한다.&lt;/strong&gt; 처음엔 &amp;ldquo;수정했으니 캐시도 새 값으로 업데이트하면 되잖아?&amp;rdquo; 싶은데, 그러면 동시에 여러 요청이 수정할 때 어느 값이 최종인지 꼬인다. 그냥 지워버리면 다음 조회가 Cache-Aside를 타고 DB에서 최신값을 다시 채운다. 훨씬 안전하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘째, 순서가 중요하다. DB를 먼저 바꾸고 캐시를 지운다.&lt;/strong&gt; 캐시를 먼저 지우면, 그 찰나에 다른 요청이 아직 안 바뀐 DB 값을 읽어 캐시를 옛 값으로 다시 채워버릴 수 있다. &amp;ldquo;DB → 캐시 삭제&amp;rdquo; 순서를 지키자.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="미리-알아두면-좋은-함정들"&gt;&lt;a href="#%eb%af%b8%eb%a6%ac-%ec%95%8c%ec%95%84%eb%91%90%eb%a9%b4-%ec%a2%8b%ec%9d%80-%ed%95%a8%ec%a0%95%eb%93%a4" class="header-anchor"&gt;&lt;/a&gt;미리 알아두면 좋은 함정들
&lt;/h2&gt;&lt;p&gt;위 두 개가 기본이고, 규모가 커지면 이런 것도 만난다. 지금 당장은 아니어도 이름은 알아두자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;캐시 스탬피드(Thundering Herd)&lt;/strong&gt;: 인기 데이터의 캐시가 만료되는 그 순간, 수많은 요청이 동시에 &amp;ldquo;캐시 없네?&amp;rdquo; 하고 전부 DB로 몰린다. TTL에 약간의 무작위(jitter)를 주거나, 캐시 갱신은 한 요청만 하도록 락을 걸어 완화한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;히트율이 낮으면 오히려 손해&lt;/strong&gt;: 캐시가 거의 안 맞으면 &amp;ldquo;캐시 확인 → 미스 → DB → 캐시 저장&amp;quot;이 그냥 DB 조회보다 느리다. 자주 조회되는 데이터에만 걸어야 한다. 안 그러면 Redis만 놀고 느려진다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;너무 큰 값&lt;/strong&gt;: 수백 KB짜리를 캐시하면 꺼낼 때마다 네트워크·메모리를 크게 먹는다. 캐시는 작고 자주 읽히는 데이터일수록 이득이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;캐시는 Redis의 시작이자 가장 실수하기 쉬운 부분이다. 정리하면 &lt;strong&gt;Cache-Aside + TTL + 수정 시 삭제&lt;/strong&gt;, 이 세 개만 지켜도 대부분의 캐시는 조용히 잘 굴러간다. 나머지는 규모가 커진 다음 얘기다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 캐시 말고 Redis를 진짜 다양하게 쓰는 실전 패턴 — 세션, Rate Limiting, 분산 락, 랭킹 — 을 한 번에 훑는다. &amp;ldquo;이런 것도 Redis로 하네&amp;rdquo; 싶은 것들이다.&lt;/p&gt;</description></item></channel></rss>