CDN
CDN(Content Delivery Network)은 전 세계 여러 위치에 분산된 엣지 서버를 통해 사용자에게 가장 가까운 서버에서 콘텐츠를 전달하는 네트워크입니다. 정적 파일, 이미지, 동영상을 캐싱해 응답 속도를 높이고 오리진 서버의 부하를 줄입니다.
핵심 개념
엣지 서버와 PoP
PoP(Point of Presence)는 CDN이 전 세계에 배치한 데이터 센터입니다. 각 PoP에는 엣지 서버가 있어 오리진 서버에서 콘텐츠를 캐싱합니다. 사용자는 DNS 응답이나 애니캐스트 라우팅으로 가까운 PoP에 연결됩니다.
핵심 개념
캐시 히트 / 미스
캐시 히트는 요청한 콘텐츠가 엣지 서버에 이미 있는 경우입니다. 응답이 빠르고 오리진 서버에 요청하지 않습니다. 캐시 미스는 콘텐츠가 없거나 만료된 경우로, 엣지 서버가 오리진에서 콘텐츠를 가져와 캐싱합니다. 히트율(Hit Rate)이 높을수록 CDN 효율이 좋습니다.
핵심 개념
Cache-Control 헤더
TTL(Time To Live)로 캐시 유효 기간을 설정합니다. max-age=31536000은 1년간 캐싱을 의미합니다. immutable은 콘텐츠가 변경되지 않음을 알려, 신선한 동안 브라우저가 재검증을 생략하게 합니다(RFC 8246). no-cache는 저장한 사본을 쓰기 전에 매번 재검증을 거치게 합니다. 정적 파일은 긴 TTL, HTML은 짧은 TTL을 권장합니다.
핵심 개념
캐시 무효화
콘텐츠 변경 시 기존 캐시를 무효화합니다. URL에 해시를 포함하는 캐시 버스팅(app.a1b2c3.js)이 가장 효율적입니다. CDN API로 특정 파일이나 경로를 강제 퍼지(purge)할 수 있습니다. Cloudflare는 캐시 퍼지 API를 제공하므로 배포 과정에서 호출할 수 있습니다.
핵심 개념
동적 콘텐츠 가속
CDN은 정적 파일만 캐싱하지 않습니다. TCP 연결 재사용, TLS 종료 최적화, HTTP/2·3 지원으로 동적 API 응답도 가속합니다. Cloudflare는 Argo Smart Routing 같은 기능으로 경로를 골라 동적 요청의 지연을 줄이도록 안내합니다. 오리진 서버와 CDN 간 연결을 상시 유지합니다.
핵심 개념
보안 기능
CDN은 DDoS 방어, WAF(Web Application Firewall), Bot 관리를 제공합니다. 오리진 IP를 감추면 공격자가 오리진에 직접 접근하기 어려워지지만, IP가 다른 경로로 알려지면 효과가 없습니다. 많은 CDN이 SSL/TLS 인증서의 발급과 갱신도 대신해 줍니다.
거리는 줄일 수 없지만 콘텐츠는 옮길 수 있다
신호가 먼 거리를 오가는 데는 물리적 시간이 듭니다. 사용자와 오리진 서버 사이를 줄일 수 없다면, 콘텐츠의 사본을 사용자 가까이에 두면 됩니다. CDN은 이렇게 여러 지역에 엣지 서버를 두고 응답을 캐시해서 지연을 줄이고 오리진의 부하를 덜어 주는 서비스입니다. 캐시가 언제까지 유효하고 어떻게 재검증하는지는 HTTP 캐시 표준인 RFC 9111이 정합니다.
숫자로 확인하기
max-age=31536000은 60 × 60 × 24 × 365 = 31,536,000초, 곧 365일입니다.
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
CDN은 사용자 요청을 어떻게 처리하나요?
사용자의 요청이 DNS 응답이나 애니캐스트 라우팅으로 가까운 엣지에 닿고, 엣지에 유효한 사본이 있으면(히트) 바로 응답합니다. 없거나 만료되었으면(미스) 오리진에서 가져와 응답하고 저장합니다.
면접 질문
배포한 정적 파일이 사용자에게 바로 반영되게 하려면 어떻게 하나요?
파일 이름이나 URL에 내용 해시를 넣어 내용이 바뀌면 URL도 바뀌게 하고 긴 TTL을 씁니다. HTML처럼 이름이 고정된 파일은 짧은 TTL이나 재검증을 쓰거나 CDN의 퍼지 기능을 호출합니다.
면접 질문
Cache-Control의 public, private, no-cache, no-store는 어떻게 다른가요?
public과 private은 공유 캐시(CDN)에 저장해도 되는지의 범위를 정합니다. no-cache는 저장하되 재검증 후 사용하고, no-store는 저장하지 않습니다.
면접 질문
stale-while-revalidate는 무엇인가요?
만료된 사본을 잠시 그대로 응답하면서 뒤에서 새 사본을 받아 오는 방식입니다. 사용자는 대기 없이 응답을 받고 다음 요청부터 최신 사본을 받습니다. CDN과 브라우저마다 지원이 다릅니다.
실제 시스템에서는
CF-Cache-Status로 HIT, MISS 같은 캐시 상태를 알려 준다고 설명합니다. curl -I 주소로 확인할 수 있습니다.
Cache-Control은 spring.web.resources.cache.cachecontrol.max-age 같은 속성으로 지정할 수 있습니다. CDN이 이 헤더를 따를지는 CDN의 캐시 규칙에 달려 있습니다.
함께 보면 좋은 토픽
참고 자료
Java — Spring Boot Cache-Control 헤더 설정
import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.*; /* ── 정적 리소스 Cache-Control 설정 ─────────────────────── */ @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // JS/CSS: 콘텐츠 해시 파일명 사용 → 1년 캐싱 + immutable registry.addResourceHandler("/js/**", "/css/**") .addResourceLocations("classpath:/static/js/", "classpath:/static/css/") .setCacheControl(CacheControl .maxAge(365, TimeUnit.DAYS) .cachePublic() .immutable()); // 이미지: 1시간 캐싱 registry.addResourceHandler("/images/**") .addResourceLocations("classpath:/static/images/") .setCacheControl(CacheControl .maxAge(1, TimeUnit.HOURS) .cachePublic()); } } /* ── HTML 응답: 캐싱 방지 ────────────────────────────────── */ @Controller public class TopicController { @GetMapping("/{subject}/{topic}") public ResponseEntity<String> topic( @PathVariable String subject, @PathVariable String topic, Model model) { // HTML은 캐시 금지 — 내용이 바뀔 수 있음 HttpHeaders headers = new HttpHeaders(); headers.setCacheControl( CacheControl.noStore().getHeaderValue() ); return ResponseEntity .ok() .headers(headers) .body("topics/" + subject + "/" + topic); } } /* ── Cloudflare 캐시 퍼지 (배포 시 자동 실행) ──────────── */ public class CloudflarePurgeService { private final String zoneId = System.getenv("CF_ZONE_ID"); private final String apiKey = System.getenv("CF_API_KEY"); public void purgeAll() { var client = HttpClient.newHttpClient(); var request = HttpRequest.newBuilder() .uri(URI.create( "https://api.cloudflare.com/client/v4/zones/" + zoneId + "/purge_cache")) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"purge_everything\": true}")) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenAccept(res -> log.info("CF purge: {}", res.body())); } }
정적 리소스(JS, CSS)는 콘텐츠 해시가 파일명에 포함되므로 파일이 변경되면 URL 자체가 바뀝니다.
따라서 max-age=1년 + immutable로 설정해도 안전합니다. Spring은 ResourceHandlerRegistry에서 이를 자동으로 처리합니다.
HTML은 항상 최신 버전을 보여야 하므로 no-store로 캐싱을 금지합니다. Cloudflare는 이 헤더를 존중해 HTML을 캐싱하지 않습니다.
배포 시 Cloudflare API로 캐시를 퍼지합니다. API 키는 반드시 환경 변수에서 읽어야 합니다.
GitHub Actions에서 배포 완료 후 purge_cache API를 호출하는 스텝을 추가하면 됩니다.
nginx — CDN 오리진 서버 설정
# nginx.conf — Cloudflare가 앞단에서 CDN 역할 server { listen 80; server_name csflow.kr; # Cloudflare Real IP 복원 real_ip_header CF-Connecting-IP; # HTML — CDN 캐싱 금지 location / { proxy_pass http://csflow_backend; add_header Cache-Control "no-store"; add_header Vary "Accept-Encoding"; } # 정적 리소스 — CDN이 1년 캐싱 location ~* \.(css|js|png|jpg|svg|ico|woff|woff2)$ { proxy_pass http://csflow_backend; proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Connection ""; # Cloudflare가 이 헤더를 보고 엣지에 캐싱 expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; } # API — 캐싱 금지 location /api/ { proxy_pass http://csflow_backend; add_header Cache-Control "no-store, no-cache"; add_header Pragma "no-cache"; } # Actuator — 외부 차단 location /actuator { deny all; return 403; } }
Cloudflare가 앞단에서 CDN 역할을 할 때 nginx는 오리진 서버로 동작합니다. Cache-Control 헤더를 통해 Cloudflare가 어떤 콘텐츠를 캐싱할지 결정합니다.
정적 리소스에 max-age=31536000 immutable을 설정하면 Cloudflare 엣지 서버가 1년간 캐싱합니다. 이후 해당 파일 요청은 오리진까지 오지 않습니다.
API 응답은 no-store로 캐싱을 막습니다. 캐시된 API 응답이 다른 사용자에게 전달되면 보안 문제가 발생할 수 있습니다.
Q1
CDN(Content Delivery Network)의 기본 원리는?
Q2
CDN(Content Delivery Network)의 기본 원리는?
Q3
CDN에서 캐시 미스(cache miss)가 발생하면?
Q4
CDN에서 캐시 미스(cache miss)가 발생하면?
Q5
CDN이 원본 서버의 부하를 줄여주는 이유는?
Q6
CDN이 원본 서버의 부하를 줄여주는 이유는?