04. CDN & Signed URL — 멀리 보내고, 안전하게 잠그기
CDN은 파일을 사용자 가까이 옮긴다. 서명 URL은 그 파일에 시간/IP/사용자 자물쇠를 건다. 이 두 메커니즘은 분리된 듯 보이지만 함께 설계해야 한다 — CDN의 캐시 정책이 서명을 무력화시킬 수 있기 때문이다.
한 줄 답
CDN은 “가까이” 의 문제를, 서명 URL은 “잠금” 의 문제를 푼다. 그러나 CDN이 서명된 URL을 잘못 캐시하면 만료된 자물쇠가 다른 사용자에게 열린다 — 그래서 둘은 같은 설계 단위다.
Why — 왜 CDN이 필요한가
거리는 곧 RTT다
| 경로 | 왕복 시간(RTT) | TCP 핸드셰이크 + TLS = 5RTT 정도 |
|---|---|---|
| 서울 ↔ 서울 PoP | ~5ms | 25ms |
| 서울 ↔ 도쿄 | ~30ms | 150ms |
| 서울 ↔ 미국 동부 | ~180ms | 900ms |
| 서울 ↔ 유럽 | ~250ms | 1.25초 |
→ 미국 origin에서 서울로 비디오 첫 프레임 = 1초 이상 지연. CDN의 PoP(Point of Presence) 가 서울에 있으면 같은 작업이 25ms.
Origin 보호
10만 명이 동시에 5GB 영상을 본다면:
- CDN 없음: origin 부하 = 500TB Egress, 비용은 GB당 45,000**
- CDN 있음: edge에서 캐시 히트 99% → origin 부하 = 5TB, $450
→ CDN은 지연 단축과 비용 절감과 origin 보호를 한 번에 한다.
How — CDN의 동작 원리
캐시 계층 구조
| 레이어 | 역할 | TTL 일반값 |
|---|---|---|
| Browser cache | 사용자 디바이스 | 짧게 (1시간) |
| Edge PoP | 가장 가까운 캐시 | 길게 (1일~1년) |
| Origin Shield | 모든 edge가 거치는 중간 캐시 | 매우 길게 |
| Origin | 진짜 파일 (S3 등) | ∞ |
캐시 키 (Cache Key)
CDN이 “이거 같은 요청이야?”를 판단하는 기준.
기본: host + path 만 (https://cdn.com/video.mp4)
추가 가능:
- 쿼리 스트링 (전부 / 일부 / 무시)
- 특정 헤더 (
Accept-Language등) - 쿠키
- 디바이스 타입
함정: 쿼리 무시가 기본이면
?signature=A와?signature=B가 같은 응답으로 캐시된다 → 보안 사고.
CloudFront vs Cloudflare 비교
| 항목 | AWS CloudFront | Cloudflare |
|---|---|---|
| PoP 수 | ~600+ | ~310+ |
| 가격 모델 | Egress 종량제 (지역별) | Workers/Stream 단위, Egress 무료 |
| 서명 URL | Canned Policy / Custom Policy | Workers + HMAC 직접 구현 |
| Origin Shield | 옵션 | 자동 (Argo Smart Routing) |
| TLS 인증서 | ACM 통합 | Universal SSL 자동 |
| 무효화 비용 | 첫 1000회/월 무료, 이후 $0.005/path | 무료 (purge) |
| 통합 | S3, ALB, MediaPackage | R2, Workers, KV |
선택 기준: AWS 생태계 안 → CloudFront. Egress 비용 최소화 → Cloudflare R2 + CDN.
What — 서명 URL의 두 모델
모델 1 — S3 Presigned URL (Origin 직접)
https://bucket.s3.ap-northeast-2.amazonaws.com/key
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=AKIA.../20260510/ap-northeast-2/s3/aws4_request
&X-Amz-Date=20260510T120000Z
&X-Amz-Expires=300
&X-Amz-SignedHeaders=host
&X-Amz-Signature=9b5c...- 누가: AWS SDK가 서명. (
getSignedUrl(s3Client, GetObjectCommand)) - 언제 쓰나: 업로드(PUT)는 거의 항상 이것. 다운로드도 단일 객체면 OK.
- 단점: S3 origin이 직접 호출됨 → Egress 비용, CDN 캐시 안 탐.
실사용 예시 (업로드 5분, 다운로드 1시간)
// 의사 코드 — 실제 파일 경로는 프로젝트별로 다름
async generateUploadPresignedUrl(s3Key, mimeType, expiresIn = 300) {
const command = new PutObjectCommand({
Bucket: this.bucketName,
Key: s3Key,
ContentType: mimeType,
CacheControl: 'public, max-age=31536000', // 또는 no-store
});
return getSignedUrl(this.s3Client, command, { expiresIn });
}모델 2 — CloudFront Signed URL (Edge 잠금)
https://d1234.cloudfront.net/private/video/.../master.m3u8
?Policy=eyJTdGF0...
&Signature=AbCdEf...
&Key-Pair-Id=K2JCJM...- 누가: 백엔드가 RSA 비밀키로 서명. (
getSignedUrl({url, keyPairId, privateKey, dateLessThan})) - 언제 쓰나: HLS 스트리밍, 사용자별 권한, IP 잠금이 필요할 때.
- 장점: edge에서 검증 → origin 부담 없음, 비디오 매니페스트 + 세그먼트 모두 잠금.
Canned Policy vs Custom Policy
| 항목 | Canned | Custom |
|---|---|---|
| 잠금 대상 | 단일 URL | URL 패턴 (와일드카드) |
| 조건 | 만료 시각만 | 만료 + IP + DateGreaterThan |
| Policy 인코딩 | 생략 가능 (URL이 곧 정책) | base64-Policy 파라미터 필수 |
| 사용처 | 이미지 1장 | HLS (마스터+세그먼트 모두 한 번에 잠금) |
Custom Policy 패턴 (실사용 예)
비디오 1개에 마스터 manifest + 수십 개 세그먼트 + sprite + waveform이 묶여 있음. Custom Policy로 한 번 서명하고 fileId 폴더 전체를 풀어준다:
// 의사 코드 — CloudFront 서명 서비스
const policy = {
Statement: [{
Resource: `${this.#domain}/private/*/${categoryDir}/${userId}/${fileId}/*`,
Condition: {
DateLessThan: { 'AWS:EpochTime': Math.floor(expiresAt / 1000) },
},
}],
};
const signed = getCloudFrontSignedCookies({
keyPairId, privateKey, policy: JSON.stringify(policy),
});
return {
policy: signed['CloudFront-Policy'],
signature: signed['CloudFront-Signature'],
keyPairId: signed['CloudFront-Key-Pair-Id'],
expiresAt,
};→ 이 3개 파라미터를 HLS 매니페스트의 모든 URL 끝에 붙이면 마스터/하위 모두 잠금 해제.
모델 3 — Signed Cookie (HTTP 요청 자동 첨부)
URL 파라미터가 아니라 쿠키에 정책/서명을 박는 방식.
| 장점 | 단점 |
|---|---|
| URL이 깨끗 (공유/북마크 가능) | 도메인 통제 안 되면 못 씀 |
| 동일 도메인의 모든 요청에 자동 첨부 | 모바일 앱 (브라우저 외)에서 처리 복잡 |
→ 웹 앱에선 cookie, 모바일/RN에선 URL params가 일반적인 분기.
What — 캐시 무효화 (Invalidation)
비용과 전략
| 전략 | 비용 | 적합한 경우 |
|---|---|---|
| 무효화 안 함 + 영구 캐시 | $0 | 콘텐츠 해시 들어간 URL (logo.9b5c2.png) |
| TTL 짧게 (1시간) | 그만큼 origin 부하 | 자주 갱신되는 메타 |
| 명시적 invalidation | $0.005/path (CF) | 핫픽스, 사고 대응 |
버전 파라미터 (?v=2) | $0 | 작은 정적 파일 |
| 객체 키 자체 변경 | $0 (자연 만료) | 큰 미디어 |
CloudFront Invalidation 함정
- 와일드카드 1개 = 1 path.
/private/*도 1 path지만 모든 PoP에 전파 5~15분 걸림. - 무효화 중에도 옛 응답이 일부 사용자에게 도달 가능.
- 비용은 작지만 운영 빈도가 곧 설계의 안티패턴 신호다 — 자주 무효화 = 캐시 키 설계 잘못.
콘텐츠 해시 패턴 (권장)
# 안 좋은 예
/assets/logo.png — 버전 바뀌면 무효화 필요
# 좋은 예
/assets/logo.9b5c2dc.png — 내용 바뀌면 URL 자체가 바뀜 (immutable 캐시)→ 빌드 시점에 webpack/vite가 내용 해시를 파일명에 박는다. CDN은 영원히 캐시. HTML만 짧게 캐시하고, HTML이 새로운 해시 URL을 가리키게 한다.
What-if — 보안 함정
함정 1 — presigned URL이 슬랙에 떠다님
"여기 영상 보세요: https://bucket.s3.amazonaws.com/private/video.mp4?...&Expires=1234567890..."- 만료 1시간 → 1시간 동안 그 슬랙을 본 누구나 다운로드 가능
- 만료 7일 (악습) → 7일 동안 노출
- 만료 1주일짜리 URL이 GitHub public repo에 푸시됨 → 영구 노출까지
대책:
- 업로드 만료 ≤ 5분
- 다운로드 만료 ≤ 1시간
- 민감 콘텐츠는 CloudFront Signed URL + IP 조건 또는 Signed Cookie
함정 2 — CDN이 쿼리 무시 캐시
CloudFront 기본 설정 (이전 버전): Forward Query Strings: No.
사용자 A: GET /file.mp4?signature=A → 서명 검증 통과 → 200 응답 → CDN이 캐시
사용자 B: GET /file.mp4?signature=B (만료된 서명) → CDN이 *A의 응답*을 그대로 줌 → 200 ❌대책: 서명 URL을 쓰는 path에는 서명 파라미터를 캐시 키에 포함 또는 서명 검증을 CDN이 직접. CloudFront Signed URL은 후자. (CDN이 직접 검증해서 만료 처리)
함정 3 — CORS 설정 누락
브라우저에서 fetch + presigned PUT을 하려면 S3 버킷에 CORS 설정이 있어야 한다.
{
"AllowedMethods": ["PUT", "GET"],
"AllowedOrigins": ["https://app.example.com"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"] // ← Frontend가 ETag를 읽으려면 필수!
}ExposeHeaders에 ETag 빼먹으면 → response.headers.get('ETag') 가 null → multipart complete 실패.
함정 4 — 캐시 무효화 누락의 시각적 증상
- 동영상이 새 버전으로 교체됐는데 옛 영상이 그대로 재생 24시간
- 썸네일 sprite가 갱신됐는데 옛 sprite의 좌표 그리드가 새 영상 위에 잘못 매핑
대책: 콘텐츠 해시 URL + S3 object key 자체에 버전 prefix.
Insight — 흥미로운 이야기
“CDN은 1998년 Akamai의 발명, MIT 알고리즘 대회에서 출발”
Tim Berners-Lee가 1995년 “웹이 곧 폭주할 것”이라며 MIT에 알고리즘 대회를 주최했고, 거기서 우승한 consistent hashing 논문이 곧 Akamai로 창업됐다. 즉, CDN은 캐시 기술이 아니라 알고리즘 기술로 출발했다.
“CloudFront Signed URL이 RSA를 쓰는 이유”
S3 Presigned는 HMAC-SHA256(대칭키)이라 AWS 내부 IAM 자격만으로 충분하다. CloudFront는 공개키 검증을 edge에서 해야 하므로 RSA(비대칭) 가 필요했다. 그래서 키 페어를 발급하고 공개키를 CloudFront에 등록하는 추가 절차가 있다. “왜 이게 더 복잡한가”의 답은 edge가 비밀키를 들고 있을 수 없어서.
“Origin Shield는 가난한 자의 멀티 티어 캐시”
CloudFront는 “edge 600개가 모두 origin을 직접 부르면 origin이 죽는다”는 문제를 Origin Shield라는 단일 중간 PoP를 두어 해결했다. 모든 edge가 그 한 곳을 거치므로 origin은 최대 1번만 fetch. 비용은 약간 더 들지만 (지역 간 transfer), 트래픽 폭주에 강하다.
요약 + Mermaid
CDN은 거리·비용·보호의 3중 문제를 푼다. 서명 URL은 “잠금”의 문제를 푼다. 둘은 함께 설계해야 한다 — 서명 URL이 CDN에 잘못 캐시되면 보안 사고. Custom Policy + 콘텐츠 해시 URL + 짧은 만료가 이 패턴의 3종 세트. CDN 무효화는 도구가 아니라 설계 실수의 보정 도구다 — 자주 쓰면 설계가 잘못된 것.