AWS에는 파일을 업로드하고 가져올 수 있는 S3라는 서비스가 있습니다. 이미지 같은 파일을 올려 웹페이지에 보여주거나, 간단한 파일 공유 서비스를 만드는데 널리 쓰이죠.
그런데 얼마전, 이 S3 서비스에서 버킷 이름만 알면 누구나 쓰기 요청을 보내 요금 폭탄을 맞게 할 수 있는 취야겆ㅁ이 공개되어 큰 화제가 되었습니다. 단순히 업로드를 막는 권한 설정만으로는 이 취약점을 막을 수 없었습니다. 요청이 성공하든 실패하든, 요청 그 자체에 비용이 부과되는 방식이었기 때문입니다.
결국 AWS 부사장이 직접 나서서 문제를 해결하겠다고 약속했고, 며칠뒤 허가되지 않은 요청에 대해서는 요금을 부과하지 않도록 정책이 변경되며 사건은 일단락되었습니다.
https://x.com/jeffbarr/status/1785386554372042890
물론 문제가 해결되었으니 안심해도 되겠다고 생각할수도 있었겠지만, 저는 이 사건을 계기로 ‘사용자에게 S3 버킷 이름이 직접 노출되는 것이 과연 안전한가?’ 라는 근본적인 의문을 갖게 되었습니다. 이 고민은 저를 직접 프록시 서버를 설계하고, 예상치 못했던 비용문제에 부딪히며, 결국 아키텍처의 트레이드 오프를 배우게 되는 긴 여정을 만들어냈습니다.
대안 1 : Presigned URL 활용하기, 그러나…
가장 먼저 떠오른 보편적인 방법은 AWS Presigned URL 이었습니다. 백엔드 서버가 S3에 대한 임시 출입증(URL)을 발급하고, 클라이언트는 이 출입증을 이용해 정해진 시간 동안만 직접 S3와 통신하는 방식입니다.
서버 부하가 적고 보안에도 이점이 있지만, 결정적으로 Presigned URL 주소 안에는 여전히 버킷 이름이 그대로 노출되는 문제가 있었습니다. 만약 비슷한 취약점이 또 발생한다면 속수무책일 수 밖에 없겠죠.
대안 2 : CDN, 비용이 발목을 잡다
다음 AWS CloudFront와 같은 CDN(Content Delivery Network)을 사용하는 것입니다. 이방식은 버킷 이름을 완벽하게 숨길 수 있고, 전 세계 사용자들에게 빠른 속도를 제공합니다.
하지만 당시의 저는 지금 하고 있는 작은 프로젝트에 CDN까지 도입하는 것은 너무 과하고 비쌀 것이라고 막연하게 생각했습니다. 결국 이 선택지는 비용 문제 때문에 일단 보류하게 되었습니다.
고안한 해결책 : 스트리밍 프록시 서버 구축
그래서 저는 백엔드 서버를 데이터 중계자로 사용하는 방법을 직접 만들기로 했습니다. 하지만 단순히 서버가 파일을 전부 다운로드 했다가 다시 업로드 하는 방식은 심각한 문제를 가지고 있었습니다.
예를 들어 1GB짜리 영상을 중계한다면, 서버 메모리가 순간적으로 1GB나 필요하고 전송 시간은 최소 2배 이상으로 늘어납니다. 심지어 메모리 가 부족에 가상 Swap 메모리까지 끌어와 쓰게 되면 처리 속도는 더욱 느려질것입니다. 결국 이런 요청들로 인해 서버 전체가 마비될 수도 있습니다. 이문제를 해결하기 위해 제가 고안한 방법이 바로 스트리밍 프록시 였습니다.
비유하자면, 일반적인 중계 방식은 마당의 큰 물통(서버 메모리)에 물을 가득 채워 옮기는 것이고, 스트리밍 프록시는 수원지(S3)와 우리 집(클라이언트) 사이에 직통 수도관(스트림)을 연결하고 중간에서 밸브만 조작하는 방식입니다. 서버는 데이터를 저장하지 않고 그냥 흘려보내기만 하므로 가볍고 빠릅니다.
이 방법을 통해 버킷 이름을 숨기면서도 서버 부하를 최소화 할 수 있었습니다.
아키텍처 및 구현
- 클라이언트가 EC2 서버에 이미지를 요청합니다.
- EC2는 AWS SDK를 이용하여 AWS로 부터 S3에 대한 Presigned URL을 요청합니다.
- AWS로부터 Presigned URL을 반환받습니다.
- EC2는 발급 받은 URL을 이용해
http-proxy-middleware
라이브러리로 S3에 데이터를 요청합니다. - S3가 데이터 스트림을 보내오면, 미들웨어는 이 스트림을 가공 없이 그대로(passthrough) 클라이언트에게 전달합니다.
아래는 컨트롤러 로직의 핵심 코드 입니다.
// image/controller/image.controller.js
export const handleGetImage = async (req, res, next) => {
// ... 서비스 레이어에서 Presigned URL 생성 ...
const imageURL = new URL(getImageURL(req.query).url);
try {
// 프록시 미들웨어를 생성하여 요청을 S3로 전달
createProxyMiddleware({
target: imageURL.origin, // 프록시 요청을 보낼 실제 S3 도메인
changeOrigin: true, // S3의 가상 호스팅을 위해 호스트 헤더 변경
pathRewrite: () => `${imageURL.pathname}${imageURL.search}`, // S3 경로와 파라미터를 그대로 사용
})(req, res, next);
} catch (error) {
// ... 에러 처리 ...
}
};
이렇게 보안, 비용, 서버 부하까지, 모든 문제를 균형있게 해결한 완벽한 아키텍처라고 생각했습니다.
그러나 제게는 한가지 큰 착각이 있었습니다.
반전 : 나의 가장 큰 착각, 비용문제
프로젝트를 마무리 하며 아키텍처를 재검토하던 중, 저는 제가 결론 내린 “CDN은 비쌀 것이다” 라는 가정이 완전히 틀렸다는 것을 발견했습니다. 클라우드 비용의 핵심인 데이터 전송 요금과 Free Tier 정책을 제대로 이해하지 못했기 때문입니다.
이미지 다운로드(GET) - 한국 리전
구분 | EC2 프록시 + S3 | CDN + S3 |
---|---|---|
데이터 전송 요금 | 무료(100GB까지) | 무료(1TB 까지) |
추가요금 | USD 0.126 (월 10TB까지) | USD 0.120 (월 9TB까지) |
CloudFront(CDN)는 독자적으로 매월 1TB라는 압도적인 무료 트래픽을 제공합니다. 반면 EC2의 무료 트래픽은 다른 모든 서비스와 공유하기 떄문에 금방 소진될 수 있습니다. Free Tier를 넘어선 요금조차 CDN이 더 저렴했습니다.
(초록색 영역: CDN 사용 시 절감액)
(실제 요금은 다를 수 있습니다)
그래프에서 보듯 트래픽이 증가할수록 비용 차이는 기하급수적으로 벌어집니다. 다운로드에서는 CDN이 명백한 승자였습니다.
파일 업로드 (POST/PUT) - 한국 리전
EC2 프록시 + S3 | CDN + S3 | |
---|---|---|
데이터 전송 요금 | USD 0.00 | USD 0.00 |
추가요금 요소 | EC2 기본 요금 | HTTP 매서드에 대한 요청 요금 |
파일 업로드 시 인터넷에서 AWS로 들어오는 데이터 비용은 두 방식 모두 무료입니다. 금전적 비용은 동일했으나, CDN은 우리 서버 자원을 전혀 사용하지 않고 별도의 네트워크를 이용하므로 안정성과 성능면에서 훨씬 우수했습니다.
그렇다면 이 프록시 아키텍처는 언제 유용할까?
그럼 제가 고안한 방법이 쓸모 없는 아키텍처일까요? 그렇지 않습니다. 이방식은 CDN으로 구현하기 어려운 복잡한 비즈니스 로직이 필요할때 강력한 무기가 됩니다.
예를 들어 넷플릭스 처럼 유료 맴버십에 따라 다른 화질(1080p, 4K)의 영상을 제공해야 할 때, 프록시 서버는 S3로 데이터를 요청하기 전에 사용자의 등급을 확인하는 인증 로직을 수행할 수 있습니다. 특정 기념일에만 콘텐츠를 보여주거나, 스트리밍 컨텐츠에 동적으로 워터마크를 삽입하는 등 무궁무진한 응용이 가능합니다.
실책이 아닌, 또 하나의 무기
결론적으로 이번 프로젝트에서 초기 비용 분석이 미흡했던 점은 아쉬움으로 남습니다. 현재 프로젝트 로직 상으로는 CDN을 쓰는것이 더 나은 선택이므로, 추후 리팩토링을 진행할 예정입니다.
하지만 이 경험을 통해 저는 단순히 기능을 구현하는 개발자를 넘어 비용과 성능, 확장성까지 고려해 주어진 상황에 맞는 최적의 아키텍처를 설계할 수 있는 엔지니어의 시간을 가지게 되었습니다. 직접 구현한 프록시 서버는 이제 언제든 상황에 따라 꺼내 쓸 수 있는 강력한 기술 카드 로 제 손에 남았습니다.