📚 목차
[Network] CloudFront 통해 서버 요청 라우팅과 캐시 설계하기
회사에서 커뮤니티 서비스를 운영하며 CloudFront 인프라를 깊게 다룰 기회가 있었다.
처음에는 CloudFront를 단순히 정적 파일을 빠르게 내려주는 CDN 정도로만 이해했다. 하지만 실제 운영 환경에서는 CloudFront가 단순 캐시 계층을 넘어, 사용자의 요청을 가장 먼저 받아 어떤 Origin으로 보낼지 결정하는 서비스 진입점에 가까웠다.
특히 팀에서 다루던 커뮤니티 서비스는 하나의 코드베이스와 인프라를 여러 서비스가 공유하는 멀티테넌트 구조였다. 이 구조에서는 단순히 “캐시를 잘하면 빠르다” 정도로 끝나지 않았다. 어떤 요청을 어떤 Origin으로 보낼지, 어떤 기준으로 캐시를 나눌지, 어떤 헤더와 쿠키를 Origin까지 전달할지를 세밀하게 설계해야 했다.
이 글에서는 회사에서 CloudFront 기반 인프라를 다루며 정리한 내용을 바탕으로, 멀티테넌트 커뮤니티 서비스에서 CloudFront를 어떻게 이해하면 좋을지 정리해보려 한다.
멀티테넌트란?
멀티테넌트는 여러 고객, 서비스, 브랜드 또는 조직이 하나의 시스템을 공유하면서도 각자 독립된 서비스처럼 동작하는 구조를 말한다.
예를 들어 하나의 커뮤니티 플랫폼이 있다고 가정해보자. 내부 구현은 하나의 코드베이스와 공통 서버를 사용하지만, 실제 사용자에게는 서비스마다 다른 커뮤니티처럼 보여야 한다.
서비스 A → A 전용 커뮤니티처럼 보임
서비스 B → B 전용 커뮤니티처럼 보임
서비스 C → C 전용 커뮤니티처럼 보임이때 각 테넌트는 다음과 같은 값이 달라질 수 있다.
- 도메인
- 서브디렉토리
- 서비스 설정
- 브랜드 색상
- 커뮤니티 설정
- 언어
- 웹뷰 여부
- 노출 정책
- API 응답문제는 사용자가 보는 화면은 테넌트마다 다르지만, 내부적으로는 같은 CloudFront, 같은 서버, 같은 코드베이스를 공유할 수 있다는 점이다. 그래서 멀티테넌트 서비스에서는 요청을 정확히 구분하고, 캐시도 테넌트 단위로 안전하게 분리하는 것이 중요하다.
1. 전체 인프라 구조와 요청 흐름

