S3 요금 청구서를 처음 뜯어보면 대부분 놀랍니다. 저장 용량보다 요청·검색 요금이 생각보다 크고, 몇 년째 아무도 읽지 않는 오래된 객체가 여전히 표준 클래스에서 최고 단가로 과금되고 있기 때문입니다.
S3 비용 최적화는 결국 “각 객체를 실제 접근 패턴에 맞는 스토리지 클래스에 올려두는” 문제입니다. 이 글에서는 요금 구조를 짚고, 라이프사이클로 자동 전환·만료시키는 법, 접근 패턴을 예측하기 어려울 때 쓰는 Intelligent-Tiering, 전환이 손해가 되는 함정과 비용 측정 방법을 다룹니다.
S3 요금은 저장 용량만이 아니다
저장 단가만 보고 클래스를 고르면 오히려 전환·검색 요금 때문에 총비용이 늘어나는 역설이 생깁니다. 저장 요금(GB-월) 외에 아래 축을 함께 봐야 합니다.
- 요청 요금: PUT/LIST 와 GET 이 별도 단가. 저비용 클래스일수록 높습니다.
- 검색 요금: Glacier 계열은 데이터를 꺼낼 때 GB당 요금이 따로 붙습니다.
- 전환 요금: 라이프사이클로 클래스를 바꿀 때 객체 1,000개당 발생합니다.
여기서 핵심은 최소 저장 기간과 객체당 오버헤드입니다. Standard-IA 는 최소 30일, Deep Archive 는 180일이 있어 그 전에 지우거나 옮기면 남은 기간만큼 청구됩니다. 또 IA·Glacier 계열은 객체당 최소 128 KB를 청구하고, 아카이브 계열은 메타데이터 오버헤드가 붙습니다. 작은 파일이 수천만 개라면 이 오버헤드만으로 청구서가 뒤집힙니다.
스토리지 클래스 지도 그리기
클래스는 접근 빈도, 가용 영역 수(다중 AZ / 단일 AZ), 검색 지연(즉시 / 분·시간)의 세 축으로 이해하면 됩니다.
- S3 Standard: 자주 접근하는 데이터. 최소 기간·검색 요금 없음. 전환 판단의 기준선.
- Standard-IA: 한 달에 몇 번만 읽는 데이터. 저장 단가는 낮지만 GB당 검색 요금 있음.
- One Zone-IA: IA 와 같지만 단일 AZ 저장. 더 싼 대신 AZ 장애 시 유실 위험. 재생성 가능한 데이터에만.
- Glacier Instant Retrieval: 드물게 읽지만 읽을 때는 즉시 필요한 데이터.
- Glacier Flexible / Deep Archive: 몇 분~수 시간(Deep 은 최대 12시간) 대기 가능한 아카이브. 최저 단가.
판단의 출발점은 “얼마나 자주, 얼마나 빨리 읽어야 하는가”입니다. 접근 빈도가 낮을수록 저장은 싸지지만 읽기가 비싸고 느려지므로, 실제 빈도를 모른 채 무작정 아카이브로 내리면 검색 요금 폭탄을 맞습니다.
라이프사이클 규칙의 기본 구조
라이프사이클(Lifecycle) 규칙은 객체의 나이(생성 후 경과일)를 기준으로 클래스를 자동 전환하거나 만료(삭제)시키는 정책입니다. 대표 패턴은 “일정 기간 지나면 IA, 더 지나면 Glacier, 아주 오래되면 삭제”하는 계단식 전환입니다.
{
"Rules": [
{
"ID": "logs-tiering",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER_IR" },
{ "Days": 365, "StorageClass": "DEEP_ARCHIVE" }
],
"Expiration": { "Days": 2555 }
}
]
}
가장 중요한 건 Filter입니다. 접두사·태그와 함께 크기 범위로도 대상을 좁힐 수 있습니다. 버킷 전체에 뭉뚱그려 걸면 “작은 객체를 IA 로 내려 오히려 손해 보는” 상황이 생기므로, Filter 안에 ObjectSizeGreaterThan: 131072(128 KB)를 넣어 최소 청구 크기보다 작은 객체를 전환에서 제외하는 것이 중요합니다.
계단식 전환의 손익분기
전환은 공짜가 아닙니다. 전환은 객체 1,000개당 요청 요금이 붙고 아카이브 계열로 갈수록 비쌉니다. 핵심 질문은 전환 비용을 저장 단가 차이로 회수하는 데 며칠이 걸리는가입니다.
def transition_breakeven_days(
object_count: int, # 전환할 객체 수
avg_size_gb: float, # 객체 평균 크기(GB)
src_price_per_gb: float, # 현재 클래스 GB-월 단가(USD)
dst_price_per_gb: float, # 목적지 클래스 GB-월 단가(USD)
transition_price_per_1k: float, # 1,000개당 전환 요청 요금
) -> float:
# 일회성 전환 비용
transition_cost = (object_count / 1000) * transition_price_per_1k
total_gb = object_count * avg_size_gb
# 하루당 절감액 = 월단가 차이 / 30일
daily_saving = total_gb * (src_price_per_gb - dst_price_per_gb) / 30
if daily_saving <= 0:
return float("inf") # 저장 단가가 더 비싸면 전환 무의미
return transition_cost / daily_saving
이 계산은 검색 요금을 뺀 보수적 추정입니다. 실제로는 전환 후 데이터를 얼마나 다시 읽을지가 결정적입니다. 손익분기가 며칠 나오더라도 전환 후 자주 읽는다면 검색 요금이 절감액을 잠식하므로, 접근 로그를 반드시 확인해야 합니다.
버전 관리·미완료 멀티파트 정리
가장 흔하면서도 놓치기 쉬운 낭비가 둘 있습니다. 버전 관리가 켜진 버킷에서 삭제된 객체의 과거 버전(noncurrent version)이 계속 쌓이는 것, 그리고 실패한 멀티파트 업로드 조각이 목록에 안 보이면서 과금되는 것입니다.
{
"ID": "cleanup-noncurrent-and-incomplete",
"Filter": {},
"Status": "Enabled",
"NoncurrentVersionExpiration": {
"NoncurrentDays": 90,
"NewerNoncurrentVersions": 3
},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 7
}
}
NewerNoncurrentVersions는 최신 3개 버전은 남기고 오래된 것만 만료시키는 안전장치입니다. AbortIncompleteMultipartUpload는 거의 모든 프로덕션 버킷에 넣어야 하는 규칙으로, 완료되지 않은 조각을 자동 정리해 보이지 않는 저장 요금을 막습니다. 지금 쌓인 조각은 아래로 확인합니다.
# 미완료 멀티파트 업로드 조회 (보이지 않는 과금의 주범)
aws s3api list-multipart-uploads --bucket my-bucket
--query 'Uploads[].{Key:Key,Initiated:Initiated}' --output table
# 특정 조각 수동 중단
aws s3api abort-multipart-upload --bucket my-bucket
--key path/to/object --upload-id "EXAMPLE_ID"
Intelligent-Tiering: 접근 패턴을 모를 때
라이프사이클은 “나이 기반”이라, 접근 패턴이 나이와 무관하게 들쭉날쭉한 데이터에는 잘 맞지 않습니다. 오래된 객체가 갑자기 다시 뜨거워지는 미디어 라이브러리나 사용자 업로드가 그렇습니다. 이때 S3 Intelligent-Tiering은 객체별 접근을 모니터링해 자동으로 티어를 오르내립니다. 30일 연속 미접근이면 Infrequent Access 로 내리고, 다시 접근되면 자동으로 Frequent 로 올립니다. 옵트인하면 Archive·Deep Archive 티어까지 확장됩니다. 검색 요금이 없고 자동 승격되므로, 접근 예측이 어려운 워크로드의 안전한 기본값입니다.
# Terraform: 버킷에 Intelligent-Tiering 아카이브 티어 옵트인
resource "aws_s3_bucket_intelligent_tiering_configuration" "media" {
bucket = aws_s3_bucket.media.id
name = "media-archive-config"
filter { prefix = "user-uploads/" }
tiering {
access_tier = "ARCHIVE_ACCESS" # 90일 미접근 시
days = 90
}
tiering {
access_tier = "DEEP_ARCHIVE_ACCESS" # 180일 미접근 시
days = 180
}
}
주의할 점이 있습니다. Intelligent-Tiering 은 객체당 소액의 모니터링 요금이 붙고, 128 KB 미만 객체는 이 요금이 없는 대신 티어링 대상에서도 제외됩니다. 따라서 대부분이 128 KB보다 작은 초소형 객체 버킷이라면 이득이 거의 없습니다.
전환의 함정: 언제 손해가 나는가
무작정 저비용 클래스로 내리는 것은 흔한 실수입니다. 전환이 오히려 손해가 되는 대표적 함정은 다음과 같습니다.
- 작은 객체 전환: 실제 20 KB여도 128 KB로 청구됩니다. 크기 필터 없이 대량 전환하면 저장 요금이 되레 늘 수 있습니다.
- 최소 저장 기간 전 삭제: 로그처럼 짧게 보관 후 지우는 데이터를 Deep Archive(180일)로 내리면, 삭제해도 남은 기간이 청구됩니다.
- 전환 후 잦은 읽기: IA·Glacier 는 GET·검색 단가가 높아, 다시 자주 읽으면 절감액이 사라집니다.
- 중복 전환 요금: Standard→IA→Glacier 로 두 번 전환하면 요금도 두 번. 패턴이 확실하면 곧장 목적지로 보내는 게 쌉니다.
정리하면, 전환 결정은 저장 절감액에서 전환 요금 + 예상 검색 요금 + 최소 기간 리스크를 뺀 순이익으로 판단합니다. 접근 로그가 없다면 함부로 아카이브로 내리기보다 Intelligent-Tiering 에 맡기는 편이 안전합니다.
비용을 실제로 측정하는 법
최적화는 측정 없이 성립하지 않습니다. S3 Storage Lens는 버킷·접두사 단위로 클래스별 용량 분포와 미완료 멀티파트, 비현행 버전 비율 같은 낭비 지표를 보여줍니다. S3 Inventory는 모든 객체의 크기·클래스·수정일 목록을 정기 출력해 주고, 이를 Athena 로 질의하면 정확한 전환 후보를 뽑을 수 있습니다.
-- Standard 에서 90일 이상 수정 안 된 대용량 객체 = 전환 1순위 후보
SELECT
count(*) AS object_count,
round(sum(size) / 1073741824.0, 2) AS total_gib
FROM s3_inventory
WHERE storage_class = 'STANDARD'
AND last_modified_date 131072; -- 128KB 미만 소형 객체 제외
여기에 태그 기반 비용 배분을 더합니다. 버킷에 team 태그를 붙이고 Cost Allocation Tag 를 활성화하면 Cost Explorer 에서 팀별 비용을 분리해 볼 수 있습니다. “누구의 데이터가 돈을 쓰는가”가 보이면 우선순위가 명확해집니다.
마무리: 실전 적용 순서
S3 비용 최적화의 본질은 단가가 싼 클래스로 무조건 내리는 것이 아니라 각 데이터의 실제 접근 패턴에 맞는 클래스에 배치하는 것입니다. 측정 없이 정책부터 거는 것이 가장 흔한 실패 원인이므로, 아래 순서를 지키세요.
- 측정 먼저: Storage Lens 로 낭비 지표(미완료 멀티파트, 비현행 버전, 미접근 대용량)를 확인한다.
- 즉시 이득부터: 모든 버킷에
AbortIncompleteMultipartUpload와 비현행 버전 만료 규칙을 넣는다. 리스크 없이 바로 절감된다. - 크기 필터로 안전하게: 전환 규칙에는 항상
ObjectSizeGreaterThan을 128 KB 이상으로 둔다. - 패턴이 명확하면 라이프사이클, 불명확하면 Intelligent-Tiering에 맡긴다.
- Inventory + Athena 로 검증: 클래스 분포가 의도대로 바뀌었는지, 검색 요금이 튀지 않는지 확인한다.
자주 묻는 질문
Q. 라이프사이클과 Intelligent-Tiering 을 같은 버킷에 함께 쓸 수 있나요?
A. 가능하지만 대상이 겹치지 않게 접두사·태그로 분리해야 합니다. 나이와 접근이 비례하는 접두사(logs/)는 라이프사이클로, 예측이 어려운 접두사(user-uploads/)는 Intelligent-Tiering 으로 나누세요. 한 객체에 둘의 규칙이 동시에 적용되면 전환 요금이 중복될 수 있습니다.
Q. 작은 파일이 수천만 개인데 저장 요금을 줄이려면 어떻게 하나요?
A. 이 경우 클래스 전환은 효과가 없거나 역효과입니다. IA·Glacier 의 128 KB 최소 청구와 오버헤드 때문입니다. 근본 해법은 작은 파일들을 하나의 큰 객체로 병합·아카이빙하는 것입니다. 일별 로그를 gzip·tar 로 묶어 대용량 객체로 만든 뒤 전환하면 객체 수가 줄어 요금이 급감합니다.
Q. Deep Archive 로 내렸다가 급히 데이터가 필요해지면 얼마나 걸리나요?
A. Deep Archive 는 Standard 검색이 최대 12시간, Bulk 는 최대 48시간까지 걸려, 즉시성이 조금이라도 필요하면 부적합합니다. “몇 시간 내 접근 가능성이 있다”면 Glacier Instant Retrieval 이나 Intelligent-Tiering 의 Archive Instant Access 티어를 고려하세요. Deep Archive 는 법적 보관 의무나 최후의 백업처럼 거의 확실히 다시 읽지 않는 데이터에만 적합합니다.