멀티파트 업로드 핵심 정리 — S3 성능 3편

2026-05-02AWS SAA-C03 스터디

Amazon S3 심화 정리 3편. 큰 파일을 박스로 쪼개 트럭 여러 대로 보내는 비유로 멀티파트 업로드를 풀고 — Transfer Acceleration의 엣지 로케이션 활용, Byte-Range Fetch 병렬 다운로드, S3 Select 서버 측 SQL 필터, Batch Operations 대량 작업, 요청률 한계와 prefix 분산까지 친절하게 정리.

📚 Amazon S3 심화 정리 · 3편 / 14편 — S3 성능 3편

이 글은 Amazon S3 심화 정리 시리즈의 세 번째 편입니다. 1편에서는 S3가 무엇인지, 2편에서는 누가 들어올 수 있는지를 풀었어요. 이번 편은 속도입니다 — 큰 파일을 빠르게 올리고, 멀리 있는 사용자도 빠르게 받게 하고, 수백만 객체에 한 번에 작업을 거는 방법. 그 출발점에 멀티파트 업로드가 있어요. 큰 파일을 한 번에 못 올리는 한계를 푸는 가장 기본 도구라, 이번 글의 절반은 멀티파트 업로드를 어떻게 다루는지에 쓰여요.

처음 보면 용어가 낯설지만 비유로 풀면 거의 다 같은 그림이에요. 큰 짐을 박스로 쪼개 여러 트럭에 동시에 싣기(멀티파트 업로드), 동네 임시 매장에서 본사로 빠르게 보내기, 큰 책을 여러 사람이 챕터별로 동시에 읽기. 이 그림 세 개면 절반은 끝납니다. 공식 사양은 S3 사용자 가이드의 성능 단원에 자세히 정리돼 있어요.

왜 이 단원이 처음엔 어렵게 느껴질까요

이유는 두 가지예요.

첫째, 숫자가 많이 나옵니다. 5MB·5GB·5TB·100MB·160GB·3,500·5,500·10,000·90일·180일 — 외울 수치가 줄지어 등장합니다. 처음 보면 어느 게 한도이고 어느 게 권장인지 헷갈려요.

둘째, 비슷해 보이는 기능이 여러 개 등장합니다. 멀티파트 업로드와 Transfer Acceleration이 둘 다 "큰 파일을 빠르게"인데 어느 자리에 어떤 걸 써야 하는지가 안 잡혀요. 병렬 다운로드(Byte-Range)와 S3 Select도 "빠르게 가져오기"인데 작동 방식이 완전히 달라서 처음엔 같은 칸에 던져 두기 쉽습니다.

해결법은 한 가지예요. 각 기능이 풀려고 하는 문제가 무엇인지 한 줄씩 잡아 두면 갑자기 정리가 됩니다. 멀티파트 업로드는 "큰 파일을 한 번에 못 올린다"를 풀고, Transfer Acceleration은 "거리가 멀다"를 풀고, Byte-Range는 "다운로드도 병렬화하고 싶다"를 풀고, S3 Select는 "필요한 줄만 갖고 싶다"를 풉니다. 문제가 다르면 도구도 다른 거예요. 이 글은 멀티파트 업로드를 가장 길게 풀고, 나머지는 그 토대 위에 한 칸씩 더 얹는 방식으로 진행해요.

S3 성능 — 한도부터 알고 갑시다

본격적으로 들어가기 전에 S3가 어디까지 처리해 줄 수 있는지를 알아야 합니다. S3는 사실 어마어마하게 빠른 서비스예요. 다만 한도가 있고, 그 한도에 닿는 순간부터 설계가 필요합니다.

S3는 접두사(prefix) 단위로 요청 처리량 한도를 가져요. 한 prefix당 다음 수치까지 즉시 처리됩니다.

  • PUT / COPY / POST / DELETE — 초당 3,500 요청
  • GET / HEAD — 초당 5,500 요청

여기서 시험 함정이 하나 있어요. 이 한도는 버킷 전체가 아니라 prefix별입니다. 한 prefix에 트래픽이 집중되면 그 prefix만 한도에 닿고, 다른 prefix는 멀쩡합니다. 그래서 prefix를 잘게 쪼개기만 해도 처리량이 그대로 배가 돼요. 뒤에서 prefix 분산을 다룰 때 자세히 풀어 갑니다.

회사 비유로 — prefix 한 개가 창구 한 개예요. 창구당 분당 처리 인원에 한도가 있어요. 손님이 한 창구에만 줄 서면 그 창구가 막힙니다. 창구를 여러 개로 늘리면 분산되죠. S3는 창구를 무한히 늘릴 수 있는 시스템이라, 설계만 잘 하면 처리량은 거의 무한입니다.