전체 요청 흐름은 크게 다음과 같이 볼 수 있다.
User
↓
CloudFront Edge
↓
캐시 HIT이면 Edge에서 바로 응답
캐시 MISS이면 Origin으로 요청 전달
↓
Origin
- S3 정적 리소스
- Astro 상용 서버
- Next.js 상용 서버
- 기타 Node 서버사용자는 하나의 도메인으로 접근하지만, 실제 요청은 CloudFront 설정에 따라 S3, Astro 서버, Next.js 서버, Node 서버 등 적절한 Origin으로 전달된다.
CloudFront는 이 구조에서 사용자 요청을 가장 먼저 받는다. 그리고 캐시가 있다면 Edge Location에서 바로 응답하고, 캐시가 없다면 설정된 Origin으로 요청을 보낸다.
캐시 HIT 흐름
캐시 HIT가 발생하면 요청은 Origin까지 가지 않는다.
User → CloudFront Edge → User이 경우 Origin 서버의 부하를 줄일 수 있고, 사용자와 가까운 Edge Location에서 응답하기 때문에 속도도 빨라진다.
캐시 MISS 흐름
캐시가 없거나 만료된 경우에는 Origin까지 요청이 전달된다.
User → CloudFront Edge → Origin → CloudFront Edge → UserOrigin 응답이 캐싱 가능한 응답이라면 CloudFront는 해당 응답을 저장하고, 이후 동일한 캐시 키를 가진 요청에 대해 Edge에서 바로 응답할 수 있다.
Astro와 Next.js가 함께 있는 구조
운영 중인 서비스에는 Astro 기반 서버와 Next.js 기반 서버가 함께 존재했다.
CloudFront
├─ Astro Origin
├─ Next.js Origin
├─ S3 Origin
└─ Node Origin사용자 입장에서는 하나의 도메인으로 접근하지만, CloudFront 내부 설정에서는 요청 경로와 조건에 따라 서로 다른 Origin으로 트래픽이 분기된다.
이 구조의 핵심은 “하나의 도메인 뒤에 여러 Origin이 존재할 수 있다”는 점이다.
2. CloudFront의 핵심 역할: 캐싱과 Origin 라우팅
CloudFront는 AWS의 CDN이다. 일반적으로는 사용자와 가까운 Edge Location에 콘텐츠를 캐싱하여 더 빠르게 응답하는 역할로 설명된다.
하지만 실제 서비스 인프라에서는 CloudFront를 단순 CDN으로만 보기 어렵다. 특히 멀티테넌트 서비스에서는 다음 세 가지 역할이 중요했다.
1) CDN 캐싱
JS, CSS, 이미지, 폰트 같은 정적 리소스는 CloudFront Edge에 캐싱하기 좋다.
User → CloudFront Edge → User캐시가 있으면 Origin까지 가지 않고 Edge에서 바로 응답한다. 이 방식은 정적 리소스 전송 속도를 개선하고, Origin 서버 부하를 줄이는 데 효과적이다.
2) Origin 라우팅
CloudFront는 요청 경로, 도메인, 헤더, 쿠키, 쿼리 스트링 등을 기준으로 어떤 Origin으로 요청을 보낼지 결정할 수 있다.
예를 들어 같은 도메인 안에서도 경로에 따라 다른 Origin으로 보낼 수 있다.
example.com/community/... → Astro Origin
example.com/app/... → Next.js Origin
example.com/assets/... → S3 Origin
example.com/api/... → Node Origin즉, CloudFront는 단순히 캐시된 파일을 내려주는 계층이 아니라, 서비스 앞단에서 트래픽을 분기하는 라우팅 계층이기도 하다.
3) 배포 안정성
S3와 CloudFront를 함께 사용하면 정적 리소스를 안정적으로 배포할 수 있다.
예를 들어 빌드된 정적 파일은 S3에 업로드하고, 사용자는 CloudFront를 통해 해당 리소스에 접근한다. 이때 CloudFront 캐시 정책과 무효화 전략을 함께 설계하면 배포 중에도 사용자에게 안정적으로 리소스를 제공할 수 있다.
또한 CloudFront가 앞단에서 Origin을 분리해주기 때문에, 서비스별 배포 방식이 달라도 사용자에게는 하나의 도메인과 일관된 진입점으로 제공할 수 있다.
3. 서브도메인 구조와 서브디렉토리 구조의 차이

