[Network] CDN 동작 원리와 캐싱 전략(정적 자산, TTL, 무효화)
개요
이미지, CSS, 자바스크립트 번들, 폰트, 동영상 같은 정적 자산(static asset)이 존재한다. 이걸 원본 서버(Origin) 한 대로 전 세계 사용자에게 직접 내려주면 세 가지 문제가 동시에 생긴다. 첫째, 물리적 거리다. 서울에 있는 서버가 브라질 사용자에게 파일을 보내면 왕복 지연(latency)이 수백 밀리초에 이른다. 둘째, 대역폭과 부하 집중이다. 같은 이미지를 수백만 번 원본에서 직접 내려주면 원본의 네트워크와 CPU가 정적 파일 서빙에 소모된다. 셋째, 가용성이다. 트래픽이 순간적으로 몰리면 정적 자산 요청만으로 원본 서버가 다운되고, 서비스가 마비된다.
CDN(Content Delivery Network)은 이 문제를 지리적으로 분산된 캐시 계층으로 푼다. 사용자와 물리적으로 가까운 엣지(Edge)서버에 콘텐츠 사본을 캐싱해 두고, 원본 대신 엣지가 응답한다. 사용자는 지구 반대편 원본이 아니라 같은 도시 또는 같은 나라의 엣지에서 파일을 받으므로 지연이 크게 줄고, 원본 서버의 정적 자원 요청은 분산된다.
| 용어 | 의미 |
|---|---|
| Origin | 콘텐츠의 원본 서버. 애플리케이션 서버 또는 Object Storage |
| Edge (PoP) | 사용자와 가까운 지점(Point of Presence)에 배치된 캐시 서버 |
| Cache Hit | 요청한 자산이 엣지 캐시에 있어 원본 조회 없이 응답한 경우 |
| Cache Miss | 엣지 캐시에 없어 원본에서 가져와 채운 뒤 응답한 경우 |
| TTL | Time To Live. 엣지가 캐싱한 사본을 신선하다고 간주하는 기간 |
| Purge | 엣지 캐시를 명시적으로 삭제하는 무효화(invalidation) 작업 |
| Origin Shield | 엣지와 원본 사이의 중간 캐시 계층. 원본 부하를 한 번 더 흡수 |
CDN이란 무엇인가
CDN은 Origin, Edge, 사용자의 3계층 구조를 가지고 있다. Origin은 정적 자원 원천 서버이고, Edge는 전 세계 여러 지역에 흩어진 캐시 서버 집합이며, 사용자는 자신과 가장 가까운 Edge에 연결된다.
사용자가 정적 자원을 요청하면 가까운 Edge가 먼저 자기 캐시를 확인한다. 사본이 있으면(Cache Hit) 원본을 거치지 않고 즉시 응답한다. 사본이 없으면(Cache Miss) Edge가 Origin에서 정적 자원을 가져와 자기 캐시에 채운 뒤 사용자에게 응답한다. 이후 같은 지역의 다른 사용자가 같은 자산을 요청하면 이미 채워진 캐시에서 Hit로 응답한다. 즉 첫 사용자만 Miss 비용을 치르고, 나머지는 가까운 Edge에서 빠르게 받는다.
대규모 CDN은 Edge와 Origin 사이에 Origin Shield라는 중간 캐시 계층을 둔다. 전 세계 수십 개 Edge가 각자 Origin을 직접 때리면 Origin 입장에서는 여전히 부하가 크다. 모든 Edge의 Miss를 하나의 Origin Shield로 모으면, Origin은 Shield 한 곳하고만 통신하므로 실제 Origin 요청 수가 크게 줄어든다.
요청이 처리되는 흐름 (Cache Hit / Miss)
사용자 요청이 어느 Edge로 갈지는 보통 DNS가 결정한다. 사용자가 cdn.example.com을 조회하면, CDN의 권한 DNS가 사용자의 위치에 가장 가까운 Edge의 IP를 돌려준다. 네트워크 라우팅이 자동으로 가장 가까운 Edge로 패킷을 흘려보낸다.
Edge에 도달한 요청은 다음 순서로 처리된다.
sequenceDiagram autonumber participant U as 사용자 participant E as Edge (가까운 PoP) participant S as Origin Shield participant O as Origin U->>E: GET /assets/app.a1b2.js alt Cache Hit (엣지에 신선한 사본 존재) E-->>U: 200 OK (엣지 캐시에서 즉시 응답) else Cache Miss (없거나 TTL 만료) E->>S: 원본 조회 요청 alt Shield Hit S-->>E: 200 OK (Shield 캐시에서 응답) else Shield Miss S->>O: 원본에서 자산 요청 O-->>S: 200 OK + Cache-Control S-->>E: 응답 전달 및 Shield 캐싱 end E-->>U: 200 OK + 엣지 캐싱 (TTL 동안 보관) end
TTL이 지난 사본은 무조건 제거되는 게 아니라, Edge가 Origin에 조건부 요청(If-None-Modified 계열)을 보내 아직 유효한 이미지인지 확인한다. 원본이 변하지 않았으면 304 Not Modified를 받아 사본을 그대로 재사용하고 TTL만 갱신한다. 이 재검증 덕분에 만료됐다고 매번 전체 본문을 다시 받지는 않는다.
Edge 내부: 계층형 스토리지와 disk I/O
Edge가 사본을 어떻게 조회하는지 알아보면 다음과 같다.
| 읽기 경로 | 대략적 지연 | 대상 |
|---|---|---|
| Edge 메모리(RAM) Hit | 수십 µs ~ 1ms 미만 | 가장 뜨거운 오브젝트, 캐시 인덱스 |
| Edge 로컬 SSD/NVMe Hit | 수백 µs ~ 수 ms | 캐시 본문 대부분 |
| Edge 로컬 HDD Hit | 수 ms ~ 수십 ms | 대용량·콜드 콘텐츠(동영상 등) |
| Origin fetch (Cache Miss) | 수십 ~ 수백 ms | 대륙 간 RTT + TLS + 원본 처리 |
서울↔브라질 왕복이 200300ms인데 로컬 SSD 읽기는 1ms 안쪽이다. 그래서 disk I/O를 감수해도 Miss를 Hit로 바꾸는 순간 1001000배 빨라진다.
Edge 한 대의 캐시는 단일 저장소가 아니다
Edge 서버의 캐시는 하나의 저장소가 아니라 계층 구조(tiered storage)다.
- In-memory (RAM): 가장 뜨거운 오브젝트 본문과, 어떤 캐시 키가 어느 디스크 위치에 있는지를 가리키는 캐시 인덱스를 메모리에 둔다. 조회는 먼저 이 인덱스를 메모리에서 확인한다.
- SSD/NVMe: 사본의 데이터가 저장되어있는 곳이다.
- HDD: 용량이 큰 롱테일·콜드 콘텐츠(동영상 세그먼트 등)를 담는다.
메모리에서 인덱스 조회 → 디스크에서 body read → 소켓으로 전송으로 조회를 수행한다.
| 실제 CDN | 스토리지 구성 | 특징 |
|---|---|---|
| Netflix Open Connect(OCA) | NVMe SSD + 대용량 HDD | 인기 콘텐츠는 SSD, 롱테일은 HDD. 피크 전 시간대에 미리 적재(fill) |
| Cloudflare / Akamai | 메모리 + SSD 계층 | 디스크→NIC 전송에 sendfile/zero-copy로 유저스페이스 복사 생략 |
정적 자산 캐싱과 TTL
CDN이 캐싱하기 좋은 대상은 모든 사용자에게 동일하고 자주 바뀌지 않는 응답이다. 이미지, CSS, 자바스크립트 번들, 폰트, 동영상 세그먼트가 여기에 해당한다. 반대로 로그인한 사용자마다 다른 응답이나 실시간으로 바뀌는 데이터는 기본적으로 캐싱 대상이 아니다. 잘못 캐싱하면 다른 사용자의 개인화 응답이 노출되는 사고로 이어진다.
Edge가 자산을 얼마나 오래 보관할지는 TTL(Time To Live)이 결정한다. TTL의 근거는 대부분 원본이 내려주는 HTTP 응답 헤더다. 우선순위는 CDN 설정, 그다음 응답 헤더 순으로 적용되는 것이 일반적이다.
| 제어 방식 | 의미 |
|---|---|
Cache-Control: max-age | 브라우저 캐시 기간(초). 최종 사용자 캐시 대상 |
Cache-Control: s-maxage | 공유 캐시(CDN·프록시) 전용 기간. max-age 보다 우선 |
Expires | 만료 절대 시각. 구형 방식. Cache-Control 이 있으면 무시 |
ETag / Last-Modified | 재검증용 식별자. TTL 만료 후 변경 여부 확인 |
| CDN 자체 TTL | 대시보드에서 경로·확장자별로 강제 지정. 헤더보다 우선 가능 |
// 잘못된 패턴: 불변 자산에 짧은 TTL, 또는 캐싱 자체를 막음
// 매 요청마다 Origin 을 때리고, CDN 이 있어도 Hit 율이 바닥이다
response.setHeader("Cache-Control", "no-store");
// 올바른 패턴: 콘텐츠 해시가 파일명에 있는 불변 자산은 아주 길게 캐싱
// immutable은 TTL 안에는 재검증조차 하지 말라는 의미
// app.a1b2c3.js 처럼 내용이 바뀌면 파일명이 바뀌므로 안전하다
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");첫째는 파일명에 콘텐츠 해시가 존재하는 정적 자원이다. 빌드 도구는 파일 내용으로 해시를 계산해 파일명에 넣는다. 코드를 고치면 내용이 바뀌고, 내용이 바뀌면 해시도 바뀌어 파일명이 통째로 달라진다. app.a1b2c3.js 를 수정하면 app.d4e5f6.js 가 되는 식이다. 이런 자산은 유니크하므로 하나의 URL이 절대 다른 내용을 담지 않는다. 내용이 바뀌는 순간 URL 자체가 바뀌기 때문이다. 그래서 불변(immutable) 자산이라 부른다. 내용이 고정된 게 보장되니 TTL을 1년(31536000초)으로 걸어도 안전하다. 갱신이 필요하면 어차피 새 파일명으로 요청이 오고, Edge 에 그 사본이 없어 자연히 새 파일을 받는다.
둘째는 파일명이 고정된 자산이다. 로고 이미지는 그림을 바꿔도 URL이 계속 logo.png 다. 여기에 긴 TTL을 걸면 로고를 교체해도 캐시에 남은 옛 로고가 TTL이 끝날 때까지 응답된다. 그래서 이런 자산은 TTL을 짧게 잡고, 만료될 때마다 재검증(revalidation)으로 원본이 바뀌었는지 확인한다.
| 자산 종류 | 예시 | TTL | 이유 |
|---|---|---|---|
| 이름이 바뀌는 불변 자산 | app.a1b2c3.js | 아주 길게 (1년) | 내용이 바뀌면 URL 이 바뀌므로 옛 캐시가 문제되지 않는다 |
| 이름이 고정된 자산 | logo.png | 짧게 + 재검증 | URL 이 그대로라 옛 캐시가 계속 나갈 수 있다 |
CDN 캐싱 방법
Edge는 요청을 캐시 키(cache key)로 식별한다. 기본 캐시 키는 호스트와 경로다. 문제는 쿼리스트링, 헤더, 쿠키를 캐시 키에 포함하느냐다. 이걸 잘못 잡으면 같은 자산인데도 캐시가 잘게 쪼개져 Hit율이 떨어진다.
# 잘못된 패턴: 추적용 쿼리스트링까지 캐시 키에 포함
# /img/cat.png?utm_source=a 와 ?utm_source=b 가 서로 다른 캐시로 취급되어
# 같은 이미지가 파라미터 조합 수만큼 중복 캐싱된다 (Hit 율 붕괴)
proxy_cache_key "$scheme$host$request_uri";
# 올바른 패턴: 콘텐츠에 영향을 주는 파라미터만 캐시 키에 남기고 나머지는 무시
# 이미지 응답에 영향 없는 utm_* 추적 파라미터는 캐시 키에서 제외한다
map $arg_v $cache_version { default ""; }
proxy_cache_key "$scheme$host$uri$cache_version";응답이 요청 헤더에 따라 달라진다면(예: 압축 방식) 원본이 Vary 헤더로 그 사실을 알려야 한다. Vary: Accept-Encoding 이 있으면 Edge는 압축 방식별로 사본을 따로 캐싱한다. Vary 를 남발해 사용자마다 값이 다른 헤더(예: User-Agent 전체)를 넣으면 캐시가 사용자 수만큼 쪼개져 사실상 캐싱이 무력화된다.
지역별 캐싱과 엣지 라우팅
Edge 캐시는 지역(PoP)마다 독립이다. 서울 Edge가 어떤 자원을 캐싱했다고 해서 프랑크푸르트 Edge가 그 자원을 가진 것은 아니다. 각 지역은 그 지역의 첫 요청에서 한 번 Miss를 겪고 캐시를 채운다. 이걸 콜드 캐시(cold cache)라 하고, 이미 채워진 상태를 웜 캐시(warm cache)라 한다.
이 특성 때문에 배포 직후나 신규 지역에서는 잠깐 Miss가 몰릴 수 있다. 트래픽이 큰 서비스는 배포 후 주요 자산을 미리 요청해 캐시를 데워 두거나(cache warming), Origin Shield로 지역 간 Miss를 한 곳에 모아 Origin 부하를 흡수한다.
백엔드에서 CDN 다루기 (Object Storage 연동)
백엔드에서 정적 자산을 다룰 때, Object Storage(S3, GCS, Cloudflare R2 등)를 Origin으로 두고 그 앞에 CDN을 붙일 수 있다. 파일은 Object Storage에 올리고, 사용자에게는 CDN 도메인 URL만 넘긴다. Edge는 최초 Miss때 Object Storage에서 파일을 당겨와(pull) 캐싱하고, 이후로는 Edge가 응답한다.
CDN URL은 어떻게 정해지고, Edge는 원본을 어떻게 아는가
클라이언트는 CDN URL을 어떻게 알고, Edge는 어느 Object Storage에서 원본을 가져올지 어떻게 아는가.
먼저 클라이언트가 URL을 아는 경로다. 백엔드가 파일의 objectKey를 저장해 두고, CDN 도메인과 그 키를 합쳐 완전한 URL 을 만들어 데이터( CDN_DOMAIN + "/" + objectKey)로 넘겨준다.예를 들어 사용자 프로필 조회 API 가 profileImageUrl 필드에 https://cdn.example.com/assets/abc123.png 같은 값을 담아 주고, 클라이언트는 그 값을 그대로 이미지 주소로 쓴다.
CDN 배포(distribution)를 만들 때 origin, 즉 Miss때 pull할 원본 도메인을 한 번 등록한다. 이 origin이 Object Storage의 엔드포인트다.
# CDN 배포를 한 번 구성한다 (개념 예시)
domain: cdn.example.com # 사용자가 요청할 도메인
origin: my-assets.s3.amazonaws.com # Miss 시 Edge 가 pull 할 원본 (Object Storage)
# DNS: cdn.example.com 을 CDN 제공자 도메인에 CNAME 으로 연결한다
# cdn.example.com. CNAME d123abc.cloudfront.net.첫째, DNS의 CNAME 덕분에 클라이언트가 cdn.example.com 을 조회하면 CDN 제공자로 넘어가고, 최종적으로 가까운 Edge의 IP로 해석된다. 그래서 클라이언트의 요청이 원본이 아니라 Edge에 도착한다. 둘째, Edge는 요청의 Host 헤더(cdn.example.com)로 어느 배포인지 식별하고, 그 배포에 등록된 origin으로 경로를 그대로 이어 붙여 페치한다. cdn.example.com/assets/abc123.png 의 Miss 는 my-assets.s3.amazonaws.com/assets/abc123.png 조회로 이어진다. 즉 도메인만 CDN으로 바뀌고 경로는 보존된다.
Edge는 언제, 어떻게 원본에서 가져오는가
CDN은 원본을 하나의 도메인(origin domain)으로 등록해 둔다. Edge는 Cache Miss시, 원본 도메인으로 GET 요청을 보낸다. 이걸 오리진 페치(origin fetch)라 한다. Object Storage는 이 요청을GET으로 받아 파일을 응답한다.
오리진 페치는 원본이 준 응답 헤더를 그대로 활용한다. Edge는 응답의 Cache-Control, ETag, Last-Modified 를 보고 TTL 과 재검증 규칙을 정한다. TTL이 만료되면 다시 원본에 조건부 GET(If-None-Match)을 보내고, 안 바뀌었으면 304 로 사본을 재사용한다.
Purge는 어떻게 호출하는가 (cdnClient 구현)
CDN은 두 평면으로 나뉜다. 데이터 평면(data plane)은 사용자와 Edge 사이의 서빙 경로다. 여기에는 백엔드가 없다. 컨트롤 평면(control plane)은 CDN 을 설정하고 조작하는 관리 경로다. Purge, 캐시 워밍, 배포 설정 변경이 모두 여기에 속한다. 백엔드가 CDN을 건드리는 것은 전부 이 컨트롤 평면, 즉 제공자가 제공하는 관리 API 를 HTTPS 로 호출하는 일이다.
CloudFront 는 AWS SDK 의 createInvalidation, Cloudflare 는 POST /zones/{zoneId}/purge_cache, Fastly는 서로게이트 키 기반 purge를 쓴다.
// domain: 외부 시스템 호출이라 Gateway 로 둔다 (특정 벤더에 의존하지 않는다)
public interface CdnGateway {
void purge(List<String> paths);
}
// infrastructure: 제공자 관리 API 를 감싼 구현 (CloudFront 예시)
@Component
public class CloudFrontCdnGateway implements CdnGateway {
private final CloudFrontClient cloudFront; // AWS SDK 클라이언트, 여기에만 주입
private final String distributionId;
@Override
public void purge(List<String> paths) {
// 컨트롤 평면 호출: Edge 노드가 아니라 CDN 관리 엔드포인트로 간다
CreateInvalidationRequest request = CreateInvalidationRequest.builder()
.distributionId(distributionId)
.invalidationBatch(b -> b
.callerReference(UUID.randomUUID().toString()) // 재시도 멱등 키
.paths(p -> p.items(paths).quantity(paths.size())))
.build();
cloudFront.createInvalidation(request); // 비동기: 전파에 수 초 걸린다
}
}무효화는 비동기라 호출 즉시 반영되지 않고 전 세계 Edge 로 전파되는 데 수 초가 걸린다. 제공자마다 경로 수와 호출 횟수에 rate limit과 과금이 있으므로, 파일 하나하나를 잦게 Purge 하기보다 와일드카드(/*)나 태그 기반 무효화로 묶는 편이 낫다. 관리 API 호출도 외부 연동이라 실패할 수 있으니 재시도와 멱등 키(callerReference)를 둔다.
서빙 전략 비교: 프리사인 URL 캐싱 vs CDN 앞단
Object Storage에 있는 파일을 사용자에게 내려주는 방법은 크게 두 갈래다. 하나는 백엔드가 프리사인 GET URL(presigned URL)을 발급하고 클라이언트가 그 URL 로 Object Storage에 직접 접근하는 방식이다. 다른 하나는 지금까지 설명한 CDN 앞단 방식으로, Edge가 자원을 캐싱하고 Miss 때만 원본을 조회한다.
프리사인 URL 방식에서 URL을 Redis나 로컬 캐시에 캐싱할 수 있다. 프리사인 URL 생성은 서명 연산이라 네트워크 호출 없이 끝나므로 생성 비용 자체는 저렴하다. 같은 객체로 만들어진 같은 URL을 재사용해 브라우저와 다운스트림 캐시 Hit를 살릴 수 있다. 요청마다 서명이 달라 URL이 매번 바뀌면 브라우저는 같은 파일도 캐시 Miss로 다시 받는다.
@Service
@RequiredArgsConstructor
public class AssetUrlService {
private final S3Presigner presigner;
private final StringRedisTemplate redis;
private static final Duration URL_TTL = Duration.ofMinutes(15);
// 캐시 TTL 은 URL 만료보다 짧게 잡는다
// 만료 직전의 URL 을 캐시에서 돌려주면 클라이언트가 403 을 받는다
private static final Duration CACHE_TTL = Duration.ofMinutes(10);
public String presignedGetUrl(String objectKey) {
String cacheKey = "asset:url:" + objectKey;
// 로컬 캐시가 아니라 Redis 공유 캐시라야 모든 인스턴스가 같은 URL 을 준다
// 같은 URL 재사용이 곧 브라우저 캐시 Hit 다
String cached = redis.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
GetObjectPresignRequest req = GetObjectPresignRequest.builder()
.signatureDuration(URL_TTL)
.getObjectRequest(b -> b.bucket("my-assets").key(objectKey))
.build();
String url = presigner.presignGetObject(req).url().toString();
redis.opsForValue().set(cacheKey, url, CACHE_TTL);
return url;
}
}프리사인 URL 방식은 사용자가 Object Storage로부터 받으므로, 엣지 캐싱이 없어 원거리 사용자에게 느리고 핫 콘텐츠 부하가 Storage에 집중된다. 반면 접근 제어는 강력하다. 권한을 검증한 사용자에게만 짧은 수명의 URL을 발급하므로 비공개 파일에 적합하다. CDN 앞단 방식은 정반대다. 엣지 캐싱으로 지연과 원본 부하를 줄이지만, 비공개 파일에 접근 제어를 걸려면 CDN의 서명 URL이나 서명 쿠키를 따로 구성해야 한다.
| 기준 | 프리사인 URL 캐싱(Redis/로컬) | CDN 앞단 캐싱 |
|---|---|---|
| 서빙 위치 | Object Storage 리전에서 직접 | 사용자 근처 Edge |
| 지연 | 리전 거리에 비례. 원거리 사용자 느림 | 낮음. 엣지 근접 |
| 원본 부하 | 핫 콘텐츠가 Storage에 집중 | Edge가 흡수, Miss만 원본 조회 |
| 대역폭 비용 | Storage egress. 대량 시 비쌀 수 있음 | CDN egress. 보통 더 저렴 |
| 접근 제어 | 강함. 권한 검증 후에만 URL 발급 | CDN 서명 URL·쿠키 별도 구성 필요 |
| 캐시 무효화 | URL 만료로 자연 만료 | Purge 또는 파일명 버저닝 |
| 캐시 공유 | Redis 공유 권장. 로컬은 URL 파편화 | 엣지가 전역으로 공유 |
| 적합 대상 | 비공개·접근 제어·소규모/지역 트래픽 | 공개·핫·전 세계 정적 자산 |
퍼블릭 정적 자산은 CDN, 사용자별 비공개 파일(영수증, 개인 다운로드 링크)은 프리사인 URL로 서빙할 수 있다. 비공개인데 전 세계에서 자주 요청되는 파일이라면 CDN 서명 URL(signed URL)로 두 방식의 장점을 합칠 수 있다.
- 클라이언트가 백엔드에 파일 접근을 요청하면
- 백엔드는 인증을 검증한 뒤 개인키로 서명한 URL을 발급한다.
- 이 서명에는 만료 시각과 허용 경로 같은 정책이 담긴다. 클라이언트는 그 서명 URL로 CDN Edge에 요청하고
- Edge는 등록된 키로 서명을 검증한다. 유효하면 진행하고, 만료되거나 위조됐으면 403을 반환한다. Miss면 Edge가 비공개 버킷에서 원본을 페치하는데
- 버킷은 공개 접근을 막아 두고 CDN의 신원(OAC, Origin Access Control)에만 읽기를 허용한다. 마지막으로 Edge가 응답하며 사본을 캐싱한다. 이후 같은 파일 요청은 서명만 유효하면 엣지에서 바로 나간다.
결과적으로 접근 제어는 백엔드 서명과 Edge 검증이, 원본 부하 분산은 Edge 캐싱이 나눠 맡는다. 비공개 파일도 로그인 사용자마다 응답이 완전히 같으면(같은 파일) 캐싱이 가능하다는 점이 핵심이다. 서명은 접근 자격만 통제하고, 응답 본문 자체는 동일하기 때문이다.
서명이 매번 달라도 DNS와 캐싱이 되는 이유
서명 URL은 만료 시각과 서명 값이 붙어 요청마다 달라질 수 있는데, 그러면 DNS 조회나 캐싱이 매번 새로 일어나는가.
DNS는 URL 전체가 아니라 도메인(호스트네임)만 해석한다. https://cdn.example.com/private/receipt.pdf?Expires=...&Signature=... 에서 DNS가 보는 것은 cdn.example.com 이고, 이를 기반으로 라우팅한다. 서명은 경로와 쿼리스트링에 담기는데, 이건 HTTP 요청의 일부라 TCP 연결이 맺어진 뒤에야 전송된다. DNS는 그 전에 도메인만 받아 IP를 돌려준다. 그래서 서명이 아무리 바뀌어도 도메인은 그대로이고, DNS는 늘 가까운 같은 Edge IP를 반환한다.
캐싱도 마찬가지로 유지된다. Edge의 캐시 키에서 서명 파라미터를 제외하도록 설정하기 때문이다. 캐시 키는 경로(어떤 오브젝트인가)를 기준으로 잡고, Expires 와 Signature 같은 인증용 쿼리는 캐시 키에서 뺀다. 그래서 서명이 다른 두 요청도 같은 오브젝트라면 같은 캐시 항목으로 Hit 된다. 만약 서명 파라미터를 캐시 키에 포함해 버리면 요청마다 캐시 키가 달라져 전부 Miss가 나는데, 이건 서명 URL을 쓸 때 피해야 할 사항이다.
시나리오: 가끔 바뀌는 정적 자원
배너 이미지, 약관 문서, 앱 설정 파일처럼 하루에 수천 번 읽히지만 변경은 한 달에 몇 번뿐인 자원이 해당한다. 목표는 평상시에는 캐시 Hit율을 높여 원본 부하를 줄이고, 드물게 변경이 생기면 그걸 빠르게 반영하는 것이다. TTL을 길게 잡으면 Hit율은 오르지만 변경 반영이 TTL만큼 늦고, 짧게 잡으면 반영은 빠르지만 원본 재검증 트래픽이 잦아진다.
| 전략 | 평상시 Hit율 | 변경 반영 | 추가 부하 | 운영 |
|---|---|---|---|---|
| 긴 TTL + 변경 시 Purge | 높음 | 즉시 (전파 수 초) | 없음 | 변경 훅에서 Purge 호출 |
| 짧은 TTL + 재검증 | 중간 | 최대 TTL 만큼 지연 | 만료마다 조건부 요청 | 자동, 무운영 |
쿼리 버전(logo.png?v=2) | 높음 | 즉시 | 없음 | 참조 URL 갱신 필요 |
변경이 잦지 않다는 이 시나리오의 특성 위에서는 긴 TTL + 변경 시 Purge가 가장 잘 맞는다. 변경이 드무니 Purge 호출 횟수가 적어 비용과 전파 지연 부담이 거의 없고, 평상시에는 긴 TTL로 대부분을 Edge Hit로 처리한다. 변경이 발생하는 순간에만 백엔드 애플리케이션에서 Purge를 호출해 해당 경로를 무효화하면, 다음 요청부터 최신본이 채워진다.
// 평상시: 긴 TTL 로 Edge Hit 율을 높인다 (변경 전까지 Edge 가 원본을 안 간다)
response.setHeader("Cache-Control", "public, max-age=86400"); // 1일
// 변경이 발생하는 드문 순간에만 해당 경로를 무효화한다
public void updateBanner(Banner banner) {
bannerRepository.save(banner); // 원본(Object Storage·DB) 갱신
cdnClient.purge(List.of("/banners/main.png")); // 변경 시에만 Purge → 즉시 반영
}URL을 바꿀 수 있다면 쿼리 버전이나 파일명 버저닝이 더 안전하다. Purge 전파를 기다릴 필요도, 캐시 키 오설정으로 옛 캐시가 남을 위험도 없다. 다만 그 URL을 참조하는 곳(HTML, DB 에 저장된 링크)을 모두 갱신해야 하므로, 참조가 여기저기 흩어져 있으면 Purge가 현실적이다.
반영 지연이 허용되고 운영을 최소화하고 싶다면 짧은 TTL + 재검증도 선택지다. 원본이 ETag 를 붙여 두면 TTL 만료 시 Edge가 조건부 요청을 보내고, 안 바뀌었으면 304 로 값을 재사용한다. 변경은 최대 TTL만큼 지연되지만 Purge 연동 없이 자동으로 최신화된다. 여기에 Cache-Control: public, max-age=60, stale-while-revalidate=600 처럼 stale-while-revalidate를 더하면, 재검증하는 동안 옛 사본을 잠깐 계속 응답해 지연과 원본 부하를 함께 낮출 수 있다.
클라이언트에서의 CDN
클라이언트 관점에서 CDN은 두 겹의 캐시 중 바깥 겹이다. 브라우저는 자기 로컬 캐시를 먼저 보고, 없으면 CDN Edge에 요청하고, Edge에도 없으면 그때 Origin까지 간다. Cache-Control: max-age 는 브라우저 캐시를, s-maxage 는 CDN 캐시를 각각 제어하므로 둘을 분리해 설정할 수 있다.
프론트엔드 빌드 도구는 이 구조에 맞춰 산출물 파일명에 콘텐츠 해시를 추가한다. 그래서 배포 때마다 바뀐 파일만 새 URL을 얻고, 바뀌지 않은 파일은 브라우저와 Edge 캐시를 그대로 재사용한다.
<!-- 빌드 도구가 파일명에 해시를 박아 준다: app.a1b2c3.js -->
<!-- 다음 배포에서 코드가 바뀌면 app.d4e5f6.js 로 URL 이 달라져 캐시가 자동 분리된다 -->
<script src="https://cdn.example.com/assets/app.a1b2c3.js"></script>
<!-- CDN 도메인에 미리 연결을 열어 첫 자산 요청의 지연을 줄인다 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="dns-prefetch" href="https://cdn.example.com" />preconnect 는 브라우저가 실제 자산을 요청하기 전에 CDN 도메인으로 DNS 조회, TCP 연결, TLS 핸드셰이크를 미리 끝내 두게 한다. 첫 요청의 왕복 지연이 눈에 띄게 줄어든다.
CDN의 다양한 활용
| 활용 | 내용 |
|---|---|
| 정적 사이트 호스팅 | 빌드 산출물 전체를 CDN에 올려 서버 없이 웹사이트를 서빙 |
| 동적 콘텐츠 가속 | 캐싱 불가한 API도 Edge까지의 최적 경로·연결 재사용으로 가속 |
| 엣지 컴퓨팅 | Edge에서 경량 함수 실행. A/B 분기, 인증, 리다이렉트를 원본 없이 처리 |
| 보안 | WAF, DDoS 완화, TLS 종단을 Edge 에서 수행해 원본을 가림 |
| 동영상 스트리밍 | 세그먼트 파일을 Edge 캐싱해 대역폭과 버퍼링을 개선 |
특히 엣지 컴퓨팅은 원본까지 가지 않고 Edge에서 로직을 실행하므로, 개인화가 약한 분기 처리(지역별 리다이렉트, 봇 차단, 간단한 인증 검사)를 앞단에서 끝낼 수 있다.
주의사항과 베스트프랙티스
CDN은 잘 쓰면 지연과 원본 부하를 동시에 줄이지만, 캐시라는 속성 때문에 특유의 실수 케이스가 있다.
첫째, 보안 관련 응답을 캐싱하면 안된다. 로그인 사용자 정보나 Set-Cookie 가 담긴 응답을 Edge 가 캐싱하면, 다른 사용자가 그 사본을 받아 남의 세션이 노출된다. 사용자마다 다른 응답에는 Cache-Control: private, no-store 를 명시하고, 캐싱 대상은 경로·확장자로 명확히 화이트리스트한다.
둘째, 캐시 키 설계로 Hit율이 갈린다. 추적용 쿼리스트링이나 사용자별 헤더를 캐시 키에 넣으면 같은 자산이 조합 수만큼 중복 캐싱되어 Hit율이 낮아진다. 콘텐츠에 영향을 주는 파라미터만 캐시 키에 남긴다.
셋째, 무효화는 즉시가 아니다. Purge는 전 세계 Edge에 전파되는 데 시간이 걸린다. 반영 즉시성이 필요한 자산은 Purge에 의존하지 말고 파일명 버저닝으로 새 URL을 발급한다.
넷째, 콜드 캐시 동시 Miss(thundering herd)를 경계한다. 인기 자산의 TTL 이 동시에 만료되면 다수 요청이 한꺼번에 Origin 으로 몰린다. Origin Shield 와 요청 병합(request collapsing)으로 동일 자산의 동시 Miss 를 하나의 원본 요청으로 합친다.
다섯째, TTL 은 자산 성격에 맞춘다. 콘텐츠 해시가 박힌 불변 자산은 길게(immutable), 이름이 고정된 자산은 짧게 잡고 재검증에 의존한다. 모든 자산에 같은 TTL 을 쓰는 것은 안전하지도 효율적이지도 않다.
여섯째, 비용 모델을 이해한다. CDN 은 엣지 트래픽, 요청 수, Purge 횟수에 과금한다. Hit 율이 높을수록 원본 트래픽과 비용이 함께 줄고, Purge 남발은 비용과 Miss 를 동시에 유발한다. 결국 좋은 캐싱 설계가 곧 비용 최적화다.