Nginx: 마이크로캐싱(Micro-Caching) 적용하기

마이크로 캐싱이란?

마이크로 캐싱은 아주 짧은 TTL(보통 1~10초) 동안 응답을 캐시에 저장해 두고, 같은 요청이 들어오면 백엔드 대신 캐시에서 바로 응답하는 기법입니다. 짧은 TTL이라 실시간성은 유지하면서도, 순간 트래픽 폭주(새로고침, 인기 API 호출 등)를 효과적으로 흡수할 수 있습니다.

필요한 이유

  • 사용자가 새로고침을 여러 번 하거나, 다수 유저가 동시에 동일 API를 호출하면 백엔드(DB, 애플리케이션)에 부하가 급증합니다.
  • 대부분의 API/페이지는 몇 초 단위로 변경되지 않기 때문에 짧게라도 캐시하면 부하를 크게 줄일 수 있습니다.

⚙️ 동작원리

  1. 첫 요청 → 캐시에 없음 → 백엔드 호출 → 응답 저장
  2. TTL(예: 3초) 안의 동일 요청 → 캐시에서 바로 응답
  3. TTL 만료 → 백엔드 재호출 후 캐시 갱신

Nginx는 $upstream_cache_status 값으로 캐시 상태를 알려줍니다.

$upstream_cache_status 값과 의미

의미
MISS캐시에 없음, 백엔드 호출
HIT캐시에서 응답
EXPIRED캐시 있었지만 TTL 만료, 백엔드 호출 후 갱신
BYPASS캐시 우회 조건에 해당(Authorization, Cookie 등)
STALETTL 지났지만 오래된 캐시로 응답(갱신 중)

기본 설정 예시 (API)

  • /etc/nginx/nginx.conf ➡️ http{} 블록
# 캐시 저장소
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=my_cache:50m max_size=1g inactive=10m use_temp_path=off;

# 인증 헤더 감지(개인화 요청은 캐시 제외)
map $http_authorization $auth_cache_bypass {
    default 1;
    ""      0;
}
  • server{} 블록
# API만 마이크로 캐싱
location ^~ /api/ {
    proxy_pass http://localhost:port;

    proxy_cache my_cache;
    proxy_cache_key "$scheme$host$request_uri";
    proxy_cache_valid 200 3s;          # TTL 3초
    proxy_cache_lock on;               # 동시 폭주 방지
    proxy_cache_use_stale updating;    # 갱신 중에는 이전 캐시 사용

    # 안전장치: 인증 요청/쿠키 발행 응답은 캐시 제외
    proxy_no_cache       $auth_cache_bypass $upstream_http_set_cookie;
    proxy_cache_bypass   $auth_cache_bypass $upstream_http_set_cookie;

    add_header X-Cache-Status $upstream_cache_status always;
}

🧱 테스트 방법

# 첫 호출 → MISS
curl -I https://example.com/api/data | grep -i x-cache-status

# TTL 내 재호출 → HIT
curl -I https://example.com/api/data | grep -i x-cache-status

🚨 주의

  • 개인화 데이터(로그인 사용자별 데이터)는 절대 캐시하면 안 됨 → Authorization/Cookie 조건으로 캐시 우회
  • POST, PUT 요청은 Nginx 기본 설정상 캐시되지 않음
  • TTL이 너무 길면 데이터 최신성이 떨어지고, 너무 짧으면 효과가 적음 → 2~3초부터 시작해서 조정
  • 쿼리스트링이 다르면 별도 캐시로 저장되므로, 불필요한 랜덤 파라미터는 제거

⛺️ 확장: 정적 파일 + 마이크로 캐싱

왜 정적 파일에도 마이크로 캐싱을 걸까?

  • 백엔드가 정적 파일(JS, CSS, 이미지 등)을 직접 서빙하는 경우, 많은 유저가 동시에 요청하면 백엔드 리소스를 불필요하게 소모합니다.
  • 특히 빌드 시 해시(app.abc123.js)가 붙은 파일은 변경 시 파일명이 바뀌므로 오래 캐시해도 안전합니다.
  • Nginx가 서버 앞단 캐시(프록시 캐시)로 한 번만 백엔드에 요청하게 하면, 다수의 요청을 빠르게 처리할 수 있습니다.

🧱 설계 원칙

  1. 요청에 쿠키가 있어도 캐시 허용
    • 정적 파일에는 보통 개인화 데이터가 없음.
  2. 응답에 Set-Cookie가 있으면 캐시 금지
    • 혹시라도 백엔드가 정적 파일 응답에 세션 쿠키를 발행하는 경우 안전하게 우회.
  3. 브라우저 캐시도 함께 설정
    • 해시가 있는 파일은 1년 + immutable 권장.
  4. 서버 캐시 TTL은 짧게
    • 마이크로 캐싱 용도로 1분~10분 정도가 적당.


⚙️ Nginx 설정 예시

# 정적 파일(해시 포함) + 마이크로 캐싱
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
    proxy_pass http://localhost:port;                # 백엔드 서빙 유지

    # --- 서버 앞단 캐시(Nginx 프록시 캐시) ---
    proxy_cache my_cache;
    proxy_cache_key "$scheme$host$request_uri";
    proxy_cache_valid 200 10m;                        # TTL: 10분 (해시 없는 파일은 더 짧게)
    proxy_cache_lock on;                              # 동시 폭주 방지
    proxy_cache_use_stale updating;

    # 요청 쿠키는 허용, 응답 Set-Cookie 시 캐시 제외
    proxy_no_cache       $auth_cache_bypass $upstream_http_set_cookie;
    proxy_cache_bypass   $auth_cache_bypass $upstream_http_set_cookie;

    # --- 브라우저 캐시 ---
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable" always;

    # 상태 확인용 헤더
    add_header X-Cache-Status $upstream_cache_status always;
}

