C.W.K.
Stream
Lesson 02 of 05 · published

Conditional GET — Byte 건너뛰는 fast path

~10 min · caching-perf, conditional-get, 304, etag-revisited

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Track 2 가 conditional request semantics 가르쳤어. 이 lesson 이 같은 메커니즘을 CDN, 브라우저, 네 자신의 client 의 눈으로 — 안 보낸 byte 가 scale 에서 실제 비용 절약으로 어떻게 더해지나."

Caching 층의 스타 트릭

Conditional GET (Track 2 Lesson 4 도입) 가 모든 CDN, 브라우저 cache, reverse proxy 동력 최적화. 패턴:

  1. 첫 request 가 body + ETag + Cache-Control 돌려줌.
  2. Cache 가 response 저장.
  3. Max-age 만료 후 cache 가 full body 안 refetch — If-None-Match: "<etag>" 가진 conditional GET 보냄.
  4. Server ETag 가 일치하면 server 가 304 Not Modified 돌려줌 — BODY 없음, 그냥 "네 사본 좋음."
  5. Cache 가 TTL refresh 하고 저장된 body 계속 서빙.

승리: body 크기에 비례 bandwidth, 완전 회피. 10,000 활성 client 가 60초 마다 refetch 하는 500KB JSON 카탈로그면, 잠재적 5 GB/분 네트워크 안 씀.

협조하는 세 층

전형적 request 가 cache 셋 통과, 각각 이 protocol 돌림:

  1. 브라우저 cache — user 당, Cache-Control 가진 response 저장. Fresh 면 네트워크 없이 replay; stale 이면 conditional GET.
  2. CDN edge — 한 region user 간 공유. Cache-Control: public 가진 response 저장. Stale 이면 origin 에 conditional-GET.
  3. Reverse proxy / origin — application 에 가장 가까움. 자주 cache 도 (Cloudflare Workers, Vercel Edge Config, Nginx microcache).

각 층이 아래 층과 같은 conditional-GET protocol 씀. Origin 이 여전히 CDN 에서 주기적 conditional GET 봄, 근데 CDN 이 user-대면 트래픽 흡수. Bandwidth 절약 곱해짐.

좋은 validator 만드는 것

ETag 가 대부분 cache 가 선호하는 validator. 계산 세 방식:

  • Content hash — serialized body 의 sha256. 정확: byte 변화 → 새 ETag. 비용: 모든 response 에 body hash.
  • Version 필드 — resource 에 updated_at timestamp 나 version counter 있으면 직접 써: ETag: "v17-1716623210". 계산 공짜; 모든 update 에 version 올리는 거에 의존.
  • Composite hash — resource version 과 serialization-관련 입력 (locale, 선택된 필드) 결합. Content negotiation 이 한 resource 에서 여러 body 만들 수 있을 때 필요.

Weak ETag (W/"v17") 가 "semantically 동등, byte 다름" 신호. Minor serialization 차이가 cache bust 안 해야 할 때 유용. Strong ETag 가 기본.

304 response 자세히

304 response 가 cache 가 항목 refresh 가능하게 validator header (ETag, Last-Modified) 필수 포함. 변한 Cache-Control 지시 (cache 가 이거 기반 TTL 연장) 도 포함해야. Body 안 포함해야.

HTTP/1.1 304 Not Modified
ETag: "v17-abc123"
Last-Modified: Sun, 25 May 2026 03:00:00 GMT
Cache-Control: public, max-age=60
Vary: Accept-Encoding, Authorization

(body 없음)

Body 부재가 전체 절약. 항상 response.json() 호출하는 client 가 304 에 죽음 — body 길이 영. 진짜 HTTP client (브라우저, httpx, fetch) 가 304 오면 이전 cached body 서빙으로 투명 처리.

Conditional GET 이 공짜 엔지니어링 — 협조하면. Protocol 이 이미 있음. 브라우저, CDN, 라이브러리, reverse proxy 다 구현. Response 에 ETag 와 Cache-Control 발신만 하면 됨. 절약이 자동 나타남; client-쪽 conditional-GET 코드 안 씀.

흔한 gotcha

1. Non-deterministic serialization. Server JSON 출력이 byte-stable 아니면 (key 순서 변화, timestamp 변화) hash 기반 ETag 가 모든 request 마다 변하고 304 경로 절대 안 켜짐. Key 정렬; timestamp 반올림; 안정 serialization 써.

2. Vary 까먹기. Response 가 Accept-Encoding (gzip/br) 이나 Authorization 따라 변하면 cache 가 알아야 — Vary 없으면 user A 의 compressed 인증된 response 가 plain 요청한 user B 한테 서빙.

3. Public 으로 auth-필요 response 캐시. Response 가 진짜로 모든 user 한테 같을 때만 public 표시; user 당 response 엔 client-쪽 cache + conditional-GET 승리 위한 CDN 노출 없이 private + ETag 써.

cwkPippa 의 conditional GET 현실