성능을 떨어뜨리는 흔한 원인 네 가지를 정리하면 이래요.

  • 단일 prefix에 요청 집중 — 창구 하나에만 줄 선 상태
  • 대용량 파일을 한 번에 올리고 받음 — 트럭 한 대로 큰 짐을 옮기는 식
  • 클라이언트와 S3 리전 사이의 지리적 거리 — 서울에서 미국 동부 버킷까지 데이터가 바다를 건넘
  • 불완전한 멀티파트 업로드 누적 — 중단된 업로드가 보이지 않는 곳에서 비용 발생

이 네 가지를 풀어 가는 도구들이 — 멀티파트 업로드, Transfer Acceleration, Byte-Range Fetch, prefix 분산, S3 Select, Batch Operations입니다. 하나씩 풀어 갈게요.

> 한 줄 정리 — S3는 prefix당 PUT 3,500 / GET 5,500까지 즉시 처리. 한도에 닿으면 prefix를 쪼갠다.

멀티파트 업로드 — 큰 짐을 박스로 쪼개기

대용량 파일을 올리는 가장 기본 도구입니다. 회사 비유로 — 트럭 한 대로 옮기기 어려운 큰 짐을 여러 작은 박스로 쪼개서 트럭 여러 대로 동시에 보내고, 도착해서 다시 합치는 식이에요.

원본 그대로 합치는 게 핵심입니다. 분할은 단지 운송 효율을 위한 것이고, 도착하면 S3가 자동으로 한 객체로 합쳐 줘요. 사용자가 받았을 때는 원본 파일 하나입니다.

왜 멀티파트가 빠른가

장점이 두 가지예요.

  • 병렬 업로드 — 여러 파트를 동시에 전송. 트럭 10대가 동시에 달리면 1대가 10번 왕복하는 것보다 훨씬 빨라요
  • 재시작 용이 — 네트워크가 중간에 끊겨도 실패한 파트만 다시 보내면 됨. 처음부터 다시 안 해도 돼요

여기서 시험 함정이 하나 있어요. 5GB를 넘는 단일 객체는 멀티파트가 필수입니다. 단일 PUT 요청은 5GB까지밖에 못 올려요. 그 이상은 멀티파트 외에는 방법이 없습니다.

권장 사용 기준

파일 크기권장 방법
100MB 미만단일 PUT 업로드
100MB ~ 5GB멀티파트 업로드 권장
5GB 초과멀티파트 업로드 필수

파트 규칙 — 외워둘 숫자들

이 부분에 시험 출제 단골 함정이 모여 있어요.

  • 파트 크기 — 5MB ~ 5GB (마지막 파트 제외)
  • 마지막 파트 — 5MB 미만 가능
  • 최대 파트 수 — 10,000개
  • 최대 객체 크기 — 5TB

여기서 시험 함정이 하나 있어요. 마지막 파트만 5MB 미만이 허용됩니다. 중간 파트는 모두 5MB 이상이어야 해요. 그리고 파트 수가 10,000개를 넘을 수 없으니 — 5TB 객체라면 한 파트 평균 약 524MB 정도가 나옵니다. 너무 작게 쪼개면 10,000개에 막혀요.

작동 흐름

1. CreateMultipartUpload → UploadId 획득
2. UploadPart (각 파트 순번대로, 병렬 가능)
3. CompleteMultipartUpload → 파트 병합
(또는 AbortMultipartUpload → 업로드 취소)

흐름은 세 단계예요. 시작 → 파트별 업로드 → 완료. 중간에 잘못되면 Abort로 취소. 보통 CLI/SDK가 이걸 알아서 처리해 줘서 우리가 직접 칠 일은 많지 않아요.

CLI로 멀티파트 업로드

# 방법 1: aws s3 cp (자동으로 멀티파트 처리)
aws s3 cp large-file.zip s3://my-bucket/

# 다중 파트 업로드 임계값 설정
aws configure set default.s3.multipart_threshold 64MB
aws configure set default.s3.multipart_chunksize 16MB

# 방법 2: aws s3api 사용 (수동)
# 1단계: 업로드 시작
UPLOAD_ID=$(aws s3api create-multipart-upload \
  --bucket my-bucket \
  --key large-file.zip \
  --query 'UploadId' \
  --output text)

echo "Upload ID: $UPLOAD_ID"

# 2단계: 파일 분할 및 파트 업로드
split -b 100m large-file.zip part_
# part_aa, part_ab, part_ac ...

# 파트 1 업로드
ETAG1=$(aws s3api upload-part \
  --bucket my-bucket \
  --key large-file.zip \
  --part-number 1 \
  --body part_aa \
  --upload-id $UPLOAD_ID \
  --query 'ETag' \
  --output text)

# 파트 2 업로드
ETAG2=$(aws s3api upload-part \
  --bucket my-bucket \
  --key large-file.zip \
  --part-number 2 \
  --body part_ab \
  --upload-id $UPLOAD_ID \
  --query 'ETag' \
  --output text)

# 3단계: 멀티파트 업로드 완료
aws s3api complete-multipart-upload \
  --bucket my-bucket \
  --key large-file.zip \
  --upload-id $UPLOAD_ID \
  --multipart-upload '{
    "Parts": [
      {"ETag": "'$ETAG1'", "PartNumber": 1},
      {"ETag": "'$ETAG2'", "PartNumber": 2}
    ]
  }'

