📁 File1. 전송 프로토콜04. CDN & Signed URL — 멀리 보내고, 안전하게 잠그기

04. CDN & Signed URL — 멀리 보내고, 안전하게 잠그기

CDN은 파일을 사용자 가까이 옮긴다. 서명 URL은 그 파일에 시간/IP/사용자 자물쇠를 건다. 이 두 메커니즘은 분리된 듯 보이지만 함께 설계해야 한다 — CDN의 캐시 정책이 서명을 무력화시킬 수 있기 때문이다.


한 줄 답

CDN은 “가까이” 의 문제를, 서명 URL은 “잠금” 의 문제를 푼다. 그러나 CDN이 서명된 URL을 잘못 캐시하면 만료된 자물쇠가 다른 사용자에게 열린다 — 그래서 둘은 같은 설계 단위다.


Why — 왜 CDN이 필요한가

거리는 곧 RTT다

경로왕복 시간(RTT)TCP 핸드셰이크 + TLS = 5RTT 정도
서울 ↔ 서울 PoP~5ms25ms
서울 ↔ 도쿄~30ms150ms
서울 ↔ 미국 동부~180ms900ms
서울 ↔ 유럽~250ms1.25초

→ 미국 origin에서 서울로 비디오 첫 프레임 = 1초 이상 지연. CDN의 PoP(Point of Presence) 가 서울에 있으면 같은 작업이 25ms.

Origin 보호

10만 명이 동시에 5GB 영상을 본다면:

  • CDN 없음: origin 부하 = 500TB Egress, 비용은 GB당 0.090.09 → **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 CloudFrontCloudflare
PoP 수~600+~310+
가격 모델Egress 종량제 (지역별)Workers/Stream 단위, Egress 무료
서명 URLCanned Policy / Custom PolicyWorkers + HMAC 직접 구현
Origin Shield옵션자동 (Argo Smart Routing)
TLS 인증서ACM 통합Universal SSL 자동
무효화 비용첫 1000회/월 무료, 이후 $0.005/path무료 (purge)
통합S3, ALB, MediaPackageR2, 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

항목CannedCustom
잠금 대상단일 URLURL 패턴 (와일드카드)
조건만료 시각만만료 + 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 끝에 붙이면 마스터/하위 모두 잠금 해제.

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 무효화는 도구가 아니라 설계 실수의 보정 도구다 — 자주 쓰면 설계가 잘못된 것.