AWS S3 비용 절감 실전 가이드: Lifecycle, Intelligent-Tiering, 자동 삭제

목차

AWS S3 비용 절감은 단일 옵션이 아니라 "저장 클래스 + Lifecycle 정책 + 객체 위생(housekeeping)"을 묶은 운영 규칙이다. 콘솔에서 버튼 하나 누르는 작업이 아니라, 버킷별 접근 패턴을 분류하고 그에 맞는 클래스로 객체를 흘려보내는 파이프라인 설계에 가깝다. 프론트엔드 2년 하다가 백엔드로 넘어와서 S3를 본격적으로 운영해본 입장에서 정리해두면, 콘솔 UI에 익숙해지는 것보다 "어떤 데이터를 어떤 클래스에 둘지" 판단 기준을 잡는 게 훨씬 비용에 직결된다.

프론트 시절에는 CDN 캐시 hit ratio 정도만 신경 썼다. 백엔드로 넘어오니 같은 정적 자산도 origin인 S3에서 어떻게 보관되고 있는지, 90일 뒤에 누가 정리하는지가 모두 비용으로 돌아온다. 이 글은 신입이 입사 첫 주에 "S3 비용 어떻게 줄여요?"라고 물었을 때 화이트보드에 그려가며 설명할 만한 체크리스트로 구성했다.

왜 S3 비용은 자기도 모르게 새는가

결국, S3 가격표는 GB당 단가만 보면 굉장히 싸 보인다. Standard 클래스가 미국 리전 기준 GB당 월 0.023 USD 수준이라 1TB 넣어도 23달러밖에 안 된다(2026년 5월 기준, ap-northeast-2는 약간 더 비싸다. 출처: AWS S3 공식 요금 페이지). 그런데 실제 청구서를 까보면 스토리지 비용보다 요청 비용, 데이터 전송 비용, 불완전 멀티파트 업로드, 버전 객체가 무섭게 쌓여 있는 경우가 많다.

이처럼, 백엔드 전환 초기에 가장 충격이었던 건 "한 번 업로드된 객체는 누군가가 명시적으로 지우지 않으면 영원히 남는다"는 사실이다. 프론트에서 npm cache는 디스크 꽉 차면 알아서 비우는 데 익숙했는데, S3는 그런 게 없다. 버전 관리 활성화된 버킷에서 같은 키로 100번 PUT 하면 100개 버전이 다 살아 있다. 청구서에 찍히는 건 "현재 객체 1개"가 아니라 "100개 전부"다.

비용이 새는 4가지 경로

  1. 콜드 데이터 방치: 30일 이상 한 번도 안 읽히는 로그/백업이 Standard 클래스에 그대로 있다.
  2. 이전 버전 누적: 버전 관리만 켜두고 만료 정책 없이 운영. 옛 버전이 현재 객체보다 많아진다.
  3. 불완전 멀티파트 업로드: 5GB 넘는 파일 업로드 실패 시 부분 청크가 남는다. 콘솔에 안 보인다.
  4. 요청 패턴 미스매치: Glacier에 넣어놓고 매일 조회하면 복원 요청 비용이 스토리지 비용을 넘긴다.

특히 3번은 안 보이는 비용이다. AWS CLI로 list-multipart-uploads를 돌려보기 전까지는 콘솔 객체 목록에 뜨지 않는다. 한 팀에서 이미지 처리 파이프라인 실패가 누적돼 수 TB가 보이지 않게 쌓여 있던 사례를 본 적이 있다고 들었다.

1단계: 현재 비용 구조 파악하기

비용 절감의 첫 단계는 "줄이기"가 아니라 "보기"다. 측정 안 되는 건 줄일 수도 없다.

Cost Explorer로 큰 그림 보기

한편, 콘솔에서 Cost Explorer → Group by: Usage Type, Filter: Service = S3로 설정하면 비용 항목별 분포가 나온다. 보통 아래 항목들이 상위에 잡힌다.

Usage Type 의미 액션 포인트
TimedStorage-ByteHrs Standard 스토리지 보관 비용 Lifecycle로 클래스 이동
TimedStorage-GIR-ByteHrs Glacier Instant Retrieval 보관 비용 적절히 활용 중인지 확인
Requests-Tier1 PUT/COPY/POST/LIST 요청 멀티파트 정리, 배치 처리
Requests-Tier2 GET/SELECT 요청 캐싱 레이어 추가 검토
DataTransfer-Out-Bytes 외부 송신 트래픽 CloudFront 경유 검토