# 중단 (실패 시)
aws s3api abort-multipart-upload \
  --bucket my-bucket \
  --key large-file.zip \
  --upload-id $UPLOAD_ID

대부분은 첫 번째 방법(aws s3 cp)만 쓰면 끝이에요. CLI가 임계값을 보고 알아서 멀티파트로 전환합니다. 두 번째 방법은 SDK로 직접 제어할 일이 있을 때 참고용.

Python SDK 예시

import boto3
import os
from math import ceil

def multipart_upload(file_path, bucket_name, object_key, part_size_mb=100):
    s3_client = boto3.client('s3')
    part_size = part_size_mb * 1024 * 1024  # 100MB
    
    # 멀티파트 업로드 시작
    response = s3_client.create_multipart_upload(
        Bucket=bucket_name,
        Key=object_key
    )
    upload_id = response['UploadId']
    
    parts = []
    
    try:
        file_size = os.path.getsize(file_path)
        total_parts = ceil(file_size / part_size)
        
        with open(file_path, 'rb') as f:
            for part_number in range(1, total_parts + 1):
                data = f.read(part_size)
                
                response = s3_client.upload_part(
                    Bucket=bucket_name,
                    Key=object_key,
                    PartNumber=part_number,
                    UploadId=upload_id,
                    Body=data
                )
                
                parts.append({
                    'ETag': response['ETag'],
                    'PartNumber': part_number
                })
                print(f"Uploaded part {part_number}/{total_parts}")
        
        # 업로드 완료
        s3_client.complete_multipart_upload(
            Bucket=bucket_name,
            Key=object_key,
            UploadId=upload_id,
            MultipartUpload={'Parts': parts}
        )
        print("Multipart upload completed!")
        
    except Exception as e:
        # 업로드 취소
        s3_client.abort_multipart_upload(
            Bucket=bucket_name,
            Key=object_key,
            UploadId=upload_id
        )
        raise e

# 사용 예시
multipart_upload('large-video.mp4', 'my-bucket', 'videos/large-video.mp4')

try / except에서 실패 시 abort_multipart_upload를 부르는 부분이 중요해요. 안 부르면 파트가 어딘가에 남아 비용이 계속 발생합니다.

불완전한 멀티파트 업로드 — 보이지 않는 비용

여기서 시험 함정이 하나 있어요. 중간에 실패한 멀티파트 업로드는 자동으로 사라지지 않습니다. 업로드가 도중에 끊기면 이미 보낸 파트가 S3 어딘가에 남아 있어요. 콘솔의 객체 목록에는 안 보이는데 — 저장 비용은 계속 발생합니다.

이걸 자동 정리하는 가장 깨끗한 방법이 수명 주기 정책으로 N일 지난 불완전 업로드를 일괄 중단하는 거예요. 아래 정책은 7일 이상 된 미완료 업로드를 자동으로 abort합니다.

# 불완전한 멀티파트 업로드 목록 조회
aws s3api list-multipart-uploads --bucket my-bucket

# 수명 주기 정책으로 자동 정리
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-bucket \
  --lifecycle-configuration '{
    "Rules": [
      {
        "ID": "CleanIncompleteUploads",
        "Status": "Enabled",
        "Filter": {"Prefix": ""},
        "AbortIncompleteMultipartUpload": {
          "DaysAfterInitiation": 7
        }
      }
    ]
  }'

실무 권장은 모든 버킷에 이 규칙을 기본으로 걸어 두는 것입니다. 비용 누수를 막는 보험이라고 생각하면 돼요.

> 한 줄 정리 — 100MB 이상은 멀티파트 권장, 5GB 초과는 필수. 파트 5MB~5GB·최대 10,000개·5TB 한도. 미완료 업로드는 라이프사이클로 자동 정리.

Transfer Acceleration — 동네 임시 매장에서 본사로

큰 짐을 잘 쪼갰는데도 느리다면? 그건 거리가 멀어서일 가능성이 높아요. 서울에 있는 클라이언트가 미국 동부(us-east-1) 버킷에 업로드하면 데이터가 태평양을 건너야 하니까요.

Transfer Acceleration은 이 거리를 줄여 주는 도구예요. 회사 비유로 — 본사로 직접 보내는 대신 동네 임시 매장(엣지 로케이션)을 거쳐, 매장에서 본사로 가는 전용 도로(AWS 백본 네트워크)로 빠르게 전달하는 식입니다.

흐름이 이래요.

클라이언트 → (가까운 엣지 로케이션) → AWS 백본 → S3 버킷

CloudFront가 전 세계에 깔아 둔 엣지 로케이션을 활용해서 클라이언트는 일단 가장 가까운 엣지까지만 빠르게 데이터를 던지면 됩니다. 그 다음부터는 AWS 내부 전용망이 책임지고 본사 버킷까지 옮겨 줘요. 인터넷 공용망보다 훨씬 안정적이고 빠릅니다.

