CloudFront 재생 경로와 반복 재생 실측
S3 직접 전송에 엣지 캐시를 얹고 서명 URL로 접근을 잠근 뒤, 같은 파일로 콜드와 웜을 재 본 기록. MSG-67, 2026-08-27 dev 실측.
요약
재생 API가 접근 권한을 판정한 뒤 S3 사전서명 URL 대신 CloudFront 서명 URL을 발급한다. 원본 버킷은 OAC로 닫아 CloudFront만 읽고, 파생 파일 경로에 인코딩 시도 ID를 붙여 교체된 영상이 옛 캐시를 받지 않게 했다. 개선 폭을 합격 조건으로 미리 고정하지 않고, 느려진 축도 그대로 적었다.
서울에서 MP4 세 개(2.6MB, 1.1MB, 3.0MB)로 측정. S3 직접은 9회 중앙값, 콜드는 파일별 첫 요청 3회의 중앙값, 웜은 파일별 10회씩 30회의 중앙값이다.
가설과 측정 방식
재생 요청은 서울 리전 S3로 바로 갔다. 국내 접속이라 첫 요청의 지연 차이는 작을 수 있다. 다만 격자 대표 영상과 핫구역처럼 같은 영상을 여럿이 반복해서 보는 자리에서는 엣지 캐시가 원본 요청과 전송량을 줄일 여지가 있다. 이걸 미리 단정하지 않고 같은 파일로 세 경로를 비교하기로 했다.
- S3 직접: 기존 사전서명 URL. 9회.
- CloudFront 콜드: 캐시가 비어 있어 엣지가 S3 원본까지 다녀오는 첫 요청. 파일별 3회.
- CloudFront 웜: 캐시가 채워진 뒤의 반복 요청. 파일별 10회.
요청 종류는 두 가지다. 파일 전체를 받는 GET과, 플레이어가 재생 시작이나 시킹 때 쓰는 첫 1MiB Range 요청. 각 요청에서 첫 바이트가 올 때까지(TTFB)와 다 받을 때까지(완료)를 쟀다.
설계
MP4 그대로, HLS 없이
Jira의 옛 AI 컨텍스트에는 인코딩 산출물이 HLS라고 적혀 있었지만 실제 산출물은 +faststart가 적용된 720p H.264와 AAC MP4 한 파일이다. CloudFront는 전체 파일과 Range 요청을 모두 캐시할 수 있고, 영상은 최대 30초라 HLS의 재생 목록, 세그먼트 저장과 정리, 서명 쿠키 계약을 새로 만들 이유가 없었다. 이 티켓은 성능 검증이지 포맷 전환이 아니다.
서명 URL과 캐시 키
재생과 썸네일은 객체 하나를 여는 요청이라 canned policy 서명 URL을 쓴다. 기존 응답의 expiresInSec=600 계약은 그대로다. 애플리케이션이 AWS SDK의 CloudFrontUtilities로 서명하고, 공개 키 ID와 PKCS#8 개인 키 파일 경로를 설정으로 받는다. 개인 키는 레포와 환경변수에 넣지 않고 EC2의 권한 600 파일로 둔다.
캐시 정책은 쿼리 문자열, 쿠키, 요청 헤더를 캐시 키에서 뺐다. 서명 파라미터는 사용자마다 다르지만 객체 경로가 같으면 같은 캐시를 쓴다. 이게 없으면 사용자마다 캐시가 따로 생겨 반복 재생의 이점이 사라진다. TTL은 기본과 최대 모두 600초다.
cloudfront.enabled=false인 로컬과 CI에서는 기존 S3 사전서명 URL을 그대로 발급한다.
원본은 OAC로 닫는다
CloudFront는 Origin Access Control로 버킷을 읽는다. 버킷 정책은 해당 배포 ARN이 videos/encoded/*, videos/blurred/*, videos/thumb/*를 읽는 s3:GetObject만 추가로 허용한다. 원본과 pending 파일은 CDN이 읽지 못한다. 배포에는 신뢰 키 그룹을 연결해 모든 조회에 서명을 요구한다.
순서가 중요하다. CloudFront 배포와 버킷 정책을 먼저 반영한 뒤 애플리케이션 플래그를 켠다. 거꾸로 하면 아직 읽을 수 없는 CDN URL이 API에서 나간다.
교체마다 다른 파일 경로
기존 인코딩본, 블러본, 썸네일 키는 videoId만 들어 있어 영상을 교체하면 같은 키를 덮어썼다. CDN이 이전 바이트를 캐시한 상태에서 같은 경로를 다시 요청하면 새 URL도 이전 영상을 받는다. 그래서 새 키에 인코딩 시도 ID를 붙였다.
videos/encoded/{userId}/{videoId}/{attemptId}.mp4
videos/blurred/{userId}/{videoId}/{attemptId}.mp4
videos/thumb/{userId}/{videoId}/{attemptId}.jpg
attemptId는 원본 S3 키에서 UUID.nameUUIDFromBytes로 결정적으로 만든다. 새 원본이면 새 경로가 되고, 같은 시도를 다시 실행하면 같은 경로라 재시도 업로드가 멱등이다. 교체 트랜잭션은 이전 시도의 원본과 파생 파일 키를 잡아 뒀다가 커밋 뒤 삭제한다. DB에 남은 구형 키는 그대로 읽을 수 있어 마이그레이션은 없다.
흐름 도해
실측
세 파일마다 전체 GET과 1MiB Range GET을 콜드 1회, 웜 10회 실행했다. 값은 모두 중앙값이다.
웜 캐시는 네 축 모두에서 S3 직접보다 빠르고, 콜드는 네 축 모두에서 느리다. 이 구성의 이점은 첫 재생이 아니라 같은 영상의 반복 재생에서 나온다.
| 요청 | S3 직접 | CloudFront 콜드 | CloudFront 웜 | 웜 개선 폭 |
|---|---|---|---|---|
| 전체 GET 첫 바이트 | 105.9ms | 199.2ms | 51.8ms | 51.1% |
| 전체 GET 완료 | 250.8ms | 284.4ms | 181.0ms | 27.8% |
| 1MiB Range 첫 바이트 | 94.4ms | 191.6ms | 48.6ms | 48.5% |
| 1MiB Range 완료 | 181.6ms | 285.0ms | 129.2ms | 28.9% |
첫 바이트가 절반으로 준 것에 비해 완료 시간은 28% 정도 줄었다. 완료 시간에는 전송 자체가 포함되는데, 서울 리전 S3와 서울 엣지 사이의 전송 속도 차이는 첫 바이트 지연 차이만큼 크지 않다. 새로 발급한 서명 URL도 같은 객체 캐시를 적중했다. 캐시 키에서 쿼리 문자열을 뺀 효과다.
함께 검증한 것
| 항목 | 결과 |
|---|---|
| 접근 제한 | 미서명 URL과 만료 URL은 403, 유효한 전체 GET은 200 |
| Range 요청 | Range: bytes=0-1048575에 206과 올바른 Content-Range. 교체 뒤 새 경로에서도 206 |
| 캐시 적중 | X-Cache가 첫 요청 Miss, 반복 요청 Hit. 웜 적중은 전체 GET 29/30, Range 30/30 |
| 영상 교체 | 비공개 영상 하나를 실제로 교체했다. 재생 경로가 videos/encoded/524/240325.mp4에서 videos/encoded/524/240325/2483237a-….mp4로 바뀌었고 이전 인코딩본과 썸네일은 S3에서 삭제됐다 |
| API 계약 | 신규 필드 없음. playbackUrl과 thumbnailUrl의 호스트와 서명 파라미터만 바뀌고 expiresInSec은 600 그대로 |
| 롤백 준비 | 배포 전 JAR과 환경 파일을 app.jar.pre-msg67, fillmap-dev.env.pre-msg67로 보존 |
교체 검증의 첫 폴링 스크립트는 zsh 예약 변수명 충돌로 중단됐다. 교체 요청은 이미 접수된 상태라 새 요청을 만들지 않고 현재 상태를 이어서 조회해 READY 수렴을 확인했다.
말할 때 주의
용어
| 용어 | 이 문서에서의 뜻 |
|---|---|
| 서명 URL | 애플리케이션이 만료 시각과 객체 경로를 개인 키로 서명한 주소. CloudFront는 신뢰 키 그룹의 공개 키로 확인한 뒤 캐시나 원본에서 파일을 준다 |
| canned policy | 객체 하나와 만료 시각만 담는 짧은 서명 정책. 시작 시각이나 IP 제한이 있는 custom policy보다 URL이 짧고 현재 계약에 맞다 |
| OAC (Origin Access Control) | CloudFront가 S3에 보내는 요청을 AWS 서명으로 인증하는 설정. 버킷을 공개하지 않고 지정한 배포만 객체를 읽게 한다 |
| Range 요청 | 파일 전체가 아니라 지정한 바이트 구간만 받는 HTTP 요청. 플레이어가 재생 시작이나 시킹 때 필요한 부분만 읽는 데 쓴다. 응답 코드는 206 |
| TTFB (첫 바이트 응답) | 요청을 보낸 뒤 응답의 첫 바이트가 도착할 때까지의 시간. 전송량과 무관한 지연을 본다 |
| 콜드 / 웜 | 엣지 캐시가 비어 원본까지 다녀오는 요청 / 캐시가 채워져 엣지에서 바로 응답하는 요청 |
| 캐시 키 | CloudFront가 "같은 응답"으로 볼 기준. 여기서는 객체 경로만이라 서명 파라미터가 달라도 캐시를 공유한다 |
출처: docs/spec/MSG-67.md 설계 결정 D1~D6, dev 실측, 2026-08-27 작업 로그. dev 배포 ID와 자원 식별자는 스펙 문서에 있다. 이 문서는 2026-09-05에 정리했다.