DataTransfer-Out이 상위에 있으면 사실상 S3 자체 문제가 아니라 CDN 미사용 문제다. 이건 별도 주제라 이 글에서는 다루지 않는다.

S3 Storage Lens 켜기

이처럼, Storage Lens는 계정 전체 S3 사용 패턴을 한 화면에서 보여주는 무료 대시보드다(고급 메트릭은 유료). 무료 티어만 켜도 "버킷별 비현재 버전 비율", "30일 이상 미접근 객체 비율" 같은 핵심 지표가 보인다. 콘솔에서 S3 → Storage Lens → Default dashboard로 들어가면 된다.

# CLI로 활성 상태 확인
aws s3control list-storage-lens-configurations \
  --account-id 123456789012

처음 보면 숫자가 압도적이라 어디부터 손대야 할지 막막한데, Top N 버킷 5개에만 집중하면 80% 이상이 해결된다. 비용은 거의 항상 멱법칙 분포다.

2단계: Lifecycle 정책 설계

물론, Lifecycle은 "객체가 N일 지나면 더 싼 클래스로 옮기거나 삭제하라"는 규칙이다. 버킷마다 다르게 걸 수 있고, prefix나 태그로 범위를 좁힐 수도 있다. 이게 S3 비용 절감의 핵심 도구다.

저장 클래스 빠른 비교

저장 클래스가 너무 많아서 처음에는 헷갈린다. 핵심만 추리면 이렇다.

클래스 GB/월 단가(대략) 최소 보관 기간 검색 비용 적합한 데이터
Standard 0.023 USD 없음 없음 매일 읽히는 운영 데이터
Standard-IA 0.0125 USD 30일 GB당 부과 월 1~2회 읽히는 백업
One Zone-IA 0.01 USD 30일 GB당 부과 재생성 가능한 캐시
Glacier Instant Retrieval 0.004 USD 90일 GB당 부과 분기 1회 보는 아카이브
Glacier Flexible Retrieval 0.0036 USD 90일 분~시간 단위 연 1회 보는 컴플라이언스
Glacier Deep Archive 0.00099 USD 180일 12시간 이상 7년 보관 법적 의무

(2026년 5월 기준, us-east-1. 출처: AWS S3 Storage Classes 문서. 리전별·시점별 가격은 다르다.)

여기서 흔히 빠지는 함정이 "쌀수록 좋다"는 가정이다. Standard-IA로 옮긴 객체를 매일 읽으면 검색 비용이 보관 절감분을 넘긴다. 접근 빈도를 모르면 Intelligent-Tiering이 정답인 이유다.

Lifecycle JSON 예시

즉, 콘솔에서 만들어도 되지만 IaC로 관리하려면 JSON이 결국 필요하다. 30일 후 Standard-IA, 90일 후 Glacier Instant, 365일 후 삭제, 그리고 불완전 멀티파트는 7일 후 정리하는 정책 예시다.

{
  "Rules": [
    {
      "ID": "logs-archival-policy",
      "Status": "Enabled",
      "Filter": { "Prefix": "logs/" },
      "Transitions": [
        { "Days": 30,  "StorageClass": "STANDARD_IA" },
        { "Days": 90,  "StorageClass": "GLACIER_IR" }
      ],
      "Expiration": { "Days": 365 },
      "AbortIncompleteMultipartUpload": {
        "DaysAfterInitiation": 7
      },
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 30,
        "NewerNoncurrentVersions": 3
      }
    }
  ]
}
# 적용
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-app-logs \
  --lifecycle-configuration file://lifecycle.json

# 확인
aws s3api get-bucket-lifecycle-configuration \
  --bucket my-app-logs

NewerNoncurrentVersions: 3은 "최근 비현재 버전 3개는 남기고 나머지 30일 지나면 삭제"라는 뜻이다. 프론트에서 git stash 쌓이는 것처럼, S3 버전도 무한히 쌓이게 두지 말고 적당한 윈도우를 잡는 게 좋다.

prefix와 태그를 함께 쓰는 패턴