언제 유용한가

  • 업로드 클라이언트가 S3 버킷과 지리적으로 멀리 떨어진 경우
  • 대용량 파일을 인터넷을 통해 원격으로 업로드할 때
  • 전 세계 사용자가 동일한 버킷에 업로드할 때

여기서 시험 함정이 하나 있어요. Transfer Acceleration은 별도 요금이 듭니다. 그래서 클라이언트와 버킷이 같은 리전에 있다면 효과가 거의 없고 비용만 들어요. 거리가 멀고 데이터가 클 때만 효용이 큽니다. AWS가 친절하게 속도 비교 테스트 페이지를 제공하니까, 활성화 전에 한 번 돌려 보고 결정하는 게 좋아요.

활성화와 사용

활성화하면 새 엔드포인트(bucket-name.s3-accelerate.amazonaws.com)가 생깁니다. 이 엔드포인트로 보내는 요청만 가속이 적용돼요. 기존 엔드포인트로 보내면 평소대로 동작합니다.

# Transfer Acceleration 활성화
aws s3api put-bucket-accelerate-configuration \
  --bucket my-bucket \
  --accelerate-configuration Status=Enabled

# Transfer Acceleration 상태 확인
aws s3api get-bucket-accelerate-configuration --bucket my-bucket

# Transfer Acceleration을 사용한 업로드
aws s3 cp large-file.zip \
  s3://my-bucket/ \
  --endpoint-url https://s3-accelerate.amazonaws.com
import boto3

# Transfer Acceleration 사용 S3 클라이언트
s3_accelerated = boto3.client(
    's3',
    config=boto3.session.Config(s3={'use_accelerate_endpoint': True})
)

# 가속화된 업로드
s3_accelerated.upload_file(
    'large-file.zip',
    'my-bucket',
    'uploads/large-file.zip'
)

멀티파트와 같이 쓰면

Transfer Acceleration과 멀티파트 업로드는 서로 보완 관계예요. 멀티파트는 트럭 여러 대로 동시에 보내는 거고, Transfer Acceleration은 그 트럭들이 빠른 도로를 타게 하는 겁니다. 큰 파일을 멀리 있는 클라이언트에서 올릴 때 둘을 같이 쓰면 효과가 가장 큽니다.

> 한 줄 정리 — Transfer Acceleration = 엣지 로케이션을 거쳐 AWS 백본으로 빠르게 전달. 거리가 멀고 데이터가 클 때만 효과. 멀티파트와 함께 쓰면 시너지.

Byte-Range Fetch — 큰 책을 챕터별로 동시에 읽기

여기까지는 올리기를 빠르게 하는 도구였어요. 이번엔 받기를 빠르게 하는 도구입니다.

회사 비유로 — 두꺼운 책을 한 사람이 처음부터 끝까지 읽으면 오래 걸리지만, 챕터별로 여러 사람이 동시에 읽으면 훨씬 빠르죠. 다 읽은 다음 합치면 한 권이 됩니다.

S3는 객체의 특정 바이트 범위만 요청하는 기능을 제공해요. 이걸 Byte-Range Fetch라고 합니다. 큰 파일을 여러 범위로 쪼개 동시에 받으면 다운로드도 병렬화돼요.

두 가지 쓸모

  • 병렬 다운로드 — 다운로드 속도 향상이 주 목적
  • 부분 다운로드 — 큰 파일의 특정 부분만 필요할 때 (예: 비디오의 한 구간, ZIP 헤더만 읽기)

두 번째 용도가 의외로 유용해요. 100MB ZIP 파일에서 헤더 1KB만 보고 싶을 때, 전체를 받지 않고 필요한 바이트만 가져오면 네트워크 비용도 줄고 빠릅니다.

CLI로 범위 다운로드

# 특정 바이트 범위 다운로드
aws s3api get-object \
  --bucket my-bucket \
  --key large-file.bin \
  --range bytes=0-10485759 \  # 0 ~ 10MB
  part1.bin

aws s3api get-object \
  --bucket my-bucket \
  --key large-file.bin \
  --range bytes=10485760-20971519 \  # 10MB ~ 20MB
  part2.bin

bytes=시작-끝으로 범위를 지정해요. 끝 인덱스가 포함이라는 점만 주의하면 됩니다.

Python으로 병렬 다운로드

import boto3
import threading

def download_range(bucket, key, start, end, output_file, part_number):
    s3_client = boto3.client('s3')
    response = s3_client.get_object(
        Bucket=bucket,
        Key=key,
        Range=f'bytes={start}-{end}'
    )
    with open(f'{output_file}.part{part_number}', 'wb') as f:
        f.write(response['Body'].read())
    print(f"Part {part_number} downloaded")

# 병렬 다운로드 예시
PART_SIZE = 10 * 1024 * 1024  # 10MB
bucket = 'my-bucket'
key = 'large-file.bin'

# 파일 크기 확인
s3 = boto3.client('s3')
response = s3.head_object(Bucket=bucket, Key=key)
file_size = response['ContentLength']

