컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 10

CDN

네트워크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 인증서의 발급과 갱신도 대신해 줍니다.

Anycast 라우팅 — CDN은 동일한 IP 주소를 여러 PoP가 공유합니다. 사용자 요청은 BGP 경로 기준으로 가까운 PoP로 전달됩니다. 여기서 가깝다는 것은 네트워크 경로 기준이라 지리적으로 가장 가까운 곳과 다를 수 있습니다.
CDN과 Spring Boot — Cloudflare를 오리진 앞에 두면 정적 리소스는 엣지에서 캐싱하고, API 요청은 오리진으로 전달합니다. Spring이 보내는 Cache-Control 헤더는 CDN이 참고하는 신호지만, 실제 캐싱은 CDN의 캐시 규칙에도 좌우됩니다. csflow.kr이 이 구조로 동작하고 있습니다.
Stale-While-Revalidate — 캐시가 만료되면 즉시 삭제하는 대신, 만료된 콘텐츠를 일시적으로 제공하면서 백그라운드에서 새 콘텐츠를 가져옵니다. 사용자는 캐시 미스 없이 응답을 받고, 잠시 후 최신 콘텐츠로 교체됩니다. 응답 속도를 우선하면서 최신성도 곧 따라오게 합니다(RFC 5861, 지원 여부는 CDN마다 다릅니다).

거리는 줄일 수 없지만 콘텐츠는 옮길 수 있다

신호가 먼 거리를 오가는 데는 물리적 시간이 듭니다. 사용자와 오리진 서버 사이를 줄일 수 없다면, 콘텐츠의 사본을 사용자 가까이에 두면 됩니다. CDN은 이렇게 여러 지역에 엣지 서버를 두고 응답을 캐시해서 지연을 줄이고 오리진의 부하를 덜어 주는 서비스입니다. 캐시가 언제까지 유효하고 어떻게 재검증하는지는 HTTP 캐시 표준인 RFC 9111이 정합니다.

숫자로 확인하기

거리가 만드는 지연(가정) — 광섬유 안에서 빛이 진공의 약 3분의 2 속도, 곧 초당 약 20만km로 간다고 보면, 서울에서 약 9,000km 떨어진 서버까지 편도 약 45ms, 왕복 약 90ms가 이론상 하한입니다. 실제 경로는 직선이 아니고 장비를 거치므로 더 큽니다.
왕복이 쌓이는 비용 — TCP 연결 1 RTT, TLS 1.3 핸드셰이크 1 RTT, 요청과 응답 1 RTT를 합쳐 3 RTT가 필요합니다. RTT가 90ms이면 270ms, 가까운 엣지의 RTT가 20ms이면 60ms입니다.
히트율이 오리진에 주는 효과 — 요청이 1,000,000건일 때 히트율 95%이면 오리진에 닿는 요청은 50,000건, 99%이면 10,000건입니다. 히트율이 4%포인트만 올라도 오리진 부하는 5분의 1이 됩니다.
max-age 환산 — max-age=31536000은 60 × 60 × 24 × 365 = 31,536,000초, 곧 365일입니다.

자주 하는 오해

CDN은 정적 파일만 빠르게 한다 — 캐시할 수 없는 동적 응답도 엣지에서 TLS를 끝내고 오리진과 연결을 유지해 주면 연결 설정 지연을 줄일 수 있습니다.
no-cache는 캐시하지 말라는 뜻이다 — no-cache는 저장은 하되 쓰기 전에 재검증하라는 뜻입니다. 저장 자체를 막는 것은 no-store입니다.
CDN에서 퍼지하면 모든 사본이 사라진다 — 퍼지는 CDN의 캐시를 지울 뿐 사용자의 브라우저 캐시에 있는 사본은 그대로입니다. 그래서 파일 이름에 해시를 넣는 방식을 함께 씁니다.
CDN을 쓰면 항상 빨라진다 — 캐시 미스이면 엣지를 거쳐 오리진까지 가야 해서 오히려 한 단계가 더 있는 셈입니다. 히트율이 낮은 콘텐츠에서는 이득이 작습니다.

면접에서 이렇게 물어요

면접 질문

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과 브라우저마다 지원이 다릅니다.

실제 시스템에서는

캐시 상태 확인 — Cloudflare 문서는 응답 헤더 CF-Cache-Status로 HIT, MISS 같은 캐시 상태를 알려 준다고 설명합니다. curl -I 주소로 확인할 수 있습니다.
Spring Boot의 캐시 헤더 — 정적 리소스의 Cache-Control은 spring.web.resources.cache.cachecontrol.max-age 같은 속성으로 지정할 수 있습니다. CDN이 이 헤더를 따를지는 CDN의 캐시 규칙에 달려 있습니다.
CDN 기본 동작 — Cloudflare는 기본으로 파일 확장자를 기준으로 캐시할 콘텐츠를 정하고, HTML 같은 콘텐츠는 규칙을 만들어야 캐시한다고 문서가 설명합니다.

함께 보면 좋은 토픽

DNS — DNS로 가까운 엣지를 고르는 방식을 봅니다.
HTTP — 캐시와 재검증을 정하는 헤더를 봅니다.
HTTPS / TLS — 엣지에서 TLS를 끝내는 구조를 봅니다.
라우팅 — 애니캐스트가 의존하는 BGP를 봅니다.
로드 밸런서 — 오리진 앞에서 요청을 나누는 방식을 비교합니다.

참고 자료

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)의 기본 원리는?

사용자와 가까운 곳에 사본을 두면 더 빠릅니다.
CDN은 전 세계 여러 지역에 엣지 서버를 두고 콘텐츠를 캐싱해, 사용자가 물리적으로 가까운 엣지 서버에서 응답을 받아 지연 시간을 줄입니다.

Q2

CDN(Content Delivery Network)의 기본 원리는?

사용자와 가까운 곳에 사본을 두면 더 빠릅니다.
CDN은 전 세계 여러 지역에 엣지 서버를 두고 콘텐츠를 캐싱해, 사용자가 물리적으로 가까운 엣지 서버에서 응답을 받아 지연 시간을 줄입니다.

Q3

CDN에서 캐시 미스(cache miss)가 발생하면?

엣지에 없으면 원본까지 가야 합니다.
캐시 미스 시 엣지 서버는 원본 서버로 요청을 전달해 콘텐츠를 가져오고, 이후 요청을 위해 캐싱한 뒤 사용자에게 응답합니다.

Q4

CDN에서 캐시 미스(cache miss)가 발생하면?

엣지에 없으면 원본까지 가야 합니다.
캐시 미스 시 엣지 서버는 원본 서버로 요청을 전달해 콘텐츠를 가져오고, 이후 요청을 위해 캐싱한 뒤 사용자에게 응답합니다.

Q5

CDN이 원본 서버의 부하를 줄여주는 이유는?

캐시 히트가 많을수록 원본 서버는 한가해집니다.
정적 콘텐츠 등 반복 요청이 많은 경우 엣지 서버의 캐시 히트로 대부분 처리되어, 원본 서버까지 가는 요청 수가 크게 줄어듭니다.

Q6

CDN이 원본 서버의 부하를 줄여주는 이유는?

캐시 히트가 많을수록 원본 서버는 한가해집니다.
정적 콘텐츠 등 반복 요청이 많은 경우 엣지 서버의 캐시 히트로 대부분 처리되어, 원본 서버까지 가는 요청 수가 크게 줄어듭니다.