전체 버킷에 일괄 정책을 거는 건 위험하다. logs/, uploads/, tmp/처럼 prefix를 분리하고, 그래도 부족하면 객체 태그(retention=short)로 좁힌다. 한 버킷에 운영 데이터와 임시 데이터가 섞여 있는데 일괄 만료 정책을 걸면 살아 있어야 할 객체가 같이 사라진다.

3단계: Intelligent-Tiering 적용 시점

접근 패턴이 변동적이거나 알 수 없을 때 가장 안전한 선택이다. AWS가 객체 단위로 30일 미접근 시 자동으로 IA 계층으로, 90일 미접근 시 Archive Instant Access로 옮긴다. 검색 비용이 0이라 잘못 옮겨도 손해 없다.

다만 알아야 할 비용

객체당 월 모니터링 수수료가 붙는다. 작은 객체(128KB 이하)는 모니터링 대상이 아니라 항상 빠른 계층에 남는다. 그래서 객체 수가 수억 개 이상이고 평균 크기가 작으면 모니터링 비용이 절감분을 깎아먹을 수 있다. 객체 수 × 0.0025 USD/1000개 정도를 계산해보고 결정하자(2026년 5월 기준).

# 버킷 기본 클래스를 Intelligent-Tiering으로 설정하는 건 불가능
# 대신 Lifecycle로 0일째 전환
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-uploads \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "to-intelligent",
      "Status": "Enabled",
      "Filter": {},
      "Transitions": [{
        "Days": 0,
        "StorageClass": "INTELLIGENT_TIERING"
      }]
    }]
  }'

그런데, 업로드 시점에 클래스를 지정하는 방식도 있다. SDK 호출에 StorageClass: 'INTELLIGENT_TIERING'을 붙이면 처음부터 그 클래스로 들어간다. 신규 객체는 SDK로, 기존 객체는 Lifecycle로 처리하면 깔끔하다.

Intelligent-Tiering이 안 맞는 경우

  • 객체가 100KB 미만이고 수억 개: 모니터링 수수료 부담
  • 접근 패턴이 명확히 규칙적: Lifecycle 직접 거는 게 더 저렴
  • 컴플라이언스로 특정 클래스 강제: 선택지 없음

4단계: 불필요 객체 자동 삭제

남기는 것보다 지우는 게 비용 절감에 직접적이다. 그러나 운영 중인 객체를 잘못 지우면 사고다. 자동 삭제는 항상 테스트 → 격리 → 적용 순으로 간다.

불완전 멀티파트 업로드 정리

실제로, 가장 먼저 손대야 할 항목이다. 위 Lifecycle JSON의 AbortIncompleteMultipartUpload만 걸어도 즉시 효과가 보인다. 현재 쌓여 있는 양은 이렇게 확인한다.

aws s3api list-multipart-uploads \
  --bucket my-bucket \
  --query 'length(Uploads)'

게다가, 쌓여 있는 게 많으면 수동으로 한 번 청소한다.

# 7일보다 오래된 멀티파트만 골라 abort
aws s3api list-multipart-uploads --bucket my-bucket \
  --query 'Uploads[?Initiated<=`2026-05-23`].[Key,UploadId]' \
  --output text | while read key upload_id; do
    aws s3api abort-multipart-upload \
      --bucket my-bucket \
      --key "$key" \
      --upload-id "$upload_id"
done

특히, 처음 돌릴 때는 --dry-run 옵션이 없으니 list만 먼저 돌려서 건수를 확인하고 진행한다.

비현재 버전 정리

그러나, 버전 관리가 켜져 있다면 비현재 버전이 얼마나 쌓였는지 확인한다.

aws s3api list-object-versions \
  --bucket my-bucket \
  --query 'sum(Versions[?IsLatest==`false`].Size)' \
  --output text

그런데, 이 숫자가 현재 버전 총합보다 크다면 정리 효과가 매우 크다. Lifecycle NoncurrentVersionExpiration을 30일로 걸어두면 시간이 알아서 청소해준다.

빈 prefix와 0바이트 객체

콘솔에서 폴더처럼 보이는 0바이트 객체가 종종 만들어진다. 직접적인 비용은 거의 없지만 LIST 요청 비용을 올린다. 정기적으로 inventory 리포트를 보고 정리한다.

5단계: S3 Inventory와 자동화