threads = []
for i in range(0, file_size, PART_SIZE):
    start = i
    end = min(i + PART_SIZE - 1, file_size - 1)
    part_num = i // PART_SIZE + 1
    
    t = threading.Thread(
        target=download_range,
        args=(bucket, key, start, end, 'output', part_num)
    )
    threads.append(t)
    t.start()

for t in threads:
    t.join()

흐름은 단순해요. HEAD 요청으로 파일 크기를 알아내고, 그 크기를 PART_SIZE로 나눠 범위를 만들고, 각 범위를 별도 스레드로 동시에 받습니다. 받은 파트들을 합치면 원본이 돼요.

> 한 줄 정리 — Byte-Range Fetch = 객체의 일부 바이트만 요청. 병렬 다운로드 / 부분 다운로드 두 자리.

prefix 분산 — 창구를 여러 개로 늘리기

처음에 prefix별로 PUT 3,500 / GET 5,500 한도가 있다고 했죠. 이 한도에 닿는 워크로드라면 prefix를 잘게 쪼개기만 해도 처리량이 그대로 곱해집니다.

회사 비유로 — 창구 한 개당 분당 처리 인원이 정해져 있어요. 손님이 한 창구에만 줄 서면 그 창구가 막힙니다. 그런데 창구를 5개로 늘리고 손님을 분산시키면 처리량이 5배가 돼요.

비효율적 prefix 설계

날짜 기반 prefix는 직관적이지만 — 같은 날짜의 파일이 모두 한 prefix에 몰립니다.

my-bucket/
  2024/01/file1.jpg
  2024/01/file2.jpg
  2024/01/file3.jpg
  ... (모두 동일 접두사)

이러면 1월에 업로드된 파일을 한꺼번에 처리할 때 2024/01/ prefix만 한도에 닿아요.

효율적 prefix 설계

prefix 앞부분에 분산되는 토큰을 두면 자동으로 여러 prefix로 흩어집니다.

my-bucket/
  a/file1.jpg
  b/file2.jpg
  c/file3.jpg
  d/file4.jpg
  ... (다양한 접두사로 분산)

해시 prefix 패턴

가장 깔끔한 방법은 파일 이름의 해시 앞 몇 글자를 prefix로 쓰는 거예요. 자동으로 균등하게 분산됩니다.

import hashlib

def get_distributed_key(original_key):
    """파일 이름의 해시를 접두사로 사용하여 요청 분산"""
    hash_prefix = hashlib.md5(original_key.encode()).hexdigest()[:4]
    return f"{hash_prefix}/{original_key}"

# 사용 예시
files = ['image1.jpg', 'image2.jpg', 'image3.jpg']
for f in files:
    key = get_distributed_key(f)
    print(f"Distributed key: {key}")
    # 출력: 3f4c/image1.jpg, a8b2/image2.jpg, ...

여기서 시험 함정이 하나 있어요. prefix 분산은 워크로드가 한도에 닿을 때만 효용이 있어요. 하루 몇십 건 올리는 평범한 워크로드는 어떤 prefix를 써도 무방합니다. 초당 수천 건 이상이 한 prefix에 쏟아지는 경우에만 분산이 의미가 있어요.

> 한 줄 정리 — prefix별 한도 PUT 3,500 / GET 5,500. 한도에 닿는 워크로드라면 해시 prefix로 분산.

S3 Select — 사물함에서 꺼내기 전에 줄을 골라 받기

여기서부터는 데이터 자체를 영리하게 다루는 도구입니다.

회사 비유로 — 사물함에서 100MB짜리 엑셀 파일을 통째로 꺼내 와서 회사로 가져온 다음 필요한 줄만 골라 보는 게 보통이에요. 그런데 사물함 측에 "이 조건에 맞는 줄만 골라서 보내 주세요" 라고 부탁할 수 있다면? 데이터 1MB만 받아도 되니까 네트워크도 절약되고 처리도 빨라지죠.

S3 Select가 정확히 그 일을 합니다. 객체 전체를 다운로드하지 않고 SQL 쿼리로 필요한 데이터만 추출해요.

지원 형식

  • CSV — 구분자 기반 텍스트
  • JSON — JSON Lines 형식
  • Apache Parquet — 열 기반 압축 형식
  • BZIP2, GZIP — 압축 파일 지원 (CSV, JSON)

효용 두 가지

  • 네트워크 비용 감소 — 필요한 줄만 받으니까 전송량이 확 줄어요
  • 처리 속도 향상 — 클라이언트에서 파싱·필터링하는 부담이 사라짐

CLI 예시

# CSV 파일에서 특정 조건으로 데이터 추출
aws s3api select-object-content \
  --bucket my-bucket \
  --key data/sales.csv \
  --expression "SELECT * FROM S3Object WHERE revenue > 10000" \
  --expression-type SQL \
  --input-serialization '{"CSV": {"FileHeaderInfo": "USE"}}' \
  --output-serialization '{"CSV": {}}' \
  output.csv