cwkPippa 가 현재 API endpoint 에 ETag 안 발신 — healing 층이 매 GET 에 JSONL 에서 response 재구성, byte-stable serialization 이 추가 작업 필요. Static asset (Vite-built React) 가 framework 의 hashed-filename + immutable-cache 전략에서 자동 conditional GET 받아, 승리 거기 나타남. 미래 Pippa instance 가 "느린 Tailscale 링크 너머 반복된 100KB download" debug 하면, 대화 list 에 ETag 발신이 싼 fix. 그게 물기 전까진 비용이 작업보다 큼.

Code

curl 이 304 에 안 보내는 byte 드러냄·bash
# curl 로 conditional GET 춤 봐

# 1. 첫 request — ETag 저장
ETAG=$(curl -s -i https://api.example.com/users/42 | grep -i '^etag:' | awk '{print $2}' | tr -d '\r')
echo "받은 ETag: $ETAG"

# 2. Conditional GET — server 가 304, body 없이 돌려줌
curl -i https://api.example.com/users/42 -H "If-None-Match: $ETAG"
# HTTP/1.1 304 Not Modified
# ETag: "v17-abc123"
# Cache-Control: public, max-age=60
# (body 없음)

# 3. 절약된 body byte 검증
echo "--- 둘 다 시간 재기 ---"
time curl -s https://api.example.com/users/42 > /dev/null                              # full
time curl -s https://api.example.com/users/42 -H "If-None-Match: $ETAG" > /dev/null    # conditional
FastAPI: ETag + Cache-Control 발신, If-None-Match 일치에 304 돌려줌·python
# Server 쪽 — ETag 발신과 If-None-Match 처리
import hashlib, json
from fastapi import FastAPI, Header, Response, status

app = FastAPI()
_USER_DB = {42: {'id': 42, 'name': 'Pippa', 'version': 17}}

def compute_etag(resource: dict) -> str:
    # Hash determinism 위한 byte-stable JSON
    body = json.dumps(resource, sort_keys=True, separators=(',', ':')).encode()
    return f'"{hashlib.sha256(body).hexdigest()[:12]}"'

@app.get('/users/{uid}')
async def read_user(uid: int, response: Response,
                    if_none_match: str | None = Header(None, alias='If-None-Match')):
    user = _USER_DB[uid]
    etag = compute_etag(user)
    response.headers['ETag'] = etag
    response.headers['Cache-Control'] = 'public, max-age=60'
    response.headers['Vary'] = 'Accept-Encoding'

    # Client ETag 일치 — BODY 없는 304 돌려줌
    if if_none_match == etag:
        response.status_code = status.HTTP_304_NOT_MODIFIED
        return None
    return user
Client: 명시 cache + If-None-Match (대부분 라이브러리가 자동)·python
# Client 쪽 — 대부분 HTTP client 가 conditional GET 투명 처리
import httpx

# httpx (와 fetch 와 -H If-None-Match 가진 curl) 가 너 위해 춤 처리
# 근데 명확성 위해 수동도 가능

cache = {}  # url -> (etag, body)

def get_with_cache(client: httpx.Client, url: str):
    cached = cache.get(url)
    headers = {'If-None-Match': cached[0]} if cached else {}
    resp = client.get(url, headers=headers)

    if resp.status_code == 304:
        return cached[1]  # cached body 씀

    if resp.status_code == 200 and 'ETag' in resp.headers:
        body = resp.json()
        cache[url] = (resp.headers['ETag'], body)
        return body

    resp.raise_for_status()
    return resp.json()

# 두 번 호출 — 두 번째 호출이 304 + cached body
with httpx.Client() as c:
    user1 = get_with_cache(c, 'https://api.example.com/users/42')
    user2 = get_with_cache(c, 'https://api.example.com/users/42')  # 304 경로
    assert user1 == user2

External links

Exercise

어느 JSON resource 든 서빙하는 단일 FastAPI endpoint 에 ETag + conditional-GET 처리 추가. 그 다음 후속 If-None-Match 에 첫 response 의 ETag 써서 curl 로 연속 100 request. 전송된 총 byte 측정 (--write-out '%{size_download}\n' 으로). 비교: full response 100 vs 1 full + 99 conditional 304. 보너스: ETag 계산에 일부러 sort_keys=True 빼고 dict-순서 non-determinism 이 일부 request 의 304 경로 깨는 거 봐.
Hint
ETag = sha256(json.dumps(resource, sort_keys=True)) 계산. Bash: total=0; etag=''; for i in {1..100}; do read body etag < <(curl -s -i -H "If-None-Match: $etag" url | ...); total=$((total + ${#body})); done; echo $total. 기대 결과: 전송 byte ~99% 감소. sort_keys=False 보너스가 gotcha — Python dict 순회가 현재 3.7+ 에선 순서 보존, 근데 set 입력 위 dict comprehension 쓰는 production 코드에서 hash 변동이 얼마나 자주인지 놀라.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.