개인 운영 서비스를 운영하면서 체감됐던 문제 중 하나는 이미지 트래픽에 대한 요청 비용이었다
서비스 특성상 이미지가 한 번 업로드되어 사용될 때 거의 변하지 않는 정적 리소스로 사용되는데
구조상 API 요청을 통해 매번 S3의 URL을 응답받아 사용하는 방식이었다
그러다 보니 자연스럽게 이런 의문이 들기 시작했다
변하지 않는 정적 리소스를 매번 새로 요청할 필요가 있을까?
계속 이렇게 매번 요청을 하면 요청 횟수와 그에 따른 데이터 요청 비용이 사용자가 늘어남에 따라 비례해서 증가할거라고 예상했습니다
실제로 혼자 서비스를 1달간 사용했을 때 이미지의 요청 수는 AWS 프리티어의 기준 2만 요청 정도 나왔다
(프리티어는 2만 요청수까지 무료로 제공한다)

이러한 구조는 실제로 사용자가 늘어나면 비용과 성능 모두에서 문제가 될 수 있겠다는 판단이 들었다
그래서 정적 이미지를 리소스를 S3 + Cloudflare에 캐시를 적용하여 정적 리소스인 이미지의 요청을 최소화하여
비용과 서버 부하를 줄였던 과정에 대해 정리해 본다
1. 문제 인식
문제가 된 리소스는 다음과 같은 특성을 가지고 있었다
1. 이미지 파일 (png, jpg)
2. 사용자별로 달라지지 않음 (매번 동일)
3. 자주 변경되지 않음
초기 베타 버전에서는 페이지를 새로 고침 하거나 변경할 때마다 동일한 이미지 요청이 반복적으로 발생하였다
이 구조는 기능적으로 문제가 없었지만 운영 관점에서 보면 문제점은 명확했다
요청 횟수가 늘어날수록 스토리지 요청 비용과 데이터 전송 비용이 계속 누적된다
2. 캐시(Cache)란 무엇인가
캐시(Cache)란 간단하게 자주 사용되거나 쉽게 변하지 않는 데이터를 더 빠르고 비용이 적제 드는 위치에 임시로 저장해 두는 기술이다
매 요청마다 원본 데이터 소스에 접근하는 대신, 이미 한 번 가져온 데이터를 재사용함으로써 응답 속도를 줄이고
시스템 부하의 비용을 함께 낮추는 것이 캐시의 목적이라고 볼 수 있다
원본 접근을 줄인다
캐시를 적용하면 매 요청마다 원본 스토리지나 서버로 요청이 전달되지 않는다
대신 캐시된 데이터가 응답을 대신하게 된다
이로 인해 다음과 같은 효과를 볼 수 있다
- 서버 부하 감소
- 스토리지 요청 횟수 감소
- 네트워크 트래픽 감소
동일한 데이터 요청에 강하다
캐시는 동일한 데이터가 반복적으로 요청되는 상황에서 가장 큰 효과를 낸다
예를 들어 이미지 파일,CSS,JS 같은 정적 리소스
내용이 거의 변하지 않는 데이터는 캐시와 매우 잘 맞는 대상이다
3. 캐시를 적용하려 했던 이유
이 서비스에 캐시를 도입하기로 했던 이유는 이미지 리소스의 특성과 요청 패턴이 캐시의 장점과 맞아 떨이지기 때문이다
먼저 서비스에서 사용되는 이미지는 한 번 업로드되면 수정되지 않고 사용자 별로 달라지지 않는다
즉 동일한 데이터가 반복적으로 요청되는 구조였다
또한 이미지는 페이지 하나당 여러 개가 사용되며 페이지 접근이 늘어날수록 요청 회수도 함께 증가한다
이 구조에서 원본 스토리지로 직접 요청이 계속 전달되면
- 스토리지 요청 비용

- 데이터 전송 비용