SELECT ... FROM S3Object WHERE ... 형식이 익숙한 SQL 그대로예요. 입력·출력 직렬화만 명시하면 됩니다.

Python 예시

import boto3
import json

s3_client = boto3.client('s3')

# CSV 데이터에서 SQL 쿼리 실행
response = s3_client.select_object_content(
    Bucket='my-bucket',
    Key='data/large-dataset.csv',
    ExpressionType='SQL',
    Expression="SELECT name, revenue FROM S3Object WHERE revenue > 10000",
    InputSerialization={
        'CSV': {
            'FileHeaderInfo': 'USE',
            'RecordDelimiter': '\n',
            'FieldDelimiter': ','
        },
        'CompressionType': 'NONE'
    },
    OutputSerialization={
        'CSV': {}
    }
)

# 결과 스트리밍 처리
for event in response['Payload']:
    if 'Records' in event:
        data = event['Records']['Payload'].decode('utf-8')
        print(data)
    elif 'Stats' in event:
        stats = event['Stats']['Details']
        print(f"Bytes scanned: {stats['BytesScanned']}")
        print(f"Bytes returned: {stats['BytesReturned']}")

Stats스캔한 바이트와 반환한 바이트가 같이 옵니다. 이걸 보면 얼마나 많은 데이터를 절감했는지 한눈에 보여요.

Glacier Select

Glacier에 보관 중인 아카이브에도 SQL 쿼리를 거는 동생 기능이 있어요. 아카이브 전체를 복원하지 않고 필요한 데이터만 추출합니다. 7~10년 묵은 로그에서 특정 사건을 찾아낼 때 — 전체 복원 비용 없이 필요한 줄만 꺼내 볼 수 있습니다.

여기서 시험 함정이 하나 있어요. S3 Select는 단일 객체 안에서만 동작합니다. 여러 객체를 가로지르는 쿼리는 못 해요. 여러 객체를 한 번에 쿼리하려면 Athena 자리예요. Athena는 S3에 있는 여러 객체를 묶어 쿼리할 수 있는 별도 서비스입니다.

> 한 줄 정리 — S3 Select = 서버 측 SQL 필터, 단일 객체 한정. 여러 객체 쿼리는 Athena.

S3 Inventory — 객체 목록을 LIST 안 쓰고 받기

버킷에 객체가 수백만 개 있을 때, 목록을 한 번에 보고 싶다면? LIST API로 페이지마다 한 번씩 쳐서 받는 건 너무 느리고 요청 비용도 큽니다.

S3 Inventory는 버킷 내 모든 객체와 메타데이터의 정기 리포트를 미리 만들어 주는 기능이에요. 매일 또는 매주 한 번 리포트를 생성해 지정된 S3 버킷에 떨어뜨립니다. 그걸 받아 보면 LIST보다 훨씬 빠르고 저렴해요.

리포트에 들어가는 항목

  • 객체 이름(키), 크기, 최종 수정 날짜
  • 스토리지 클래스, 암호화 여부
  • 멀티파트 업로드 여부, 버전 ID
  • Object Lock 설정 여부

생성 주기 / 형식

  • 주기 — 매일 / 매주
  • 형식 — CSV / Apache ORC / Apache Parquet
  • 저장 위치 — 지정된 S3 버킷
# 인벤토리 구성 생성
aws s3api put-bucket-inventory-configuration \
  --bucket my-source-bucket \
  --id my-inventory \
  --inventory-configuration '{
    "Id": "my-inventory",
    "IsEnabled": true,
    "Destination": {
      "S3BucketDestination": {
        "Bucket": "arn:aws:s3:::my-inventory-bucket",
        "Format": "CSV"
      }
    },
    "Schedule": {
      "Frequency": "Daily"
    },
    "IncludedObjectVersions": "All",
    "OptionalFields": [
      "Size", "LastModifiedDate", "StorageClass",
      "ETag", "IsMultipartUploaded", "EncryptionStatus"
    ]
  }'

# 인벤토리 구성 확인
aws s3api get-bucket-inventory-configuration \
  --bucket my-source-bucket \
  --id my-inventory

이 인벤토리 리포트가 다음에 나올 Batch Operations의 매니페스트로 그대로 들어갑니다. 둘이 한 쌍이에요.

> 한 줄 정리 — Inventory = 버킷 객체 목록 정기 리포트. LIST API의 대량 작업용 대안. Batch Operations의 매니페스트로 활용.

S3 Batch Operations — 사물함 안 모든 봉투에 도장 찍기

객체가 수백만 개인 버킷에서 모든 객체의 스토리지 클래스를 바꾸거나, 모두에 태그를 붙이거나, 모두에 Lambda를 실행해야 한다면? 한 객체씩 API를 호출하면 며칠이 걸리고 비용도 큽니다.

