개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 네트워크 › 웹 서버 앞의 장비

CDN

원본을 바꿨는데 왜 사용자는 아직 옛 파일을 받나

CDN은 원본 서버(오리진)가 가진 파일의 사본을 사용자와 가까운 서버(엣지)에 보관해 두고, 엣지가 대신 답하게 하는 방식입니다. 처음 요청은 엣지에 사본이 없어 오리진까지 다녀오지만(MISS), 그 뒤로는 엣지가 바로 답합니다(HIT). 대신 엣지는 정해진 유효 시간 동안 오리진에 다시 묻지 않으므로, 그사이 오리진의 파일을 바꿔도 사용자는 옛 사본을 받습니다.

일부만 실행으로 확인 · 오리진과 직접 만든 캐싱 서버(엣지 역할)를 127.0.0.1에 띄워 열여섯 가지 조합을 실행해 대조. 실제 CDN 서비스로는 확인하지 않음

이 실습에 나오는 개념

CDN
전 세계 여러 지역에 배치된 서버에 콘텐츠의 복사본을 두고, 사용자와 가까운 서버가 응답하게 하는 네트워크다. 이미지, 영상, 스크립트 같은 정적 파일의 전송이 빨라지고 원본 서버로 가는 요청이 줄어든다.
오리진과 엣지
오리진은 파일의 원본을 가진 서버이고, 엣지는 사용자와 가까운 곳에 놓여 그 사본을 보관했다가 대신 답하는 CDN의 서버다. 사용자의 요청은 먼저 엣지에 도착하며, 엣지에 쓸 수 있는 사본이 있으면 바로 답하고(HIT) 없으면 오리진에서 받아 와 전달하면서 보관한다(MISS). 엣지는 오리진이 응답 헤더로 정해 준 유효 시간 동안에는 오리진에 다시 묻지 않는다. 그래서 응답이 빨라지고 오리진의 부하가 줄지만, 그동안 오리진에서 바뀐 내용은 엣지에 반영되지 않는다.
캐시 키
캐시가 보관한 사본을 찾을 때 쓰는 이름표다. HTTP 캐시는 기본적으로 요청 메서드와 주소 전체(호스트, 경로, 쿼리 문자열)를 캐시 키로 쓴다. CDN은 제품과 설정에 따라 캐시 키에 넣는 값이 다르며, 쿼리 문자열을 기본으로는 넣지 않는 CDN도 있다(CloudFront의 기본 캐시 키는 도메인 이름과 경로다). 쿼리 문자열이 캐시 키에 들어가면 같은 파일이라도 쿼리 문자열이 한 글자만 달라도 다른 사본으로 취급되어 원본 서버에 다시 다녀온다. 이 성질을 거꾸로 이용해 파일이 바뀔 때마다 주소의 버전 값을 바꾸면 옛 사본을 지우지 않고도 새 내용을 내보낼 수 있다. 내용과 무관한 쿼리 문자열 때문에 사본이 잘게 나뉘지 않도록, CDN에는 캐시 키에 넣을 값과 뺄 값을 정하는 설정이 있다.
캐시 무효화(Purge)
유효 시간이 아직 남은 사본을 캐시에서 강제로 지우는 일이다. 원본을 바꿨는데 CDN의 엣지가 옛 사본을 계속 내보낼 때, 해당 주소의 무효화를 요청하면 다음 요청부터 원본에서 새로 받아 온다. HTTP 표준에 정해진 기능이 아니라 CDN이나 프록시 제품이 저마다 제공하는 기능이며, 주소 하나 단위나 경로 묶음 단위로 지운다. 엣지의 사본만 지울 뿐 사용자의 브라우저가 따로 보관한 사본은 지우지 못한다. 그래서 자주 바뀌는 파일은 무효화에 기대기보다 주소에 버전이나 해시를 넣는 방식을 함께 쓴다.
TTL(Time To Live)
데이터가 유효한 시간이다. 캐시에 값을 넣을 때 TTL을 지정하면 그 시간이 지난 뒤 자동으로 삭제된다. 원본과 어긋난 값이 캐시에 무기한 남는 것을 막는 기본적인 안전장치다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 0초: 사용자가 GET /app.css?v=1 요청을 보냅니다. 요청은 오리진이 아니라 사용자와 가까운 엣지 서버에 도착합니다. 엣지에는 이 주소의 사본이 없습니다(MISS). 엣지가 멀리 있는 오리진에서 파일을 받아 와 사용자에게 주고, 사본을 보관합니다. 오리진이 응답에 적은 s-maxage=300에 따라 이 사본은 300초 동안 그대로 써도 됩니다.
  2. 10초: 다른 사용자가 같은 주소를 요청합니다. 엣지에 사본이 있고 받아 온 지 10초라 유효 시간 안입니다(HIT). 엣지가 오리진에 묻지 않고 바로 답합니다. 오리진은 이 요청이 있었는지도 모릅니다. 여러 사용자가 한 사본을 함께 쓰기 때문에 공유 캐시라고 부릅니다.
  3. 30초: 개발자가 오리진의 app.css를 새 내용으로 바꿔 배포합니다. 오리진은 이 사실을 엣지에 알리지 않습니다. 엣지가 보관한 사본은 옛 내용 그대로입니다.
  4. 40초: 사용자가 /app.css?v=1 주소를 요청합니다. 엣지의 사본은 받아 온 지 40초라 유효 시간(300초) 안입니다(HIT). 엣지가 보관한 사본을 그대로 줍니다. 오리진의 파일은 10초 전에 바뀌었지만 엣지는 알지 못합니다. 사용자는 옛 내용을 받습니다.
  5. 80초: 사용자가 /app.css?v=1 주소를 요청합니다. 사본은 받아 온 지 80초라 유효 시간(300초) 안입니다(HIT). 엣지가 바로 답합니다. 오리진의 파일이 바뀐 지 50초가 지났는데도 사용자는 여전히 옛 내용을 받습니다. 유효 시간이 끝나는 300초까지 계속 이렇게 됩니다.

