안녕하세요, 자바파커입니다.
서버는 서울에 있습니다. 국내 사용자는 빠릿합니다. 그런데 브라질 사용자가 "느리다"고 합니다.
서버 사양을 올렸습니다. 그대로입니다.
당연합니다. 서버가 느린 게 아니라 멀어서 느린 거니까요.
결론부터 말씀드리면 — 네트워크 지연의 바닥은 거리가 정합니다. CPU를 아무리 올려도 이 바닥은 안 내려갑니다. CDN은 서버를 빠르게 만드는 물건이 아니라 사본을 사용자 가까이 옮겨서 거리를 줄이는 물건입니다.
거리는 왜 지연이 되나
빛보다 빠른 건 없습니다. 그리고 광섬유 안에서 빛은 진공에서보다 느립니다 — 대략 초속 20만 km입니다.
서울에서 상파울루까지 직선으로 약 18,300km. 왕복이면 36,600km입니다.
36,600 km ÷ 200,000 km/s = 0.183초 = 183ms이게 이론상 최소값입니다. 케이블이 지구를 직선으로 뚫고 갈 수 없고 중간에 라우터를 여러 번 거치므로, 실제로는 왕복 280~320ms 정도 나옵니다.
| 서울에서 | 대략 거리 | 실제 왕복(RTT) |
|---|---|---|
| 서울 | 0 km | 5~20 ms |
| 프랑크푸르트 | 8,600 km | 230~260 ms |
| 상파울루 | 18,300 km | 280~320 ms |
여기서 중요한 건 왕복이 한 번으로 안 끝난다는 점입니다. TCP 연결에 한 번, TLS 협상에 또 한두 번, 그 다음에야 실제 요청이 갑니다. 왕복 300ms짜리 경로라면 화면에 첫 바이트가 뜨기까지 1초가 그냥 지나갑니다.
왕복 수가 어떻게 첫 응답 지연이 되는지는 HTTPS 핸드셰이크 글에 자세히 정리해 뒀습니다.
엣지 — 사본을 가까이 둔다
거리를 줄이는 방법은 하나뿐입니다. 가까운 곳에 사본을 두는 것.
CDN 사업자는 전 세계 수백 곳에 엣지 서버(edge server, PoP) 를 깔아 뒀습니다. 사용자의 요청은 원래 서버(오리진)까지 가지 않고 가장 가까운 엣지에서 끊깁니다.
| 엣지 없이 | 엣지 있을 때 | |
|---|---|---|
| 상파울루 사용자 → | 서울 오리진 (18,300 km) | 상파울루 엣지 (150 km) |
| 왕복 | 약 300 ms | 약 12 ms |
같은 파일입니다. 더 가까운 사본일 뿐입니다.
첫 요청은 안 빨라집니다
여기가 실무에서 가장 많이 오해하는 지점입니다.
엣지에 처음부터 파일이 들어 있는 게 아닙니다. 엣지는 빈 상태로 시작합니다. 누군가 처음 요청하면 엣지에도 그 파일이 없으니, 엣지가 오리진까지 가서 받아온 다음 사용자에게 넘겨줍니다.
1번째 요청 브라질 사용자 → 상파울루 엣지 → (없음) → 서울 오리진 → 엣지 → 사용자
= 원래보다 오히려 조금 더 걸립니다
2번째 요청 브라질 사용자 → 상파울루 엣지 → (있음) → 사용자
= 12ms이 상태를 응답 헤더가 알려줍니다.
| 헤더 값 | 의미 |
|---|---|
x-cache: MISS |
엣지에 없어서 오리진까지 다녀왔습니다 |
x-cache: HIT |
엣지가 갖고 있던 사본으로 바로 답했습니다 |
헤더 이름은 사업자마다 다릅니다. CloudFront는
x-cache: Hit from cloudfront, Cloudflare는cf-cache-status: HIT를 씁니다.
그래서 "CDN 붙였는데 안 빨라졌다"는 대부분 MISS를 보고 있는 것입니다. 배포 직후, 트래픽이 적은 지역, TTL이 막 만료된 시점 — 전부 MISS가 나는 조건입니다. 측정하실 거면 같은 URL을 두 번 이상 호출한 뒤에 비교하세요.
엣지는 무엇을 "같은 파일"로 보나
엣지가 "이건 갖고 있다"를 판단하는 기준을 캐시 키(cache key) 라고 합니다. 기본은 보통 이렇습니다.
캐시 키 = 호스트 + 경로 + 쿼리스트링그래서 실무에서 이런 일이 생깁니다.
/app.js와/app.js?v=2는 서로 다른 캐시입니다- 추적용 파라미터(
?utm_source=...)가 붙으면 매번 다른 키가 되어 캐시가 거의 안 먹습니다 - 모바일과 데스크톱에 다른 HTML을 주는데 캐시 키에 그 구분이 없으면, 먼저 캐시된 쪽이 양쪽에 나갑니다
마지막 사례가 특히 위험합니다. 로그인한 사용자의 페이지가 캐시돼서 다른 사람에게 나가는 사고가 이 지점에서 납니다. 사용자마다 달라지는 응답에는 캐시를 걸지 않거나, 구분되는 값을 캐시 키에 반드시 포함시켜야 합니다.
얼마나 오래 들고 있나 — TTL
엣지가 사본을 얼마나 보관할지는 오리진이 헤더로 지시합니다.
Cache-Control: public, max-age=31536000, immutable| 지시자 | 뜻 |
|---|---|
public |
공용 캐시(CDN)가 저장해도 됩니다 |
private |
브라우저만 저장하고, CDN은 저장하지 마세요 |
max-age=N |
N초 동안 신선한 것으로 취급합니다 |
no-store |
아무데도 저장하지 마세요 |
immutable |
만료 전엔 재검증도 하지 마세요 |
자산 종류에 따라 값이 갈립니다.
| 대상 | 권장 | 이유 |
|---|---|---|
| 파일명에 해시가 붙은 JS·CSS·이미지 | max-age=31536000, immutable |
내용이 바뀌면 파일명이 바뀌므로 영원히 캐시해도 안전 |
| HTML | 짧게, 또는 no-cache |
여기서 새 파일명을 가리켜야 하므로 최신이어야 함 |
| 로그인 사용자 응답 | private 또는 no-store |
남에게 나가면 사고 |
배포했는데 예전 게 나올 때
가장 흔한 사고입니다. 원인은 둘 중 하나입니다.
하나, 아직 TTL이 안 지났습니다. 엣지는 자기가 들고 있는 사본이 만료되기 전엔 오리진에 묻지 않습니다. 오리진을 아무리 바꿔도 엣지는 모릅니다.
둘, HTML까지 길게 캐시했습니다. JS 파일명은 바뀌었는데 그걸 가리키는 HTML이 옛날 것이면, 사용자는 계속 옛 파일을 부릅니다.
해결은 두 갈래입니다.
| 방법 | 하는 일 | 언제 |
|---|---|---|
| 캐시 버스팅 | 내용이 바뀌면 파일명도 바뀌게 함 (app.a3f9c1.js) |
평상시 정석 |
| 퍼지(무효화) | 엣지에 "그거 버려라"를 직접 지시 | 급할 때, 되돌릴 때 |
평소엔 퍼지에 의존하지 마세요. 반영에 시간이 걸리고, 사업자에 따라 건수 과금이 있으며, 무엇보다 전 세계 엣지가 동시에 비면 그 트래픽이 오리진으로 한꺼번에 몰립니다. 파일명에 해시를 붙이는 빌드 설정이 정답이고, 퍼지는 사고 대응용입니다.
동적인 것은 어떻게 하나
사용자마다 다른 응답은 캐시할 수 없습니다. 그러면 CDN이 쓸모없냐 하면, 그렇지 않습니다.
- 연결은 여전히 가까운 곳에서 끊깁니다. TCP·TLS 왕복을 엣지와 하고, 엣지↔오리진은 이미 열려 있는 연결을 재사용합니다. 캐시가 하나도 안 돼도 핸드셰이크 왕복만큼은 줄어듭니다
- 아주 짧은 TTL도 효과가 큽니다. 초당 1,000건 들어오는 API에
max-age=1만 걸어도 오리진이 받는 건 초당 1건입니다 - 정적과 동적을 경로로 나누세요.
/static/*는 길게,/api/*는 캐시 없이
정리 — 순서대로 보실 것
CDN이 기대만큼 안 빠르다면 이 순서로 확인하시면 됩니다.
x-cache헤더를 봅니다. MISS만 나온다면 캐시가 아예 안 걸린 것입니다Cache-Control을 봅니다. 오리진이no-store나private을 주고 있으면 엣지는 저장하지 않습니다- 캐시 키를 봅니다. 쿼리스트링이 매번 달라지면 HIT이 날 수 없습니다
- 두 번 이상 호출하고 비교합니다. 첫 호출은 원래 MISS입니다
자주 묻는 질문 (FAQ)
CDN을 쓰면 서버 비용이 줄어드나요?
대개 줄어듭니다. 엣지가 대신 답한 요청은 오리진에 도달하지 않으므로 오리진의 트래픽과 부하가 함께 내려갑니다. 다만 CDN 자체의 전송 요금이 붙으므로, 트래픽이 아주 적은 서비스에서는 총액이 오히려 늘 수도 있습니다.
이미지만 올려도 효과가 있나요?
있습니다. 페이지 용량의 대부분은 보통 이미지와 JS입니다. 다만 HTML이 느리면 이미지 요청 자체가 늦게 시작되므로, 이미지만 붙이고 HTML은 멀리 두면 체감은 기대보다 작습니다.
캐시 때문에 사고가 났다는 얘기를 들었습니다
로그인한 사용자의 응답이 캐시돼 다른 사람에게 나가는 사고입니다. 원인은 거의 항상 둘 중 하나입니다 — 개인화된 응답에 Cache-Control: public이 붙었거나, 사용자를 구분하는 값이 캐시 키에 없거나. 개인화 응답에는 private 또는 no-store를 명시하세요.
오리진이 죽으면 CDN이 대신 서비스해 주나요?
기본 동작은 아닙니다. TTL이 살아 있는 동안은 엣지가 계속 답하지만, 만료된 뒤엔 오리진에 물으러 갔다가 실패합니다. 다만 대부분의 CDN이 만료된 사본이라도 일단 내보내는 옵션(stale-if-error 계열)을 제공하므로, 장애 대비가 필요하면 이걸 켜 두시면 됩니다.
정리하며
CDN은 서버를 빠르게 만들지 않습니다. 거리를 줄일 뿐입니다.
- 지연의 바닥은 거리가 정합니다. 사양으로는 안 내려갑니다
- 엣지는 빈 상태로 시작합니다. 첫 요청은 원래 MISS입니다
- 무엇을 같은 파일로 볼지는 캐시 키가 정합니다. 쿼리스트링이 흔한 함정입니다
- 평상시 무효화는 퍼지가 아니라 파일명 해시로 합니다
- 개인화된 응답에
public을 붙이면 사고가 납니다
CDN을 붙이고 "생각보다 별로인데" 하셨던 적 있으신가요? x-cache 헤더를 보셨을 때 HIT이던가요, MISS던가요?