사용자 수에 비례해 계속 증가하게 된다
이미지는 실시간으로 변경될 필요가 없는 리소스이기 때문에 일정 기간 동안 캐시된 데이터를 사용하더라도
서비스 기능이나 사용자 경험에 문제가 되지 않을 거라고 생각했다
4. 해결 방향: S3 스토리지 객체 & CDN 캐시 적용
이미지 요청 비용 문제를 해결하기 위해 가장 먼저 고민했던 부분은 단순했다
변하지 않는 이미지라면 매번 원본 스토리지까지 요청할 필요는 없다
이를 기준으로 스토리지 레벨 → CDN 레벨 순서로 캐시를 적용했다.
S3 객체 메타데이터에 Cache-Control 설정
가장 먼저 적용한 것은 S3에 저장된 이미지 객체 자체의 캐시 정책이었다
각 이미지 객체의 메타데이터에 다음과 같은 헤더로 설정했다

이 설정의 의미는 다음과 같다
- public: 누구나 캐시 가능 (중간 캐시,CDN 포함)
- max-age: 315360000: 최대 1년 동안 유효
- immutable: 유효 기간 동안 리소스가 변경되지 않음을 명시
이미지가 한 번 업로드되면 거의 변경되지 않는 특성을 가지기 때문에
길게 캐시해도 데이터 정합성 문제가 없을 거라고 판단했다
CDN 적용을 위한 구조 구성 과정
기존의 서버를 이미 Cloudflare(CDN) 도메인을 구입 후 적용하여 Cloudflare를 앞단에 배치하여 보안 및 트래픽 관리를 위임하고 있었기 때문에 동일한 구조를 활용하여 CDN 캐시 또한 Cloudflare를 통해 적용하려고 했다
정적 이미지 리소스에 CDN 캐시를 적용하기 위해서 Cloudflare가 원본 데이터를 가져올 수 있는 Origin 서버 엔드포인트가 필요했다
1. S3 정적 웹 사이트 호스팅 활성화
S3 버킷에 대해 정적 웹 사이트 호스팅을 활성화했다

정적 웹 사이트 호스팅을 적용하게 되면
- S3 객체를 HTTP 기반으로 제공 가능
- 정적 리소스를 제공하기 위한 고정된 엔드포인트가 생성
이 단계의 목적은 이미지 리소스를 정적 웹 리소스로 분리하고 Cloudflare의 도메인을 적용하여 CDN이 S3의 앞단에 붙을 수 있는 구조를 설계하는 것이었다
CDN을 적용하기 위한 전제 조건에 가깝다
S3 정적 웹 사이트 호스팅 적용 방법도 나중에 정리해 볼 생각이다
2. 정적 웹 사이트 도메인을 Cloudflare에 연결
다음으로 S3 정적 웹 사이트 호스팅으로 생성된 엔드포인트를 Cloudflare에서 구입한 도메인과 연결했다

S3 버킷 이름이 외부에 노출되는 것을 방지하기 위해 버킷 이름도 도메인과 동일하게 변경하여 정적 웹 사이트 호스팅을 적용하고정적 리소스 제공을 위해 Cloudflare에 CNAME 레코드를 설정하고 프록시 모드를 활성화하여 모든 요청을 Cloudflare를 경유하도록 구성했다
Cloudflare는 CND 캐시 및 HTTPS를 담당하고 캐시 MISS 발생 시 S3 정적 웹 사이트 호스팅 엔드포인트를 Origin으로 조회하도록 설정했다
이제 모든 이미지 요청은 Cloudflare를 거쳐서 전달되는 구조가 되었다
Cloudflare의 CDN 기능을 활용할 수 있는 상태가 됐다
3. Cloudflare Cache Rules 적용
S3 객체에 캐시 정책을 설정하고 정적 웹 사이트 호스팅을 통해 CDN을 적용할 수 있는 구조를 만든 뒤
마지막으로 Cloudflrare Cache Rules를 통해 캐시 동작을 보다 명확하게 제어하도록 구성했다
- Cache Rule 생성
모든 요청이 아닌 정적 리소스 요청에만 적용되도록 조건을 걸었다
여기서 "수신 요청이 일치하는 경우..." 이 부분은 URL 경로 즉 S3 버킷의 객체가 저장되는 폴더이름을 작성해 준다