회사 비유로 — 사물함 안 모든 봉투에 한 번에 도장을 찍는 자동 기계예요. 어떤 봉투가 대상인지(매니페스트), 무슨 도장을 찍을지(작업), 결과는 어디에 저장할지(리포트)만 알려 주면 S3가 알아서 모두 처리합니다.

지원 작업 유형

  • 객체 복사 (Copy) — 다른 버킷·prefix로 복제
  • 태그 수정 (Put Object Tagging) — 일괄 태그 부여
  • ACL 변경 (Put Object ACL) — 권한 일괄 변경
  • 스토리지 클래스 전환 (Initiate Glacier Restore) — Glacier에서 일괄 복원
  • Lambda 함수 실행 (Invoke Lambda) — 객체별 커스텀 처리
  • Object Lock 설정 — 보존 정책 일괄 적용

Lambda 호출이 가장 강력해요. 객체별로 임의의 코드를 돌릴 수 있으니 사실상 못 하는 작업이 거의 없습니다.

구성 요소 네 가지

  1. 작업(Job) — 무엇을 할 것인가
  2. 매니페스트(Manifest) — 어떤 객체에 할 것인가 (S3 Inventory 리포트 또는 CSV)
  3. IAM 역할 — 권한
  4. 완료 보고서 — 결과 저장 위치

매니페스트가 핵심이에요. 보통 S3 Inventory 리포트를 그대로 매니페스트로 씁니다. 인벤토리와 배치 작업이 한 쌍이라고 한 게 이 때문이에요.

작업 생성 예시

# IAM 역할 생성 (배치 작업용)
cat > batch-ops-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"Service": "batchoperations.s3.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws iam create-role \
  --role-name S3BatchOpsRole \
  --assume-role-policy-document file://batch-ops-trust-policy.json

# 배치 작업 생성 (스토리지 클래스 변환 예시)
aws s3control create-job \
  --account-id 123456789012 \
  --operation '{
    "S3PutObjectCopy": {
      "TargetResource": "arn:aws:s3:::destination-bucket",
      "StorageClass": "STANDARD_IA"
    }
  }' \
  --manifest '{
    "Spec": {
      "Format": "S3InventoryReport_CSV_20161130",
      "Fields": ["Bucket", "Key", "VersionId"]
    },
    "Location": {
      "ObjectArn": "arn:aws:s3:::my-inventory-bucket/inventory-report.csv",
      "ETag": "etag-value"
    }
  }' \
  --report '{
    "Bucket": "arn:aws:s3:::my-reports-bucket",
    "Format": "Report_CSV_20180820",
    "Enabled": true,
    "ReportScope": "AllTasks"
  }' \
  --priority 10 \
  --role-arn arn:aws:iam::123456789012:role/S3BatchOpsRole

# 배치 작업 상태 확인
aws s3control describe-job \
  --account-id 123456789012 \
  --job-id JOB_ID

흐름은 — Trust Policy로 IAM 역할 만들고, create-job으로 작업·매니페스트·리포트·역할을 한 번에 묶어 던집니다. S3가 백그라운드에서 처리하고, 끝나면 리포트 버킷에 결과를 떨어뜨려요.

기존 객체 복제 — Batch Replication

여기서 시험 함정이 하나 있어요. S3 복제 규칙은 규칙 생성 이후에 새로 추가된 객체에만 적용됩니다. 규칙을 켜기 전에 이미 존재하던 객체는 자동으로 복제되지 않아요.

이걸 풀어 주는 게 Batch Replication입니다. S3 Batch Operations로 기존 객체에 일괄 복제 작업을 거는 거예요. 콘솔에서 복제 규칙을 만들 때 S3가 친절하게 "기존 객체도 복제할까요?"라고 제안하기도 합니다.

# 복제 규칙 생성 후 기존 객체 복제를 위한 배치 작업
# (콘솔에서 진행 시 자동으로 제안됨)

aws s3control create-job \
  --account-id 123456789012 \
  --operation '{
    "S3ReplicateObject": {}
  }' \
  --manifest-generator '{
    "S3JobManifestGenerator": {
      "SourceBucket": "arn:aws:s3:::source-bucket",
      "EnableManifestOutput": false,
      "Filter": {
        "EligibleForReplication": true
      }
    }
  }' \
  --report '{
    "Enabled": false
  }' \
  --priority 1 \
  --role-arn arn:aws:iam::123456789012:role/S3BatchOpsRole

이때는 매니페스트를 직접 만들 필요가 없어요. manifest-generator가 소스 버킷을 스캔해 자동으로 만들어 줍니다.

> 한 줄 정리 — Batch Operations = 수백만 객체에 한 번에 작업. Inventory 리포트가 매니페스트. 기존 객체 복제는 Batch Replication.

성능 최적화 종합 — 어느 자리에 무엇을 쓰나

여기까지 본 도구들을 한 표로 정리하면 이래요. 어떤 문제가 있을 때 어떤 도구를 꺼내는지 알아 두면 시험·실무 모두에서 즉답할 수 있습니다.

