codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›PostgreSQL 깊게 다루기 + Redis · Kafka›5단계

5단계

3-layer 캐시 전략

0회 조회

3-layer 캐시 전략

한 층이 빠르면 다음 층 부하가 줄어듭니다. edge → Redis → PG 세 층이 일반적.

1. 전체 구조

[Browser] ← Cache-Control / ETag
    ↓
[CDN / Caddy] ← edge cache
    ↓
[앱] ← unstable_cache · Redis
    ↓
[PG] ← pg-cache · query cache

각 층의 TTL 을 다르게 설정.

2. Edge (Caddy · CDN)

example.com {
  reverse_proxy localhost:3000

  header /images/* Cache-Control "public, max-age=31536000, immutable"
  header /api/*    Cache-Control "no-store"
}
  • 정적 자산 — 1 년 (파일명에 hash)
  • API — no-store

3. 브라우저 — HTTP 헤더

return NextResponse.json(data, {
  headers: {
    "Cache-Control": "public, max-age=60, stale-while-revalidate=120",
  },
});

stale-while-revalidate — 만료돼도 백그라운드 갱신 동안 기존 값 반환. 체감 빠름.

4. 앱 레이어 — Next.js unstable_cache

import { unstable_cache } from "next/cache";

export const getTopPosts = unstable_cache(
  async () => db.query("SELECT * FROM posts WHERE published=true ORDER BY likes DESC LIMIT 20"),
  ["top-posts"],
  { tags: ["posts"], revalidate: 60 }
);
// mutation 후 무효화
import { revalidateTag } from "next/cache";
revalidateTag("posts");

5. 앱 레이어 — Redis

async function cachedQuery<T>(key: string, ttl: number, fetcher: () => Promise<T>): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  const fresh = await fetcher();
  await redis.setex(key, ttl, JSON.stringify(fresh));
  return fresh;
}

unstable_cache 와 Redis 의 차이:

  • unstable_cache — 프로세스 인스턴스별 메모리
  • Redis — 전체 클러스터 공유

여러 앱 인스턴스가 있으면 Redis 가 더 효율적 (같은 키 한 번만 계산).

6. DB 레이어 — pg-cache / query_cache_size

PostgreSQL 은 자체 쿼리 결과 캐시가 없습니다 (MySQL 과 차이). shared_buffers · effective_cache_size 로 간접 튜닝.

# postgresql.conf
shared_buffers = 25% of RAM
effective_cache_size = 75% of RAM

대신 materialized view 로 비싼 집계 미리 계산:

CREATE MATERIALIZED VIEW mv_daily_stats AS
SELECT date_trunc('day', created_at) AS day, count(*) FROM events GROUP BY 1;

REFRESH MATERIALIZED VIEW mv_daily_stats;   -- 주기적 (cron)

7. 무효화 전략

전략 장점 단점
TTL 간단 사용자가 변경 즉시 반영 안 됨
mutation 시 DEL 즉시 반영 무효화 누락 시 stale
stale-while-revalidate UX 좋음 일시적 stale 허용 필요
webhook (revalidateTag) 정확 구현 복잡

각 데이터 특성에 맞게. 사용자 프로필 = mutation 시 DEL, 랭킹 = 60초 TTL.

8. 키 네이밍

<domain>:<entity>:<id>[:<variant>]

user:profile:123
post:detail:456:ko
search:results:react:page1

prefix 가 명확하면 디버깅 편리 + DEL user:profile:* 패턴으로 일괄 무효화.

9. stampede — 캐시 만료 시 동시 재계산

인기 캐시 만료 순간 수백 요청이 동시에 DB 호출.

// single-flight pattern
const pending = new Map<string, Promise<any>>();

async function getCached(key: string, fetcher: () => Promise<any>) {
  if (pending.has(key)) return pending.get(key);
  const p = fetcher().finally(() => pending.delete(key));
  pending.set(key, p);
  return p;
}

10. 자주 걸리는 자리

  • 모든 경로 TTL 같음 — 변하지 않는 것 vs 자주 변하는 것 구분 필요
  • 캐시 키 변수 누락 — 언어 · 페이지 · user_id 안 포함해서 cross-user leak
  • unstable_cache 에 user-specific 데이터 — 서버 공통 캐시라 사용자 섞임
  • Redis OOM — maxmemory-policy 미설정

하고픈 말

3 층 캐시가 늘 필요한 건 아닙니다. 트래픽이 작으면 unstable_cache 만 · 중간이면 + Redis · 대규모에서 + CDN 순차 도입이 자연스러움.

Next

  • 06-kafka-when

← 4단계

Redis 의 역할 5가지

6단계 →

Kafka — 언제 · 언제 아닌지