Part 1. 기능 개요

평소 인터넷을 통해 옷을 구매하는 경우 정말 나와 어울리는지 입어볼 수 없다는 것이 문제였다. 따라서 최근 구글에서 출시한 나노 바나나 모델을 통해 사용자의 사진과 쇼핑몰의 옷 사진을 합성해 보여줄 수 있다면 빠르게 가상으로 피팅하고 구매 결정을 빠르게 할 수 있게 도와 구매 전환율을 높일 수 있을 것이라 생각했다.(위 사진처럼 - 저는 톰브라운 가디건 없어요) 하지만 구글의 나노 바나나 API는 비용이 들어가는 유료 API 였기에 무제한적으로 사용되는 것을 막기 위한 사용량 제한 로직이 필요했다. 그리고 그전에 사용자의 사진을 업로드 할 수 있는 기능도 필요했다
Part 2. 사진 업로드 기능 구현(Presigned URL)
먼저 사용자의 사진을 업로드 할 수 있는 기능이 필요하다. S3를 통해 업로드하는 과정에는 3가지 방법이 있다.
1. 서버 경유 업로드 - 클라이언트가 서버로 파일을 보내고 서버가 해당 파일을 S3에 업로드
2. 클라이언트 직접 업로드 - 클라이언트가 AWS SDK를 이용해 S3에 직접 파일을 업로드
3. Presigned URL 방식 - 서버에게 PresignedURL을 받아 S3로 직접 업로드
각각의 방식을 더 자세히 살펴보면,
1. 서버 경유 업로드
클라이언트가 서버로 파일을 전송하면 서버가 S3에 업로드하는 방식이다. 구현이 간단하고 서버에서 파일을 검증하거나 이미지 리사이징 같은 전처리를 할 수 있다는 장점이 있다. 하지만 파일이 클라이언트에서 서버로, 다시 서버에서 S3로 두 번 전송되기 때문에 서버 부하가 크고 대역폭을 2배로 소비한다. 따라서 소량의 작은 파일을 다루거나 업로드 전 전처리가 반드시 필요한 경우에 적합하다.
2. 클라이언트 직접 업로드 (AWS SDK)
클라이언트가 AWS SDK를 사용해 S3에 직접 업로드하는 방식이다. 서버 부하가 전혀 없고 빠르다는 장점이 있지만, AWS 자격 증명을 클라이언트에 노출해야 하는 심각한 보안 문제가 있다. AWS Cognito 같은 서비스를 사용하더라도 Presigned URL 방식과 같이 상세한 권한 관리가 어렵고, 클라이언트 코드에 AWS 리소스 사용 로직이 오픈된다는 점에서 보안상으로도 위험하다.
3. Presigned URL 방식
서버가 임시 업로드 URL을 생성해 클라이언트에게 전달하고, 클라이언트는 그 URL로 S3에 직접 업로드하는 방식이다. AWS 자격 증명을 노출하지 않으면서도 서버 부하를 최소화할 수 있어 보안과 성능의 균형이 가장 좋다. 또한 URL 생성 시 파일명, 크기, 만료시간, 컨텐츠 타입 등을 제한할 수 있어 세밀한 권한 제어가 가능하다. 다만 URL 요청과 업로드라는 2단계 과정이 필요하고, 업로드 완료를 서버에 알리는 콜백 로직이 추가로 필요하다.
3가지 방식 중에서는 서버 부하와 보안 문제를 고려하여 Presigned URL 방식을 선택했다. 다만 Presigned URL 방식을 선택했을 때의 문제점과 부가적인 문제점을 더 고려해 보아야 할 것 같다.
추후 S3 비용 문제를 고려한다면?
S3 비용 정책을 간략하게 정리하면
1. 스토리지 비용 - $0.025 (GB당)
2. PUT/POST 요청 비용 - $0.005 per 1,000
3. GET/SELECT 요청 비용 - $0.0004 per 1,000
4. S3 → 인터넷 (다운로드) - $0.126 per GB (첫 10TB)
예를 들어, 월 사용자 10만 명에 각자 사진 10장(한장 당 1MB) 업로드 및 10번 조회를 했다고 가정해보자
스토리지
- 총 100만 장 × 1MB = 1TB
- 스토리지 비용: 1,000GB × $0.025 = $25/월
다운로드 (각 사진 평균 10회 조회)
- 총 1,000만 회 × 1MB = 10TB
- 전송 비용: 10,000GB × $0.126 = $1,260/월
요청 비용
- PUT: 100만 / 1,000 × $0.005 = $5
- GET: 1,000만 / 1,000 × $0.0004 = $4
───────────────────────────────────
월 총 비용: $1,294 (약 170만원)
S3 비용의 대부분이 다운로드 비용이다!
하지만 사진과 같은 파일은 자주 변경되는 경우가 없기 때문에 CDN과 같은 캐시를 두어서 다운로드 비용을 절약할 수 있다. CDN으로 AWS의 CloudFront를 사용할 때 비용은 다음과 같다
1. CloudFront → 인터넷 (다운로드) - $0.085 per GB (첫 10TB)
2. HTTPS 요청 - $0.010 per 10,000
S3 다운로드
- 10,000GB × $0.126 = $1,260
CloudFront
- 10,000GB × $0.085 = $850
- CloudFront 요청 비용: 약 $10
- 총: $860
절감액: $400 (약 32%)
이처럼 S3의 다운로드 비용을 절약하기 위해서 CDN을 사용할 수 있다. 하지만 현재는 시나리오만큼의 많은 양을 저장하고 요청하고 있지는 않기 때문에 추후에 비용 문제를 고려할만큼의 트래픽이 발생한다면 CDN 도입을 고려하겠다.
업로드 완료 확인 누락 문제
클라이언트가 S3에 업로드는 했지만 서버에 완료 알림을 보내지 않는 경우가 발생할 수 있다. 네트워크 끊김, 브라우저 닫기 등으로 콜백이 실패하면 S3에는 파일이 있지만 DB에는 레코드가 없는 불일치 상태가 된다. 이 문제는 클라이언트 측 재시도 로직과 서버 측 정리 작업으로 대응할 수 있다. 클라이언트에서는 완료 알림 전송을 여러 번 재시도하고, 임계치(예: 3~5회)까지 실패하면 업로드 실패로 간주하여 사용자에게 재업로드를 안내한다. 서버에서는 주기적인 배치 작업을 통해 DB에 레코드가 없는 고아 파일(orphaned files)을 탐지하고 자동으로 삭제하여 스토리지 비용 낭비를 방지하도록 하자.
Part 3. 처리율 제한 알고리즘(Token Bucket)
처리율 제한 알고리즘에는 여러가지 방식이 있었다. 해당 알고리즘을 선택하기 전에 현재 우리 서비스에 어떠한 방식이 필요했는지 체크해보았다.
1. 시간 당 몇번씩 사용 가능 횟수가 충전되는 방식
2. 특정 최대치까지는 충전가능(버스트 가능)
3. 최대한 적은 메모리 사용
알고리즘으로는 다음과 같은 것들이 있었다
1. Token Bucket
2. Fixed Window Counter
3. Sliding Window Log
4. Sliding Window Counter
1. Token Bucket
토큰 버킷 알고리즘은 일정한 속도로 토큰이 버킷에 채워지고, 요청이 올 때마다 토큰을 소비하는 방식으로 최대 용량이 있어서 토큰이 넘치면 더 이상 쌓이지 않는다. 요청 시 토큰이 있으면 처리하고 토큰을 제거하며, 토큰이 없으면 요청을 거부한다. 장점으로는 버스트 트래픽을 허용하면서도 장기적으로는 평균 처리율을 보장하고, 메모리 사용이 적으며(토큰 개수와 마지막 충전 시간만 저장) 구현이 직관적이다. 단점으로는 여러 등급별로 사용량을 다르게 설정한다면 코드 구현이 복잡해질 수 있다.
2. Fixed Window Counter
고정 윈도우 카운터 알고리즘은 시간을 고정된 구간으로 나누고 각 구간마다 요청 횟수를 카운트하는 방식이다. 예를 들어 1시간을 윈도우로 설정하면 매 정시마다 카운터가 리셋된다. 장점은 구현이 매우 간단하고, 메모리 사용이 적으며(카운터 하나만 필요) 이해하기 쉽다. 단점으로는 윈도우 경계에서 심각한 버스트 문제가 발생할 수 있다. 예를 들어 시간당 100회 제한이 있을 때 00:59에 100회, 01:01에 100회 요청하면 2분 동안 200회 처리가 가능해진다.
3. Sliding Window Log
슬라이딩 윈도우 로그 알고리즘은 모든 요청의 타임스탬프를 로그로 저장하고, 현재 시간으로부터 윈도우 크기만큼 이전의 요청들을 카운트하는 방식이다. 새 요청이 올 때마다 오래된 로그를 제거하고 현재 윈도우 내의 요청 개수를 확인한다. 장점은 가장 정확한 제한을 제공하며, 윈도우 경계 문제가 없다. 단점으로는 모든 요청의 타임스탬프를 저장해야 하므로 메모리 사용량이 매우 크고 오래된 로그를 정리하는 연산이 필요하며 대용량 트래픽에서 성능 문제가 발생할 수 있다.
4. Sliding Window Counter
슬라이딩 윈도우 카운터 알고리즘은 고정 윈도우와 슬라이딩 윈도우의 장점을 결합한 방식이다. 이전 윈도우와 현재 윈도우의 카운터를 가중 평균하여 현재 시점의 요청 수를 추정한다. 예를 들어 현재 시간이 윈도우의 30% 지점이라면 이전 윈도우의 70%와 현재 윈도우의 30%를 합산한다. 장점은 메모리 효율적이면서도(두 개의 카운터만 필요) 고정 윈도우의 경계 문제를 완화하고, 슬라이딩 로그보다 훨씬 가볍다. 단점으로는 근사치를 사용하므로 완벽하게 정확하지 않고, 구현이 다소 복잡하며, 엄격한 제한이 필요한 경우 부적합할 수 있다.
결과적으로는 Token Bucket!
제시된 요구사항에 모두 만족하는 알고리즘은 Token Bucket이다. Token Bucket은 일정 속도로 자동 충전되는 개념을 직접적으로 구현하며, 버킷의 최대 용량으로 충전 상한선을 자연스럽게 표현할 수 있다. 메모리 측면에서도 현재 토큰 개수와 마지막 충전 시간 두 개의 값만 저장하면 되므로 효율적이다. Sliding Window Log는 모든 타임스탬프를 보관해야 하므로 메모리 사용량이 큽니다. Fixed Window Counter와 Sliding Window Counter는 충전 개념보다는 제한 개념에 가까워 의도와 맞지 않는다.
'프로젝트' 카테고리의 다른 글
| [바로] 룩 상세 조회 기능 개선 (Ft. Redis 캐싱) (0) | 2025.09.26 |
|---|---|
| [바로] 일괄 주문 기능 개선 Vol.2 (Ft. Kafka, Transactional Outbox) (0) | 2025.09.15 |
| [바로] 스와이프 기능 개선 (Ft. MongoDB, Tomcat Thread 튜닝) (0) | 2025.09.11 |
| [바로] 인기상품조회 기능 개선 (Ft. DB Connection Pool) (0) | 2025.09.05 |
| [바로] Redis의 Lua Script는 Atomic 하지 않다..? (0) | 2025.09.04 |