매번 CLI로 손으로 확인할 수는 없다. S3 Inventory를 일일 또는 주간으로 켜두면 객체 목록과 메타데이터가 Parquet/CSV로 떨어진다. Athena로 쿼리하면 "30일 이상 미접근 객체 총 용량" 같은 게 한 줄로 나온다.

-- Athena에서 inventory 테이블 쿼리
SELECT storage_class,
       COUNT(*) AS objects,
       SUM(size) / 1024 / 1024 / 1024 AS gb
FROM   s3_inventory
WHERE  last_modified_date < date_add('day', -90, current_date)
GROUP  BY storage_class
ORDER  BY gb DESC;

실제로, 이 결과를 월 1회 Slack으로 받게 EventBridge → Lambda → Slack 웹훅 체인을 걸어두면 운영 부담이 거의 0이 된다. 프론트 쪽에서 Sentry 알림 받는 감각이랑 비슷하다고 보면 된다.

6단계: 주의사항과 흔한 실수

실제로, 비용 절감을 너무 공격적으로 하다가 사고 나는 경우가 더 많다. 아래는 실제 운영에서 자주 본 함정들이다.

  • Glacier에 핫 데이터 넣기: 자주 읽는 데이터를 Glacier로 옮기면 복원 비용이 보관 절감분의 수십 배가 된다. 접근 빈도 확인이 우선이다.
  • 만료 정책을 prefix 없이 걸기: 버킷 전체에 365일 만료를 걸어두고 운영 데이터가 사라진 사례를 종종 본다. 항상 prefix나 태그로 범위를 좁혀라.
  • 버전 관리 끄기: 버전 비용이 무서워서 버전 관리를 꺼버리면 실수 복구 수단이 사라진다. 끄는 게 아니라 NoncurrentVersionExpiration으로 윈도우를 잡는 게 맞다.
  • CRR/SRR 복제 비용 누락: 복제 활성화된 버킷의 Lifecycle을 바꾸면 복제 대상 버킷에서 다른 동작이 일어난다. 양쪽 정책을 같이 본다.
  • Requester Pays 미설정: 외부 팀이 자주 GET 하는 데이터셋이면 Requester Pays로 비용 책임을 분리하는 게 깔끔하다.

운영 중인 버킷에 처음 Lifecycle을 거는 건 30일 후 IA 전환처럼 부드러운 규칙부터 시작한다. Expiration은 한 달 정도 inventory를 보고 영향 범위 확인한 뒤에 붙인다.

언제 어떤 전략을 쓸지 판단 기준

물론, 지금까지 도구는 다 봤다. 현장에서 결정할 때 쓰는 기준은 이렇다.

  • 접근 패턴이 명확히 규칙적이라면 Lifecycle 직접 설계가 맞다. 로그처럼 N일 후 거의 안 보는 게 확실하면 IA → Glacier 순으로 흘려보낸다.
  • 접근 패턴을 모르거나 변동적이라면 Intelligent-Tiering이 맞다. 사용자 업로드, 미디어, 분석 결과물처럼 누가 언제 볼지 모르는 객체가 여기 해당한다.
  • 객체가 너무 작고 너무 많다면(평균 100KB 미만, 수억 개) Lifecycle만 쓰는 게 맞다. Intelligent-Tiering 모니터링 수수료가 절감분을 깎는다.
  • 법적 보관 의무가 있다면 Glacier Deep Archive + Object Lock이 맞다. 다른 클래스로 가면 보관 단가가 손해다.

특히, 지금 당장 실행할 수 있는 액션 세 가지로 마무리한다.

  1. 모든 운영 버킷에 AbortIncompleteMultipartUpload: 7일 Lifecycle 규칙을 추가한다. 부작용 없고 비용은 즉시 줄어든다.
  2. Storage Lens 무료 대시보드를 켜고 비용 상위 5개 버킷을 추린다. 여기서부터 손대면 효율이 가장 좋다.
  3. 버전 관리 켜진 버킷에 NoncurrentVersionExpiration 30일 + NewerNoncurrentVersions 3개 규칙을 건다. 비현재 버전 누적이 멈춘다.

AWS S3 비용 절감은 한 번에 끝나는 작업이 아니라 분기마다 inventory를 보면서 정책을 조정하는 운영 사이클이다. 처음 한 번만 제대로 구조를 잡아두면 그 뒤로는 알아서 굴러간다.

관련 글