CloudFront 설정이 복잡해지는 지점은 “하나의 Host 안에서 여러 서비스를 구분해야 할 때”이다.
서브도메인 기반 구조
서브도메인 기반 구조에서는 테넌트나 서비스마다 도메인이 분리된다.
tenant-a.example.com → Tenant A Origin
tenant-b.example.com → Tenant B Origin이 구조에서는 Host 자체가 요청을 구분하는 기준이 된다. 따라서 CloudFront에서도 비교적 단순하게 도메인별 Origin을 연결할 수 있다.
Host: tenant-a.example.com
→ Tenant A Origin서브디렉토리 기반 구조
반면 서브디렉토리 기반 구조에서는 하나의 루트 도메인 아래에 여러 서비스가 들어간다.
example.com/community
example.com/challenge
example.com/benefit이 경우 Host는 모두 같다.
Host: example.com그래서 단순히 Host만 보고는 어떤 서비스 또는 테넌트 요청인지 알 수 없다. 이때는 CloudFront의 Behavior, Origin, Cache Policy, Origin Request Policy, 커스텀 헤더 등을 조합해 요청을 세밀하게 구분해야 한다.
같은 Host에서 요청을 구분하는 방법
하나의 Host 안에서 여러 서브디렉토리를 운영한다면, 경로 기반 Behavior를 사용할 수 있다.
/community/* → Community Origin
/challenge/* → Challenge Origin
/assets/* → S3 Origin또는 Origin으로 요청을 보낼 때 커스텀 헤더를 추가해, Origin 서버가 요청의 성격을 판단하도록 만들 수 있다.
예를 들어 특정 서브디렉토리 요청을 내부적으로 구분하기 위해 다음과 같은 헤더를 전달할 수 있다.
Header Name: X-before-subdirectory-domain
Header Value: community이렇게 하면 Origin 서버는 CloudFront가 전달한 헤더를 보고, 이 요청이 어떤 서비스 또는 테넌트에서 온 것인지 판단할 수 있다.
중요한 점은 CloudFront가 단순히 요청을 전달하는 것이 아니라, Origin 서버가 올바르게 동작할 수 있도록 필요한 컨텍스트를 함께 전달할 수 있다는 것이다.
4. Edge에서 Origin까지 설정해야 하는 것들
CloudFront에서는 크게 두 구간의 동작을 나누어 생각할 수 있다.
User → CloudFront Edge
CloudFront Edge → Origin각 구간에서 설정해야 하는 내용이 다르다.
User → Edge 구간
사용자가 CloudFront에 요청할 때의 정책을 설정하는 구간이다.
대표적으로 Viewer Protocol Policy가 있다.
HTTP 허용
HTTP 요청을 HTTPS로 리다이렉트
HTTPS만 허용일반적으로 서비스에서는 보안을 위해 HTTP 요청을 HTTPS로 리다이렉트하거나, HTTPS만 허용하는 방식으로 설정한다.
이 구간은 사용자가 CloudFront에 어떤 방식으로 접근할 수 있는지를 결정한다.
Edge → Origin 구간
CloudFront가 Origin으로 요청을 보낼 때 어떤 정보를 전달할지 설정하는 구간이다.
Origin 서버는 단순히 Path만 보고 동작하지 않을 수 있다. 인증, 언어, 디바이스, 웹뷰 여부, 테넌트 정보 등을 헤더나 쿠키를 통해 판단할 수 있기 때문이다.
Origin으로 전달할 수 있는 대표적인 값은 다음과 같다.
- Query String
- Header
- Cookie
- Host
- Authorization운영 환경에서는 Origin이 사용자의 요청 정보를 최대한 동일하게 받아야 하는 경우가 있다. 이런 경우 AllViewer 성격의 Origin Request Policy를 사용할 수 있다.
다만 모든 값을 Origin으로 전달한다고 해서 항상 좋은 것은 아니다. 전달되는 값이 많아질수록 캐시 효율이 떨어지거나, Origin 서버가 처리해야 하는 요청 다양성이 늘어날 수 있다. 따라서 “Origin이 실제로 필요한 값이 무엇인지”를 기준으로 전달 범위를 결정해야 한다.
자동 객체 압축
CloudFront는 응답 객체를 자동으로 압축할 수 있다.
JS, CSS, HTML, JSON 같은 텍스트 기반 리소스는 압축 효과가 크다. 자동 압축을 켜면 CloudFront가 사용자에게 더 작은 크기의 응답을 내려줄 수 있다.
압축 전 HTML/CSS/JS
↓
CloudFront 자동 압축
↓
압축된 응답 전송이 설정은 네트워크 전송량을 줄이고, 특히 정적 리소스 로딩 성능 개선에 도움이 된다.
5. Cache Policy와 Origin Request Policy

CloudFront 설정에서 특히 헷갈리기 쉬운 개념이 Cache Policy와 Origin Request Policy이다.
둘 다 헤더, 쿠키, 쿼리 스트링과 관련이 있어 비슷해 보이지만 역할은 명확히 다르다.
Cache Policy
→ CloudFront가 캐시를 구분하는 기준
Origin Request Policy
→ CloudFront가 Origin으로 전달하는 요청 정보Cache Policy
Cache Policy는 CloudFront가 무엇을 기준으로 캐시를 나눌지 결정한다.
즉, 캐시 키를 만드는 정책이다.
캐시 키에는 다음과 같은 값이 포함될 수 있다.
- Path
- Query String
- Header
- Cookie캐시 키가 다르면 CloudFront는 서로 다른 응답으로 판단한다.
예를 들어 같은 Path로 접근하더라도 테넌트가 다르면 서로 다른 HTML이 내려가야 한다고 가정해보자.
Tenant A 요청 → A용 HTML
Tenant B 요청 → B용 HTML이때 테넌트 식별 값이 캐시 키에 포함되지 않으면 문제가 생길 수 있다.
Tenant A 요청
→ A용 HTML이 CloudFront에 캐싱됨
Tenant B 요청
→ 같은 캐시 키로 판단됨
→ B 사용자에게 A용 HTML이 내려갈 위험따라서 멀티테넌트 서비스에서는 테넌트를 구분할 수 있는 Host, Path, Header, Query String 등을 캐시 키에 적절히 포함해야 한다.
TTL 설정
TTL은 캐시를 얼마나 오래 유지할지 결정하는 값이다.
CloudFront에서는 일반적으로 다음 TTL을 설정한다.
Minimum TTL
Default TTL
Maximum TTL정적 리소스는 파일명이 해시 기반으로 관리된다면 긴 TTL을 가져갈 수 있다.
app.abc123.js
style.def456.css반면 HTML처럼 자주 바뀌거나 테넌트 설정에 따라 응답이 달라지는 리소스는 TTL을 짧게 가져가거나 캐싱하지 않는 편이 안전할 수 있다.
멀티테넌트 구조에서는 TTL을 잘못 설정하면 오래된 설정이 계속 보이거나, 다른 테넌트의 응답이 남는 문제가 발생할 수 있다. 그래서 리소스의 성격에 따라 TTL을 다르게 가져가야 한다.
Origin Request Policy
Origin Request Policy는 CloudFront가 Origin으로 요청을 보낼 때 어떤 정보를 함께 전달할지 결정한다.
예를 들어 다음 정보를 Origin에 전달할 수 있다.
- Headers
- Cookies
- Query Strings여기서 중요한 점은 Origin에 전달한다고 해서 반드시 캐시 키에 포함되는 것은 아니라는 점이다.
예를 들어 어떤 헤더는 Origin 서버의 로직에는 필요하지만, 캐시를 나누는 기준으로 쓰고 싶지는 않을 수 있다.
Origin에는 전달한다
하지만 캐시 키에는 포함하지 않는다반대로 어떤 값은 캐시 키에 반드시 포함해야 할 수도 있다.
테넌트 식별 헤더
서비스 구분 Path
언어 설정
웹뷰 여부이 값들이 응답 결과에 영향을 준다면 캐시 키에 포함되어야 한다.
두 정책의 차이
두 정책은 다음처럼 구분하면 쉽다.
Cache Policy
→ CloudFront 내부에서 캐시를 어떻게 나눌 것인가?
Origin Request Policy
→ Origin 서버에 어떤 요청 정보를 넘길 것인가?예를 들어 Authorization 헤더를 생각해보자.
Origin 서버가 인증을 위해 Authorization 헤더를 필요로 한다면 Origin Request Policy에는 포함해야 한다. 하지만 사용자별 응답이 캐싱되면 안 되는 구조라면, 해당 요청은 캐싱하지 않거나 매우 신중하게 캐시 키를 설계해야 한다.
이처럼 Cache Policy와 Origin Request Policy를 분리해서 이해해야 안전한 CloudFront 설정을 만들 수 있다.
6. 캐시 HIT인데 왜 Status Code는 200인가
실제 서비스의 개발자도구 Network 탭을 열어보면 다음과 같은 응답을 볼 수 있다.

여기서 헷갈렸던 부분은 “캐시 HIT이면 304가 나와야 하는 것 아닌가?”였다. 결과는 아니었다. 캐시 HIT이어도 Status Code는 그대로 200 OK가 맞다.
핵심은 두 개념을 분리해서 봐야 한다는 점이다.
Status Code 200 OK
→ 요청한 HTML을 정상적으로 응답했다는 HTTP 상태 코드
X-Cache: Hit from cloudfront
→ 그 200 응답을 Origin 서버가 아니라 CloudFront 캐시에서 내려줬다는 의미즉 위 응답은 다음 조합으로 이루어져 있다.
Status Code: 200 OK
X-Cache: Hit from cloudfront
Age: 40
Via: ... cloudfront.net이걸 풀어보면 "CloudFront 엣지 캐시에 저장되어 있던 HTML 200 응답을 그대로 내려준 상태"라는 뜻이다.
Cache-Control과 Age로 판단하는 법
여기서 실제로 캐시가 살아있는지 판단하려면 다음 두 헤더를 같이 봐야 한다.
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=31536000
Age: 40각 값의 의미는 다음과 같다.
max-age=0
→ 브라우저 캐시는 즉시 stale 처리
s-maxage=60
→ CloudFront 같은 shared cache(CDN)는 60초 동안 캐시 가능
Age: 40
→ 이 응답이 CloudFront 캐시에 저장된 지 약 40초 지났다는 의미s-maxage=60이고 Age가 40이라면 아직 캐시 유효 시간 안쪽이다. 그래서 CloudFront는 Origin까지 갈 필요 없이 캐시에 있던 응답을 곧바로 내려준 것이다.
정리하면 다음과 같다.
200 OK → 응답 성공
X-Cache: Hit from cloudfront → CloudFront 캐시에서 응답
X-Cache: Miss from cloudfront → 캐시에 없어서 Origin까지 갔다 옴
Age → 캐시에 저장된 후 지난 시간304와 혼동하기 쉬운 이유
“캐시 HIT = 304”라고 생각하기 쉬운 이유는, 브라우저 캐시 재검증(revalidation) 흐름과 CDN 캐시 흐름을 같은 개념으로 섞어서 이해하기 때문이다.
304 Not Modified
→ 브라우저(또는 캐시)가 이미 가진 리소스를 조건부 요청으로 재검증했을 때,
"너가 가진 거 아직 써도 된다"고 응답하는 상태
200 OK + X-Cache: Hit from cloudfront
→ CloudFront가 캐시된 응답 본문을 애초에 그대로 내려주는 상태CloudFront가 캐시된 HTML을 직접 내려줄 때는 본문을 실제로 전송하는 것이기 때문에 304가 아니라 200이 나오는 것이 자연스럽다. 304는 조건부 요청(If-None-Match, If-Modified-Since)에 대한 응답일 때 나오는 상태 코드이고, CloudFront 캐시 HIT는 그 요청/응답 흐름 자체가 다르다.
마무리
이번 경험을 통해 CloudFront는 단순히 정적 파일을 빠르게 내려주는 CDN이 아니라는 점을 체감했다.
특히 멀티테넌트 커뮤니티 서비스에서는 CloudFront가 다음 역할을 동시에 수행한다.
- 사용자 요청의 첫 진입점
- Edge 캐싱 계층
- Origin 라우팅 계층
- 서브디렉토리 기반 서비스 분기
- 테넌트 구분을 위한 헤더 전달
- 정적 리소스 배포 안정화
- 캐시 키와 TTL을 통한 응답 분리가장 중요했던 부분은 캐시 키 설계였다.
멀티테넌트 구조에서는 같은 코드베이스와 같은 도메인을 사용하더라도, 테넌트마다 응답이 달라질 수 있다. 이때 테넌트 구분값을 캐시 키에 포함하지 않으면 다른 테넌트의 화면이나 데이터가 잘못 내려갈 수 있다.
그래서 CloudFront를 설정할 때는 다음 질문을 계속 던져야 한다.
이 요청은 어떤 Origin으로 가야 하는가?
이 응답은 캐싱해도 되는가?
캐싱한다면 무엇을 기준으로 캐시를 나눌 것인가?
Origin은 어떤 헤더, 쿠키, 쿼리 스트링을 필요로 하는가?
이 값이 응답 결과에 영향을 주는가?
TTL은 얼마나 가져가는 것이 안전한가?CloudFront는 편리한 CDN이지만, 멀티테넌트 구조에서는 잘못 설정했을 때 장애 범위가 커질 수 있는 인프라 계층이기도 하다.
따라서 CloudFront를 “CDN 설정” 정도로만 바라보기보다는, 서비스의 요청 흐름과 배포 안정성, 캐시 안정성을 책임지는 앞단 인프라로 이해하는 것이 중요하다.
회사에서 이 구조를 직접 다루며 배운 가장 큰 점은 다음과 같다.
CloudFront의 핵심은 캐시를 켜는 것이 아니라,
어떤 요청을 어떤 기준으로 구분하고,
어떤 Origin으로 보내며,
어떤 응답을 얼마나 안전하게 재사용할지 결정하는 것이다.