- Postgres: 키 전략(ULID/UUIDv7), 인덱스 설계(BTREE+GIN/TRGM), PgBouncer+
statement_timeout, 슬로우쿼리 Top-N 튜닝 - Redis: 캐시-어사이드 + SWR(stale-while-revalidate), 키 버저닝, 스탬피드 방지(뮤텍스/확률갱신)
 - Search: 1단계 FTS(Postgres) → 2단계 OpenSearch/Elastic(CDC로 색인 일관성 확보)
 - 일관성: Outbox + 소비자(워커), 아이템포턴시 키
 - Next.js: ISR/태그 리밸리데이트, Route Handler 결과 캐싱 + ETag
 - 가시성: 캐시 히트율/DB p95/슬로우로그/색인 지연을 대시보드로 고정 관찰
 
1) 스키마·키·인덱스: “처음부터 빨라지게”
키 선택
- 단조 증가가 필요하면: UUIDv7 또는 ULID (정렬·샤딩 친화)
 - 외부 노출 ID는 내부 PK와 분리(보안/URL 안정성)
 
-- uuid-ossp 또는 pg_uuidv7 확장 사용 가정
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), -- 내부 PK
  public_id uuid UNIQUE NOT NULL DEFAULT uuid_generate_v4(), -- 외부용
  user_id uuid NOT NULL,
  status text NOT NULL,
  total_cents int NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON orders (user_id, created_at DESC);
인덱스 기본기
- 정확 매칭/정렬: BTREE
 - 부분 문자열/유사검색: 
pg_trgm+ GIN - 전문검색: 
tsvector+ GIN 
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_users_name_trgm ON users USING gin (name gin_trgm_ops);
슬로우 쿼리 루틴
-- pg_stat_statements 필수
SELECT total_exec_time, mean_exec_time, calls, query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
- 인덱스 미스 → 커버링 인덱스 추가
 - 조인 키 타입 불일치 → 타입 통일
 - 대용량 업데이트 → 배치 + 파티셔닝 검토
 
2) 연결·풀링·타임아웃 (FastAPI + SQLAlchemy)
# db.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
engine = create_async_engine(
  DB_URL,
  pool_size=20, max_overflow=20,
  pool_timeout=10, pool_recycle=1800, pool_pre_ping=True,
)
Session = async_sessionmaker(engine, expire_on_commit=False)
서버/DB 공통: statement_timeout(예: 5s), idle_in_transaction_session_timeout(예: 10s)로 좀비 세션 제거.
PgBouncer: transaction pooling + 최대 연결 수를 DB보다 작게.
3) 트랜잭션·동시성·아이템포턴시
안전한 업서트
INSERT INTO inventory (sku, qty) VALUES ($1, $2)
ON CONFLICT (sku) DO UPDATE SET qty = inventory.qty + EXCLUDED.qty;
아이템포턴시(중복 요청 차단)
# idempotency.py (Redis)
async def once(key: str, ttl=300):
    ok = await redis.setnx(f"idem:{key}", "1")
    if ok: await redis.expire(f"idem:{key}", ttl)
    return ok
큐 처리 시 경합 억제
-- 작업 큐: SKIP LOCKED로 병렬 안전
DELETE FROM job_queue
WHERE id = (
  SELECT id FROM job_queue
  WHERE run_at <= now()
  ORDER BY priority DESC, run_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
RETURNING *;
4) 캐싱 전략(응답/데이터/계산 결과)
패턴 맛보기
- Cache-Aside(권장): 미스 시 DB→캐시 세팅. 가장 유연
 - Write-Through: 쓰기마다 캐시에 즉시 반영(지연↑)
 - Write-Back: 캐시에 적고 나중에 DB 반영(복잡/장애시 위험)
 
스탬피드 방지
- 확률적 조기 갱신: TTL의 10~20% 남았을 때 일부 요청만 갱신 시도
 - 분산 락: 
SET key val NX PXor Redlock(간단 용도) - 버전 키: 
user:{id}:v{ver}→ 무효화는 버전 증가로 O(1) 
async def get_user(user_id: str):
    ver = await redis.get(f"user:{user_id}:ver") or "1"
    ck = f"user:{user_id}:v{ver}"
    cached = await redis.get(ck)
    if cached: return json.loads(cached)
    # stampede 방지 락
    if not await redis.setnx(ck+":lock", "1"):
        await asyncio.sleep(0.05); return json.loads(await redis.get(ck))
    try:
        data = await db_fetch_user(user_id)
        await redis.setex(ck, 300, json.dumps(data))
        return data
    finally:
        await redis.delete(ck+":lock")
5) Redis 자료구조 실전
- String: 일반 캐시/카운터
 - Hash: 프로필 같은 필드 부분 업데이트
 - ZSET: 랭킹/시간순 스트림 인덱스
 - SET: 유니크 토큰/피처 플래그
 - Bloom 필터: 존재 가능성 빠른 체크(미스 폭발 완화)
 