풀고 싶은 문제도구
큰 파일 빠르게 올리기멀티파트 업로드
거리가 먼 클라이언트의 업로드Transfer Acceleration
큰 파일 빠르게 받기 / 부분만 받기Byte-Range Fetch
객체 일부 줄만 골라 받기S3 Select
prefix 한도에 닿는 고트래픽prefix 분산 (해시 prefix)
읽기 트래픽 캐싱CloudFront
수백만 객체 일괄 작업S3 Batch Operations
버킷 객체 목록 빠르게S3 Inventory
불완전 멀티파트 비용 누수 방지라이프사이클 정책

CLI 업로드 최적화 설정

S3 CLI는 동시 요청 수·파트 크기·대역폭 같은 항목을 프로필별로 튜닝할 수 있어요. 대용량 워크로드에서는 한 번 손봐 주는 것만으로도 체감 속도가 크게 달라집니다.

# ~/.aws/config
[default]
s3 =
    max_concurrent_requests = 20
    max_queue_size = 10000
    multipart_threshold = 64MB
    multipart_chunksize = 16MB
    max_bandwidth = 50MB/s

CloudWatch로 모니터링

# CloudWatch로 S3 성능 지표 모니터링
aws cloudwatch get-metric-statistics \
  --namespace AWS/S3 \
  --metric-name RequestLatency \
  --dimensions Name=BucketName,Value=my-bucket \
              Name=FilterId,Value=EntireBucket \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-02T00:00:00Z \
  --period 3600 \
  --statistics Average

RequestLatency·FirstByteLatency 같은 지표를 보면 어떤 prefix가 한도에 닿는지, 어떤 시간대가 느린지가 보여요. 그걸 보고 prefix 분산이나 CloudFront 도입을 결정합니다.

핵심 수치 한 번 더

시험 직전에 다시 펼칠 수 있게 핵심 수치만 모아 둡니다.

지표수치
접두사당 초당 PUT/COPY/POST/DELETE3,500
접두사당 초당 GET/HEAD5,500
최대 객체 크기5TB
단일 PUT 업로드 최대5GB
멀티파트 최대 파트 수10,000
멀티파트 최소 파트 크기 (마지막 제외)5MB
콘솔 최대 업로드160GB

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 S3 3편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • 처리량 한도는 prefix별 — 버킷 전체가 아니라 prefix 단위
  • prefix당 PUT 3,500 / GET 5,500 / 초
  • 단일 PUT 최대 5GB, 그 이상은 멀티파트 필수
  • 멀티파트 권장 시작점 100MB, 필수 시작점 5GB
  • 파트 크기 5MB ~ 5GB, 마지막 파트만 5MB 미만 허용
  • 멀티파트 최대 파트 수 10,000개, 최대 객체 5TB
  • 흐름 — Create / UploadPart / Complete (또는 Abort)
  • 미완료 멀티파트는 자동 삭제 X — 보이지 않는 비용 발생
  • 대응 = 라이프사이클로 N일 후 자동 abort
  • Transfer Acceleration = 엣지 → AWS 백본 → 버킷 경로
  • 거리가 멀고 데이터 클 때만 효과, 별도 요금
  • 엔드포인트 bucket.s3-accelerate.amazonaws.com
  • Byte-Range Fetch = 객체의 바이트 범위만 요청
  • 두 자리 — 병렬 다운로드 / 부분 다운로드
  • prefix 분산 = 해시 앞자리를 prefix로, 한도에 닿을 때만 효용
  • 날짜 prefix는 한 prefix에 트래픽 집중되기 쉬움
  • S3 Select = 단일 객체 안에서 SQL 필터, CSV/JSON/Parquet
  • 여러 객체 가로지르는 쿼리는 Athena 자리
  • Glacier Select = 아카이브에도 SQL 적용, 전체 복원 불필요
  • S3 Inventory = 버킷 객체 목록 정기 리포트, LIST 대안
  • 리포트 형식 — CSV / ORC / Parquet, 매일 또는 매주
  • Batch Operations = 수백만 객체 일괄 작업
  • 구성 요소 4개 — Job · Manifest · IAM Role · Report
  • 매니페스트는 보통 Inventory 리포트를 그대로 사용
  • Lambda 호출 작업으로 임의 처리 가능
  • 복제 규칙은 이후 객체만 자동 적용
  • 기존 객체 복제 = Batch Replication
  • 멀티파트 + Transfer Acceleration = 시너지 (분할 + 빠른 길)
  • CLI 튜닝 — multipart_threshold / chunksize / max_concurrent_requests
  • CloudWatch — RequestLatency / FirstByteLatency 모니터링

다음 글(4편)에서는 S3 가용성과 복제 — Cross-Region Replication, Same-Region Replication, 복제 규칙·필터·옵션, 복제 지연 시간 모니터링, 복제와 라이프사이클의 상호작용을 회사 지점 간 자료 미러링 비유로 풀어 갑니다. 재해 복구·규정 준수·읽기 지연 단축 같은 자리에서 자주 등장하니 단단히 잡아 둡니다.

시리즈 다른 편

같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!