자체 CDN 서버 구축하기: 한 번 가져와 수천 번 내보내기
CDN 서버는 파일마다 원본에서 한 번만 가져오고, 그다음부터는 수천 번이든 직접 내보냅니다. 원본 서버는 요청의 일부만 보게 되고, 방문자는 짧은 로딩 시간을 얻습니다. 직접 구축하는 방법을 정리했습니다.
방문자가 사이트에서 파일을 하나 받을 때마다 원본 서버는 그 파일을 파일 시스템에서 꺼내고, 응답에 담아 내보냅니다. 하루 방문자가 열 명이면 아무도 눈치채지 못합니다. 그러나 동시 방문자가 만 명이면, 정작 애플리케이션은 할 일이 없는데도 사이트가 느려지는 이유가 바로 이것입니다.
CDN 서버는 그 앞에 자리를 잡습니다. 파일마다 원본에서 한 번만 가져오고, 그다음부터는 필요한 만큼 몇 번이든 직접 내보냅니다. 그래서 원본은 요청의 일부만 보게 되고, 방문자는 내보내는 일만 하는 서버에서 파일을 받습니다. 이 글에서는 그 효과가 실제로 얼마나 되는지, 그런 서버를 nginx로 어떻게 구축하는지, 트래픽과 위치에서 무엇이 중요한지 살펴봅니다.
CDN 서버가 실제로 하는 일
동작은 세 문장으로 설명됩니다.
- 방문자가 CDN 서버에 파일을 요청합니다.
- 그 파일이 이미 있으면 바로 나갑니다. 이것이 적중입니다.
- 없으면 CDN 서버가 원본에서 가져와 저장하고 내보냅니다. 이것이 실패이며, 파일마다 유효 기간에 한 번만 일어납니다.
원리는 스트림 서버와 같습니다. 한 번 가져와 여러 번 내보내는 것입니다. 다른 점은 스트림 서버가 흐르는 데이터를 배포하고, CDN 서버는 완성된 파일을 배포한다는 것뿐입니다.
계산: 캐시가 부담을 얼마나 덜어 주는지
결정적인 것은 적중률, 곧 캐시가 직접 응답하는 요청의 비율입니다. 하루 요청이 100만 건일 때 부담은 이렇게 줄어듭니다.
| 적중률 | 원본에 도달하는 요청 | 부담 경감 |
|---|---|---|
| 50% | 500,000 | 2배 |
| 80% | 200,000 | 5배 |
| 95% | 50,000 | 20배 |
| 99% | 10,000 | 100배 |
정적인 파일에서는 95퍼센트 이상이 보통입니다. 이미지와 스크립트, 글꼴은 좀처럼 바뀌지 않기 때문입니다. 95퍼센트에서 99퍼센트로 올라가는 구간은 숫자만 보고 짐작하는 것보다 값집니다. 남아 있는 부하를 다시 5분의 1로 줄여 주기 때문입니다.
그와 동시에 트래픽의 위치가 옮겨 갑니다. 원본은 거의 전송하지 않게 되고, 그만큼 CDN 서버가 많이 전송합니다. 하루에 이미지 요청 500,000건에 건당 400 KB라면 하루 200 GB, 곧 한 달에 약 6 TB이고, 이것이 이미지만으로 생기는 양입니다. 그래서 CDN 노드에서 중요한 숫자는 CPU가 아니라 회선과 전송량입니다.
자체 CDN 서버가 값을 하는 경우
상업용 CDN 사업자는 전송한 기가바이트마다 요금을 매깁니다. 편리하고, 전송량이 적을 때는 저렴합니다. 일정한 양을 넘어서면 관계가 뒤집힙니다.
| 경우 | 자체 CDN 서버 | 기가바이트당 과금 |
|---|---|---|
| 한 달에 몇 GB | 값을 하기 어렵다 | 더 저렴하다 |
| 한 달에 수 TB | 고정 월 요금 | 양에 비례해 늘어난다 |
| 큰 다운로드, 영상 보관함 | 분명히 유리하다 | 금방 비싸진다 |
| 규칙과 로그에 대한 완전한 통제 | 완전하다 | 제한된다 |
여기에 숫자로 적기 어려운 점이 하나 더 있습니다. 자체 노드에서는 무엇을 얼마나 오래 캐시하고 언제 없앨지 직접 정합니다. 외부 사업자가 정한 규칙에 묶이지 않고, 남이 만든 틀에 맞지 않는 것도 내보낼 수 있습니다.
nginx를 캐시로 두는 구성
설치하기
apt update
apt install -y nginx
이것으로 끝입니다. 캐시는 nginx에 들어 있습니다. 파일이 놓일 디렉터리를 만드세요.
mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn
설정하기
캐시는 http 블록에서 한 번 정의하고, 그다음 개별 location에서 사용합니다.
proxy_cache_path /var/cache/cdn
levels=1:2
keys_zone=cdn:100m
max_size=200g
inactive=30d
use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
값의 의미는 각각 이렇습니다. keys_zone=cdn:100m은 약 800,000개 항목에 충분하고, max_size는 디스크에서 쓸 공간을 제한하며, inactive=30d는 30일 동안 아무도 요청하지 않은 파일을 버리고, use_temp_path=off는 디렉터리 사이의 불필요한 이동을 막습니다.
그 아래에 실제 서버를 둡니다.
server {
listen 443 ssl;
http2 on;
server_name cdn.example.com;
ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;
location / {
proxy_pass https://origin.example.com;
proxy_set_header Host origin.example.com;
proxy_http_version 1.1;
proxy_cache cdn;
proxy_cache_valid 200 301 302 24h;
proxy_cache_valid 404 1m;
# 동시에 생긴 여러 실패를 요청 하나로 묶는다
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
# 원본에 문제가 있을 때도 계속 내보낸다
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# 세션 쿠키가 캐시를 망가뜨리지 않게 한다
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
add_header X-Cache-Status $upstream_cache_status always;
}
}
그중 세 줄은 특히 눈여겨볼 만합니다. proxy_cache_lock은 새 파일이 들어올 때 방문자 백 명이 동시에 원본에 요청하는 대신, 한 명만 요청하고 나머지는 기다리게 합니다. proxy_cache_use_stale은 원본이 다시 시작하거나 느리게 답하는 동안에도 내보내기를 유지합니다. 그리고 Set-Cookie를 숨기면, 세션 쿠키가 붙은 응답 하나가 캐시 전체를 쓸모없게 만드는 일을 막습니다.
동작하는지 확인하기
nginx -t && systemctl reload nginx
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
첫 번째 호출에서는 MISS, 두 번째에서는 HIT가 나와야 합니다. 두 번째에도 MISS라면 원인은 거의 늘 흔한 세 가지 중 하나입니다. 응답에 들어 있는 Set-Cookie, 원본이 보낸 Cache-Control: no-store, 또는 빠진 길이 정보입니다.
무엇을 캐시하고 무엇을 캐시하지 않는지
깔끔한 해법은 원본이 직접 알려 주는 것입니다. 자기 콘텐츠를 아는 쪽은 원본이고 캐시가 아닙니다. 그러려면 원본이 알맞은 헤더를 붙입니다.
| 콘텐츠 | 원본이 보낼 헤더 |
|---|---|
| 이름에 체크섬이 들어 있는 파일 | Cache-Control: public, max-age=31536000, immutable |
| 버전이 붙지 않은 이미지나 다운로드 | Cache-Control: public, max-age=86400 |
| 공개 페이지의 HTML | Cache-Control: public, max-age=300 |
| 로그인 영역, 장바구니 | Cache-Control: private, no-store |
캐시에서 직접 정하고 싶다면 파일 종류별로 location을 따로 두면 됩니다.
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2|mp4|zip)$ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800";
add_header X-Cache-Status $upstream_cache_status always;
}
콘텐츠를 골라서 없애기
가장 간단한 길은 지울 필요를 아예 만들지 않는 것입니다. 바뀐 이미지마다 새 파일 이름을 주면, 예를 들어 logo.a1b2c3.png처럼 하면 이 문제가 없습니다. 오래된 파일은 언젠가 저절로 캐시에서 빠집니다.
그럴 수 없다면 nginx에서는 무료 모듈 ngx_cache_purge가 도움이 됩니다. 추가 모듈 없이도 경로를 캐시 키에서 계산할 수 있습니다. nginx가 그 키의 MD5 값을 파일 이름으로 쓰기 때문입니다.
KEY="httpsGETcdn.example.com/bild.jpg"
H=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
rm -f "/var/cache/cdn/${H: -1}/${H: -3:2}/$H"
평소 운영에는 이 줄들을 경로 목록에 대해 실행하는 작은 스크립트 하나로 대개 충분합니다.
큰 파일과 영상
몇백 메가바이트가 넘는 파일에는 블록 단위 분할이 필요합니다. 그러지 않으면 영상에서 한 번 건너뛸 때마다 캐시가 파일 전체를 다시 받아 옵니다.
location /video/ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 206 7d;
slice 1m;
proxy_set_header Range $slice_range;
proxy_cache_key "$uri$slice_range";
}
이렇게 하면 nginx는 실제로 요청된 블록만 가져옵니다. 시청자가 영상 중간으로 건너뛰어도 기가바이트 단위가 아니라 몇 메가바이트만 회선을 지나갑니다.
TLS, HTTP/2, HTTP/3
CDN 노드는 작은 파일을 많은 연결에 내보내므로, 새 프로토콜이 값을 하는 곳이 바로 여기입니다. HTTP/2는 http2 on;으로 끝납니다. HTTP/3은 특히 이동통신 연결에서 도움이 됩니다. 네트워크가 바뀌어도 연결이 끊기지 않기 때문입니다.
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
이때 방화벽에서 UDP 443을 여는 것을 잊지 마세요. HTTP/3을 설정해 두었는데도 아무도 쓰지 않는 가장 흔한 이유가 이것입니다.
여러 위치
노드 하나만으로도 이득의 대부분, 곧 원본의 부담 경감은 이미 얻습니다. 두 번째 이득인 방문자까지의 짧은 경로는 위치가 여러 곳이 되어야 생깁니다.
분산은 DNS로 합니다. 같은 이름이 방문자의 위치에 따라 다른 노드를 가리킵니다. 그러려면 지리 기반 할당을 지원하는 DNS 사업자가 필요합니다. 모든 노드는 같은 설정과 같은 원본을 쓰고 서로를 알지 못하는데, 바로 그 점이 이 구성을 튼튼하게 만듭니다. 노드 하나가 멈추면 DNS에서 빼면 되고, 나머지가 그 몫을 받습니다.
KernelHost는 독일, 미국 서부, 미국 동부, 싱가포르, 한국에 위치를 제공합니다. 지연 시간이 함께 정리된 목록은 서버 위치에서 볼 수 있습니다. 기준을 하나 들자면, 방문자의 상당수가 있는 대륙마다 노드 하나입니다.
짐작하지 말고 측정하기
캐시 상태를 로그에 함께 쓰면 적중률을 언제든지 세어 볼 수 있습니다.
log_format cdn '$remote_addr $status $body_bytes_sent '
'$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;
집계는 한 줄로 끝납니다.
awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn
정적 파일에서 HIT의 비율이 90퍼센트 아래라면 원인을 찾아보는 것이 거의 늘 값을 합니다. 대개는 응답에 들어 있는 쿠키, 너무 짧은 유효 기간, 또는 주소 끝에 매번 달라지는 매개변수가 붙은 요청입니다. 그런 요청은 캐시 쪽에서는 매번 새 파일처럼 보입니다.
어떤 서버가 맞는지
CDN 노드는 내보내는 기계입니다. 그래서 중요한 조건의 순서가 애플리케이션 서버와 다릅니다.
- 트래픽과 회선. 부하가 전부 여기서 생깁니다. 노드 하나는 운영 중에 한 달 수 테라바이트를 어렵지 않게 전송합니다.
- 디스크. 캐시는 작고 무작위한 접근이 많아 NVMe가 분명히 유리합니다. 용량은 보유한 파일 전체 크기가 아니라, 그중 자주 요청되는 부분이 얼마나 되는지에 맞춰 정합니다.
- 메모리. 운영체제가 남은 메모리를 디스크 앞의 두 번째 캐시로 씁니다. 여기서는 메모리가 많을수록 바로 속도가 됩니다.
- CPU. 가장 덜 중요합니다. TLS에만 쓰이고, 그 일은 최신 프로세서가 한가하게 처리합니다.
바로 이런 경우에 맞춰 만든 것이 KernelHost의 무제한 트래픽 루트 서버입니다. 서버마다 속도 조절 없는 진짜 2 × 1 Gbit/s 회선, 고정 월 요금의 무제한 전송량, RAID로 묶은 데이터센터급 NVMe, 그리고 기본으로 제공되는 3.2 Tbps 플러드 방어가 들어 있습니다. 준비는 30초 안에 끝나며, 전체 루트 권한과 IPv4 주소, /64 IPv6 네트워크가 함께 제공됩니다. 요금이 PrePaid 방식으로 청구되고 월 단위로 해지할 수 있으니, 노드 하나로 시작해 방문자가 흩어지면 언제든 두 번째 위치를 더하면 됩니다.
자주 묻는 질문
CDN 서버는 정확히 무슨 일을 하나요?
실제로 부담이 얼마나 줄어드나요?
상업용 CDN 사업자가 꼭 필요한가요?
어떤 파일을 캐시하고 어떤 파일을 캐시하지 말아야 하나요?
캐시는 파일을 얼마나 오래 보관해야 하나요?
캐시에서 파일을 어떻게 지우나요?
원본 서버가 멈추면 어떻게 되나요?
큰 영상 파일에도 통하나요?
위치는 몇 곳이 필요한가요?
캐시가 동작하는지 어떻게 알 수 있나요?
2026 KernelHost GmbH. 모든 권리를 보유합니다. 본 가이드는 저작권법의 보호를 받습니다. 전체든 일부든, 또는 수정된 형태이든 저희의 서면 동의 없이 다른 웹사이트에 게시할 수 없습니다. 출처를 밝히고 링크를 덧붙인 인용은 언제든지 환영합니다.