- 캐시 적합성(Cache Eligibility) 설정
- 캐시 적합성: 캐시에 적합
- 해당 규칙에 매핑되는 요청은 캐시 가능 대상으로 처리
- 원 본 응답이 캐시 될 수 있도록 허용
이를 통해 이미지 요청이 캐시 조건에서 제외되는 상황을 방지해 준다
- Edge TTL 설정
다음으로 Edge(에지) TTL을 설정했다
- "캐시 제어 헤더가 있는 경우 사용하고,없는 경우 응답 상태에 대해 Cloudflare의 기본 TTL로 요청을 캐시 합니다."
- 상태 코드 기준으로 HTTP 200 응답 → 7일 캐시로 설정했다
정상적으로 이미지 응답이 된 경우 CDN에서 장기간 재사용되도록 유지해 준다

- Browser Cache TTL 설정
브라우저 캐시에 대해서는 원본 TTL 유지 옵션을 선택했다
S3 객체에 설정한 Cache-Control 헤더를 브라우저가 그대로 따르도록 하기 위해서 적용했다

Caching → 구성에서 브라우저 캐시 TTL 또한 6개월로 설정해 주었다

5. 최종 동작 흐름 및 비용 절감
CDN의 Cache Rules 까지 적용 이후 이미지 요청 흐름은 다음과 같이 동작한다
간단하게 서비스의 아키텍처를 그려봤다 (포함되지 않은 요소도 있습니다)

- 최초 이미지 리소스 요청 시에만 원본 스토리지 접근
- 이후 동일 요청은 Cloudflare 캐시에서 직접 응답
최종적으로 원본 스토리지 요청 횟수와 데이터 전송 비용 감소로 이어졌다
6. 적용 후 확인한 변화
캐시 적용 이후 다음과 같은 변화를 확인할 수 있었다
최초 이미지 리소스 요청


최초로 이미지 리소스를 요청했을 때는 S3 객체에 Cache-Control 헤더가 정상적으로 적용된 것을 확인할 수 있었지만
요청 자체는 원본 스토리지(S3)로 직접 전달되어 리소스를 가져오는 흐름으로 보인다
이는 최초 요청 시 캐시 구조상 정상적인 동작으로 보인다
이후 동일한 이미지 리소스 요청 (캐시 적용)


같은 이미지 리소스를 다시 요청했을 때는 네트워크 탭의 Status Code의 200 OK 옆을 보면 (from memory cache)가 출력된다
이는 브라우저가 이미 한 번 내려받은 리소스를 메모리 캐시에서 직접 로드했음을 의미한다
또한 응답 헤더를 확인해 보면 Cf-Cache-Status: HIT 를 볼 수 있는데
해당 요청이 원본(S3)으로 전달되지 않았고 Cloudflare 엣지 서버의 캐시에서 직접 응답되었음 을 의미한다
7. 테스트 과정에서 발견한 한계
적용 이후 테스트를 진행하던 중 로그아웃 이후 다시 로그인한 상황에서는 캐시가 적용되지 않는 것처럼 보였다
인증에 따라 상태 변경이 일어나 상태 변경을 감지하여 리소스를 다시 요청하는 것으로 보인다
다만 이 경우에도 요청은 원본 스토리지까지 요청하지 않고 disk cache 또는 CDN cache에서 처리되고 있었다

8. 마무리
이번 경험을 통해 캐시를 직접적으로 개발에 활용해 볼 수 있었고 실제로 비용 절감까지 이어졌기 때문에 굉장히 의미 있는 경험이라고 생각합니다
또한 특정 서비스에서도 이와 같은 문제는 공통적으로 발생할 수 있다고 생각하며 비슷한 상황에서도 하나의 참고 자료가 될 수 있지 않을까 하며 이번글을 작성하게 되었습니다
본 글의 내용은 이전에 작업했던 내용을 기반으로 작성되었으며
일부 내용은 기억에 의존하여 정리된 부분이 있어 세부 구현이나 설정 값에 있어 부정확한 정보가 포함될 수 있습니다
'사이드 프로젝트' 카테고리의 다른 글
| 개인 운영 프로젝트 N + 1 문제 해결로 1900ms → 543ms로 70% 성능 개선 (0) | 2026.01.18 |
|---|