스케일링 가운틀렛, Pt. 3: Cache Rules Everything Around Me

발행: (2026년 2월 6일 오전 03:19 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

모든 거짓 새벽이 그렇듯, 좋은 소식으로 시작합니다.

Postgres Pete는 차분합니다.
팀은 축하합니다. 누군가가 밈을 만듭니다.

하지만 뭔가 이상합니다.

  • “앱이 다운된 것”은 아니지만, 그냥… 흐물흐물합니다.
  • 로그아웃한 사용자의 지연 시간이 급증합니다.
  • 보고서 생성이 여전히 CPU를 많이 차지합니다.
  • 누군가가 홈페이지에 접속할 때마다, 서버는 이번 주에 400,000번째로 같은 쿼리를 실행합니다.

이제 느린 것이 아니라, 낭비하고 있습니다.

Chapter 3에 오신 것을 환영합니다: 이제 아키텍처가 충분히 빨라져서 얼마나 중복된 작업을 하고 있는지 드러납니다.

전쟁 이야기: 모든 것을 녹인 리더보드

몇 년 전, 우리는 게임화된 캠페인을 시작했습니다: Leaderboard, Badges, Daily rewards.

문제는?
우리는 모든 요청마다 리더보드를 재계산했습니다.

  • 각 요청은 수백만 행에 걸친 대규모 정렬, 조인, 필터링을 트리거했습니다.
  • 출시 초에는 별 문제가 없었습니다—시간당 약 100명 정도.
  • 바이럴이 되자? 하루에 90,000건의 요청이 있었습니다.

우리 DB는 다운되지 않았지만 성능은 끔찍했습니다. 60초 Redis 캐시를 추가하면 응답 시간이 912 ms → 38 ms 로 감소하고 쿼리 부하가 99.7 % 감소했습니다.

Postgres Pete가 감사 편지를 써 주었습니다.

Chapter 3: Caching Isn’t Cheating

  • 작은 규모에서는 캐싱이 선택 사항이다.
  • 큰 규모에서는 모든 것이다.

환상은 데이터베이스를 빠르게 만들고 쿼리를 효율적으로 하면 된다는 것이다.
1,000명의 사용자가 같은 라우트를 호출하고 1,000개의 동일한 응답을 생성한다면—축하합니다, 낭비를 최적화한 겁니다.

캐싱은 당신이 단순히 주문 즉시 조리하는 요리사가 아니라, 준비 스테이션을 갖춘 셰프가 되게 합니다.

캐싱 스택

레이어별로 나눠서 살펴보자, 도구가 아니라.
누군가 블로그 글을 읽었다고 해서 Redis가 필요하지는 않다. 올바른 작업에 맞는 적절한 캐시가 필요하다.

1. 페이지 및 프래그먼트 캐싱

사용 시점위치
사용자마다 변하지 않는 전체 페이지 응답WordPress, SSR 프레임워크, 마케팅 페이지, 로그아웃 뷰
정적 자산에 대한 CDN 엣지 캐싱 (Cloudflare, Fastly)정적 HTML 스냅샷, 컴포넌트 수준 프래그먼트 캐싱 (Next.js getStaticProps 또는 getServerSidePropsrevalidate 사용)

2. 쿼리 결과 캐싱

사용 시점위치
예측 가능한 결과를 반환하는 비용이 많이 드는 쿼리보고서, 리더보드, 통계 페이지
쿼리 결과를 Redis에 30–300 초 동안 캐시핵심 데이터가 변경될 때 캐시를 무효화하거나 업데이트; 결정론적 캐시 키 사용, 예: leaderboard:daily:2025-07-06

3. 객체 캐싱

사용 시점위치
자주 접근하지만 자주 변경되지 않는 엔티티사용자 설정, 가격표, 콘텐츠 메타데이터
첫 접근 시 객체를 캐시에 로드TTL + 쓰기‑통과/읽기‑통과 패턴; 네임스페이스 키(user:42:profile)로 오염 방지

4. 엣지 캐싱 및 CDN

사용 시점위치
정적 자산, 안전한 GET을 사용하는 API, 지역 지연 시간 개선Next.js, Shopify 헤드리스, 전 세계에 정적 콘텐츠를 제공하는 모든 사이트
예시GET /products?category=fitness → 캐시함. POST /checkout → 캐시하지 않음.
무효화대리 키 사용 (product:updated → /products 삭제)

Redis 예시: 캐시‑어사이드 패턴

// Basic cache-aside pattern
const getCachedUser = async (userId) => {
  const cached = await redis.get(`user:${userId}`);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(userId);
  await redis.setex(`user:${userId}`, 300, JSON.stringify(user));
  return user;
};

구체적인 예시: 홈페이지 지연 시간

지표
홈페이지 로드 시간680 ms112 ms
DB 및 API에 소요된 비율90 %
Redis 적중률12 %89 %
DB 쿼리 감소87 %

단 3개의 컴포넌트(추천 제품, 블로그 티저, 고객 후기)를 캐시한 것만으로 가장 큰 영향을 미쳤습니다.

Cache Busting: The Forgotten Half of Caching

캐시에 쓰는 것은 쉽습니다.
올바르게 무효화하는 것이 성숙한 시스템과 실험적인 시도를 구분합니다.

+------------------------+
|   Does the data        |
|   change often?       |
+------------------------+
          |
+----------------+----------------+
|                |                |
Yes              No
|                |
+--------------+  +--------------------+
| Can you hook |  | Use long TTL with  |
| into writes? |  | fallback refresh   |
+------+-------+  +--------------------+
       |Yes
       |
+--------------+
| Event-driven |
| invalidation |
+--------------+
       |No
       |
+--------------+
| Use low TTL  |
| w/ polling   |
+--------------+

Cache Busting Methods

Time‑Based (TTL)

  • 이해하기 쉽습니다.
  • 약간의 오래된 데이터(스테일니스)를 허용합니다.
  • 대시보드, 통계, 가격 책정 등에 “충분히 좋음”을 제공합니다.

Event‑Driven

  • 데이터가 업데이트될 때 캐시를 무효화합니다.
  • ORM에 훅을 달거나 pub/sub 시스템을 사용합니다.
  • 관리가 더 어렵지만 정확도가 높습니다.
// In your product update handler:
await redis.del(`product:${product.id}`);
await redis.del(`category:${product.category}:featured`);

Dependency‑Tracking (Advanced)

  • 어떤 데이터가 어떤 캐시 엔트리를 구동하는지 추적합니다.
  • 영향을 받은 부분만 재구성합니다.
  • 규율과 도구가 필요합니다(없으면 스스로를 원망하게 됩니다).

캐시가 정상 작동하고 있는지(또는 아닌지) 확인하는 징후

정상적인 캐싱캐싱 오류
핫 경로에 대한 캐시 적중률 > 80 %사용자가 오래된 데이터 또는 일관성 문제를 경험
부하가 걸려도 첫 바이트 응답 시간이 낮음적중률 낮음; 무효화가 과도하게 적용
Redis/Memcached 사용량이 예측 가능캐시 엔트리가 거대한 바이너리 블롭
충돌하는 캐시(앱 vs CDN)

캐시 디버깅: 확인할 항목

  • Hit‑rate metrics (Redis INFO statskeyspace_hits / keyspace_misses).
  • Latency – 캐시 읽기와 DB 읽기의 지연 시간을 비교합니다.
  • TTL distribution – 키가 너무 빨리 만료되거나 절대 만료되지 않는지 확인합니다.
  • Invalidation logs – 키를 무효화해야 하는 모든 쓰기가 실제로 수행되는지 보장합니다.
  • Memory pressureused_memory와 eviction 정책을 주시합니다.

TL;DR

  • 올바른 레이어에 캐시 (페이지, 쿼리, 객체, 엣지).
  • 각 데이터 유형에 맞는 TTL 또는 이벤트‑기반 전략 선택.
  • 히트율과 지연 시간 모니터링; 데이터가 오래되거나 폭발하기 전에 조정.

규율 있는 캐싱 전략을 통해 낭비적인 “충분히 빠름”을 진정으로 효율적이고 확장 가능한 성능으로 바꿀 수 있습니다.

# Cache Health Checklist

✅ INFO 통계 확인

  • keyspace_hitskeyspace_misses를 찾아보세요.

📈 로그 지연 / 놓친 조회

  • 캐시가 조용히 실패한 라우트를 태그합니다.

⚡️ Detect Cache Stampedes

  • Identify when many users request the same uncached item at once.
  • Consider locking or stale‑while‑revalidate strategies.

⏰ TTL 만료 추적

  • TTL이 실제 사용 패턴과 일치하는지 확인합니다.

고급 패턴 (준비가 되었을 때)

Stale‑While‑Revalidate

  • 오래된 데이터를 즉시 제공한다.
  • 백그라운드에서 최신 데이터를 가져와 캐시를 교체한다.
  • 사용자에게 대기 시간을 줄이고 인지된 지연을 감소시킨다.
  • 구현 옵션:
    • HTTP 헤더: Cache-Control: stale-while-revalidate=60
    • 애플리케이션 내 커스텀 미들웨어.

Soft TTL + Refresh

  • 항목에 TTL이 설정되어 있지만, 만료 시점에 가까워지면 백그라운드에서 새로 고친다.
  • 콜드 스타트를 방지하고 핫 아이템을 지속시킨다.
  • 자주 접근되지만 업데이트는 드물게 일어나는 데이터에 적합하다.
  • 구현 옵션:
    • 비동기 작업 큐
    • 미들웨어 훅

Sharded or Namespaced Caches

  • 키 접두사를 사용해 캐시 범위를 구분한다.
    • 예시: tenant-42:user:profile, locale-en:settings
  • 키 충돌을 방지하고 대량 무효화를 단순화한다.
  • 향후 자동화를 지원하도록 구조화된 키‑네이밍 규칙을 채택한다.

캐싱 안티‑패턴

  • 사용자별 또는 민감한 데이터를 전역적으로 캐싱 – GDPR 위반이 될 가능성이 높음.
  • 매일 변경되는 데이터에 대해 긴 TTL을 하드코딩 – 빠르지만 잘못된 방법.
  • 삭제 전략 없이 모든 것을 캐싱 – 두 번째, 관리되지 않는 데이터베이스가 생김.

TL;DR – Cache with Intent

  • Don’t optimize slow things – 반복해서 수행하지 않도록 하세요.
  • 올바른 문제에 맞는 적절한 캐시를 선택하세요.
  • 라이브하기 에 캐시‑무효화 전략을 설계하세요.
  • 캐시 크기만이 아니라 hit rates를 모니터링하세요.
  • 캐싱은 부정행위가 아니라 systems scale하는 방식입니다.
  • 여러분의 앱은 지금 fast한 것에 그치지 않고 efficient합니다.
  • 하지만 don’t relax too long… read and write workloadssplitting하기 need합니다.

Stay tuned.
Next up: Scaling Reads Without Sharding Your Soul

Back to Blog

관련 글

더 보기 »

Redis를 사용하여 백엔드 쿼리 최적화

원래 접근 방식 편안한 방식 엔드포인트 로직은 간단했습니다: 1. 데이터베이스를 쿼리 2. 점수별로 사용자 정렬 3. 상위 10명 반환 sql SELECT FROM users ORDER...

Redis 소개: 그것이 무엇이며 왜 빠른가

Redis란 무엇인가? Redis Remote DIctionary Server는 오픈소스이며 인메모리 데이터 구조 저장소로, 가장 인기 있는 NoSQL 데이터베이스 중 하나입니다. Salvato가 만들었습니다.