⚠️ 해시 없는 파일(예: /styles/main.css)이라면 proxy_cache_valid를 더 짧게(예: 1~5분) 두고, 브라우저 캐시도 max-age=300 정도로 조정하세요.


🧱 테스트 방법

# 첫 요청: MISS
curl -I https://example.com/_next/static/chunks/app.abc123.js | grep -i x-cache-status

# TTL 내 재요청: HIT
curl -I https://example.com/_next/static/chunks/app.abc123.js | grep -i x-cache-status

# 브라우저 캐시 헤더 확인
curl -I https://example.com/_next/static/chunks/app.abc123.js | egrep -i 'cache-control|expires'

🧤 운영 TIP

  • 정적 전용 서브도메인(예: static.example.com)을 사용하면 브라우저가 쿠키를 아예 붙이지 않아 캐시 효율 ↑.
  • 빌드 툴(Next.js, React, Vue 등)의 filename hashing을 켜두면 캐시 무효화 걱정 없이 TTL을 길게 가져갈 수 있음.
  • Nginx 프록시 캐시 + 브라우저 캐시를 동시에 쓰면 최초 1회만 백엔드 요청, 그 이후는 대부분 브라우저 또는 Nginx에서 응답.

🧤 알아두어야 할 것

🖥 서버 캐싱 (서버 단 캐시)

  • 위치: Nginx(리버스 프록시)나 CDN, API Gateway 같은 서버 측
  • 동작:
    1. 첫 요청 → 서버가 백엔드에 요청해 응답 저장
    2. TTL 안의 재요청 → 서버 캐시에서 바로 응답
    3. TTL 지나면 백엔드 재요청
  • 장점:
    • 백엔드 부하 감소 (API·정적 파일 모두 가능)
    • TTL 내에 여러 유저 요청을 합쳐서 처리 가능
    • 브라우저 캐시를 무시하고도 동작 가능 (모든 클라이언트 공통)
  • 단점:
    • TTL 안에서는 변경된 데이터가 바로 반영 안 될 수 있음
    • 서버 디스크/메모리 사용

예시: proxy_cache (Nginx), CloudFront, Varnish, Redis 기반 캐시

🌐 브라우저 캐싱 (클라이언트 단 캐시)

  • 위치: 사용자의 웹 브라우저 로컬 저장소
  • 동작:
    1. 첫 요청 시 응답을 브라우저가 저장
    2. TTL 또는 조건부 요청(ETag, Last-Modified) 안에서는 서버에 재요청 없이 바로 로컬에서 로드
    3. TTL 지나면 서버에 새로 요청
  • 장점:
    • 요청 자체를 줄여 네트워크 속도 향상
    • 서버에 트래픽이 아예 가지 않음
  • 단점:
    • 브라우저별·사용자별로 캐시가 따로 있음
    • TTL 안에 새 버전 배포 시 즉시 반영 안 됨
    • 캐시 무효화(무효화 정책, 파일명 변경) 필요

예시: Cache-Control: max-age=31536000, immutable, ETag + If-None-Match


🔥 빌드할때 왜 해시 기반 파일명으로 할까?

📌 해시 기반 파일명의 핵심 목적: 

캐시 무효화(Cache Busting)

  • 브라우저 캐싱은 빠른 로딩을 위해 오래 저장하지만, 이게 변경 반영에는 치명적일 수 있음.
  • 파일명이 고정이면 (app.js), 브라우저는 TTL 동안 절대 새로 다운로드 안 함.
  • 그래서 파일 내용이 바뀔 때마다 내용 기반 해시(MD5/SHA1 등)를 파일명에 포함시켜 버전처럼 사용.
빌드 전빌드 후
/static/app.js/static/app.9fceb3d2.js
/static/style.css/static/style.ae3d9c9f.css

🛠 동작 방식

  1. 빌드 시 JS/CSS 내용의 해시를 계산
  2. 해시를 파일명에 삽입
  3. HTML에서 그 새 파일명을 참조
  4. 브라우저는 이름이 다르면 다른 파일로 인식 → 즉시 새로 다운로드

✅ 장점

  1. 1년 캐시 가능
    • 파일명이 바뀌면 브라우저는 무조건 새로 받음 → TTL 무시
  2. 불필요한 네트워크 요청 제거
    • 내용이 안 바뀌면 계속 기존 캐시 사용
  3. CDN/프록시 캐시 효율 극대화
    • 전 세계 캐시 서버에 동일한 파일명이 유지

⚠️ 단점

  • 빌드 과정에서 HTML/JS 내부의 파일 경로를 전부 갱신해야 함
  • 해시 값이 조금이라도 달라지면 완전히 새 파일로 인식 → 캐시 100% 무효화 (좋기도 하고, 비효율일 수도 있음)

📍 정리

  • 해시 기반 파일명 덕분에 Cache-Control: max-age=31536000, immutable 같은 1년 브라우저 캐싱 정책을 안전하게 쓸 수 있음.
  • 해시 없이 고정 파일명이라면, 이렇게 길게 캐싱하면 배포 때 문제 생김.

댓글

댓글 남기기