6) Postgres FTS → OpenSearch로 성장
1단계: Postgres FTS
ALTER TABLE articles ADD COLUMN tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('simple', coalesce(title,'') || ' ' || coalesce(body,''))) STORED;
CREATE INDEX idx_articles_tsv ON articles USING gin (tsv);
SELECT id, ts_rank(tsv, plainto_tsquery('simple', $1)) AS rank
FROM articles
WHERE tsv @@ plainto_tsquery('simple', $1)
ORDER BY rank DESC
LIMIT 20 OFFSET $2;
- 한글은 형태소 분석 이슈 → ngram 기반 TRGM도 병행 고려
 
CREATE INDEX idx_articles_body_trgm ON articles USING gin (body gin_trgm_ops);
2단계: OpenSearch/Elastic로 분리
- 색인 경로
A) 앱 이중쓰기(쓰기 시 DB+ES 동시) → 간단하지만 불일치 위험
B) Outbox + CDC(권장): DB에 이벤트 행 기록 → Debezium/Kafka → 색인 워커 
-- outbox 테이블
CREATE TABLE outbox_events(
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  aggregate_type text NOT NULL, -- e.g. "article"
  aggregate_id uuid NOT NULL,
  event_type text NOT NULL,     -- "created"|"updated"|"deleted"
  payload jsonb NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
- 색인 지연(SLA) 목표: p95 < 3~5초
 - 리인덱스 전략: 새 인덱스 생성 → 앨리어스 스위치(무중단)
 
7) 벡터 검색(옵션)
- 간단/소규모: pgvector 확장 + ANN 인덱스(HNSW)
 - 대규모/멀티모달: 전용 벡터 스토어(초저지연, 대량 업서트)
 - 임베딩 관리: 버전 필드 포함(
embed_v=2), 재학습 시 백필 파이프라인 준비 
8) Next.js 캐싱/리밸리데이션
- ISR: 콘텐츠성 페이지는 
revalidate또는 태그 기반 무효화 - 사용자 맞춤(세션/쿠키 의존) → SSR + CDN 캐시 비활성/단기
 - Route Handler(BFF)에서 ETag/Cache-Control 헤더 적극 사용
 
// app/api/articles/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(_: Request, { params }: { params: { id: string }}) {
  const data = await fetchFromAPI(params.id);
  const etag = await hashJSON(data);
  const res = NextResponse.json(data);
  res.headers.set('ETag', etag);
  res.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=120');
  return res;
}
9) 마이그레이션(무중단 절차)
- 추가: 새 컬럼/테이블 nullable로 추가
 - 백필: 배치 작업(작게 쪼개서)
 - 읽기/쓰기: 애플리케이션이 새 필드 함께 사용
 - 스왑: 트래픽 전환 확인
 - 정리: 구 필드/인덱스 제거(여유 기간 후)
 
대용량 백필 시
SET LOCAL statement_timeout낮추고 소량 커밋 반복
10) 관측성(꼭 넣을 항목)
- DB: p50/p95/p99, 커넥션 사용률, 슬로우 쿼리 수, 캐시 적중률
 - Redis: 히트율, 키 수/메모리, 실패율, 록 경합
 - Search: 색인 지연, 쿼리 지연, 에러율, 리트라이율
 - 캐시 KPI: 캐시 없을 때 p95 vs 있을 때 p95(절대 수치로 기록)
 
11) 보안/컴플라이언스(요지)
- PII 필드: 컬럼 수준 암호화(KMS) 또는 앱 레벨 토큰화
 - 행 수준 보안(RLS): 멀티테넌시 시 Postgres RLS 고려
 - 백업/DR: PITR(최신 시점 복구), 정기 복구 리허설 필수
 
12) 2주 액션 플랜(템플릿)
- 슬로우 쿼리 Top 10 추출→ 인덱스/쿼리 구조 개선
 - Redis 캐시-어사이드를 상위 조회 3개 엔드포인트에 적용
 - SWR + 분산락으로 캐시 스탬피드 방지
 - FTS 도입(tsvector+GIN) → 검색 p95 측정
 - Outbox 스키마 + 워커 토대 마련(나중에 Debezium 연동 가능하게)
 - Next.js 태그 기반 ISR 적용 및 API ETag/Cache-Control 정착
 - 대시보드에 DB/Redis/Search 핵심 지표 고정 배치
 
13) 바로 복붙하는 스니펫 묶음
A. Redis SWR 윈도우
import random, time
def should_refresh(ttl_remaining: int, base_ttl: int):
    # TTL 남은 비율이 20% 이하이면서 확률적으로만 갱신 트리거
    return ttl_remaining < base_ttl*0.2 and random.random() < 0.2
B. Postgres FTS + TRGM 혼합 검색
SELECT id, title,
  ts_rank(tsv, plainto_tsquery('simple', :q)) AS rank_fts,
  similarity(title, :q) AS sim
FROM articles
WHERE tsv @@ plainto_tsquery('simple', :q)
   OR title % :q
ORDER BY (rank_fts*0.7 + sim*0.3) DESC
LIMIT 20;
C. Alembic 무중단 마이그레이션 예시
def upgrade():
    op.add_column('orders', sa.Column('note', sa.Text(), nullable=True))
    # 인덱스는 동시 생성
    op.execute('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_created ON orders (created_at DESC)')
마무리
- 데이터는 느려지기 시작하면 기하급수로 느려진다 → 초기에 키/인덱스/캐시/검색의 역할분리를 해두면 대규모로 갈수록 유리합니다..