실행 결과

코드

# 오리진이 app.css를 줄 때 붙이는 응답 헤더
Cache-Control: public, s-maxage=300     # 공유 캐시(엣지)는 300초 동안 사본을 그대로 써도 된다

// 엣지: 사본을 보관했다가 대신 답하는 서버 (Node.js로 줄여 쓴 모습)
const store = new Map();                              // 주소(경로 + 쿼리 문자열) → 보관한 사본
http.createServer(async (req, res) => {
    if (req.method === 'PURGE') {                     // 무효화: 그 주소의 사본을 지운다
        store.delete(req.url);
        return res.end('purged');
    }
    const copy = store.get(req.url);
    if (copy && age(copy) < copy.ttl) {               // 사본이 있고 유효 시간 안
        res.setHeader('X-Cache', 'HIT');
        return res.end(copy.body);                    // 오리진에 묻지 않고 바로 답한다
    }
    const fresh = await fetchFromOrigin(req.url);     // 없거나 유효 시간이 지났으면 오리진으로
    store.set(req.url, fresh);
    res.setHeader('X-Cache', copy ? 'EXPIRED' : 'MISS');
    res.end(fresh.body);
});

// 요청 순서
//  0초  curl https://cdn.shop.example/app.css?v=1
// 10초  curl https://cdn.shop.example/app.css?v=1
// 30초  오리진의 app.css를 새 내용으로 배포
// 40초  curl https://cdn.shop.example/app.css?v=1
// 80초  curl https://cdn.shop.example/app.css?v=1

이 모형의 전제

열여섯 가지 조합(파일을 바꾸지 않으면 "바꾼 뒤에 하는 일"은 쓰이지 않음)을 127.0.0.1에 띄운 오리진 서버와 엣지 역할의 작은 캐싱 프록시로 실행해, 네 번의 요청이 각각 MISS, HIT, EXPIRED 가운데 무엇이었는지와 받은 내용, 오리진에 도착한 요청 수가 모형과 같은 것을 확인했습니다. 엣지는 Node.js 24로 직접 만든 시험용이며, 사본을 보관해도 되는지와 유효 시간이 남았는지는 HTTP 캐시 규칙 구현인 http-cache-semantics 4.2(공유 캐시 모드)에 맡겼습니다. 80초를 기다리지 않도록 엣지의 시계를 빠르게 돌려(모형의 1초를 실제 30ms로) 시험했고, 사용자 역할은 Node의 fetch와 curl 8.21이 번갈아 맡았습니다. 실제 CDN 서비스에서 확인한 것은 아닙니다. 걸린 시간(20ms, 220ms)과 "가깝다, 멀다"는 측정값이 아니라 차이를 보이기 위한 예시입니다. s-maxage가 공유 캐시의 유효 시간을 정한다는 것과 유효 시간이 지난 사본을 그대로 쓰지 않는다는 것은 RFC 9111 4.2절과 5.2.2.10절, 사본을 요청 메서드와 주소로 찾는다는 것은 2절을 따랐습니다. HIT, MISS, EXPIRED라는 이름과 X-Cache 헤더는 표준이 아니라 여러 CDN과 프록시가 쓰는 관례이고 제품마다 표기가 다릅니다. 무효화(purge)도 표준에 없는 제품별 기능이며, 여기서는 PURGE 메서드로 한 주소의 사본을 지우는 흔한 방식을 따랐습니다. 유효 시간이 지났을 때 이 모형의 엣지는 파일 전체를 다시 받지만, 실제 CDN은 대개 조건부 요청으로 "바뀌었는지"만 먼저 묻습니다(HTTP 캐시와 ETag 실습 참고). 엣지가 여러 지역에 여러 대 있고 저마다 사본을 따로 보관한다는 것, 브라우저가 따로 보관하는 사본, 사본을 미리 채워 두는 기능은 다루지 않았습니다.

앞 실습: 프록시와 리버스 프록시

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요