Standard
자주 접근하는 기본 스토리지 클래스다. 검색 지연과 최소 보관 기간 부담이 없다.
업로드 직후, 활성 사용자 파일, 자주 읽는 정적 자산분류: Layer 3 - AWS 인프라 & 보안
S3(Simple Storage Service)는 AWS의 객체 스토리지(object storage) 서비스다. 객체 스토리지는 파일을 디렉터리 트리 안의 inode로 다루지 않고, bucket + key로 찾는 하나의 객체(object)로 저장한다. 이 구조 덕분에 이미지, 로그, 백업, 정적 자산, 데이터 레이크 원본처럼 크기와 개수가 계속 늘어나는 데이터를 같은 모델로 다룰 수 있다.
처음 읽을 때 붙잡을 문장은 이것이다. S3는 폴더를 무한히 크게 만든 파일시스템이 아니라, 폴더를 버리고 key 기반 조회로 확장성을 얻은 저장소다.
S3는 2006년 3월 출시됐다. 그전까지 대규모 파일 저장의 익숙한 모델은 POSIX 파일시스템(NFS, ext4 등)이었다. POSIX 파일시스템은 디렉터리, 파일, rename atomicity, inode, 권한 비트 같은 풍부한 로컬 파일 의미론을 제공하지만, 페타바이트 규모와 수억~수십억 객체에서는 그 의미론 자체가 확장 병목이 된다.
S3의 해법은 **flat namespace(평면 네임스페이스)**다. 네임스페이스는 “이름을 어떻게 배치하고 찾는가”라는 뜻이고, flat은 실제 부모-자식 디렉터리를 만들지 않는다는 뜻이다. S3는 bucket이라는 최상위 컨테이너 안에 key라는 문자열을 가진 객체를 저장한다. logs/2026/07/08/app.log는 4단계 폴더 아래 파일이 아니라, 슬래시가 들어간 긴 key 하나다.
이 전환은 기능을 덜어내고 확장성을 얻는 trade-off다. S3는 POSIX의 모든 파일 연산을 흉내 내지 않는다. 대신 bucket과 key를 기준으로 객체를 분산 저장하고, 객체 수가 매우 커져도 같은 API 모델을 유지한다. 출시 후 오랫동안 overwrite PUT/DELETE와 LIST는 eventual consistency였지만, 2020-12-01부터 S3는 GET/PUT/LIST에 강한 read-after-write 일관성을 제공한다. 다만 Cross-Region Replication처럼 다른 리전으로 복제하는 경로에서는 복제 지연 동안 여전히 eventual하게 보일 수 있다.
이 토픽이 없으면 무엇이 깨지나: 로그 아카이브, 이미지 업로드, 데이터 레이크, 빌드 아티팩트 저장을 모두 “큰 NFS”로만 설계하게 된다. 그러면 inode, 디렉터리 락, 파일 서버 확장, 지역 복제 지연이 먼저 문제가 된다. 객체 스토리지는 이 문제를 “파일시스템을 더 크게 만들기”가 아니라 “저장 단위를 object + key로 바꾸기”로 푼다.
📖 출처: Amazon S3 Update – Strong Read-After-Write Consistency (AWS News Blog, 2020-12-01), From POSIX to AWS and S3 – an Evolution (Scality)
**Bucket(버킷)**은 객체를 담는 최상위 컨테이너다. 버킷 이름은 AWS 파티션 안에서 전역적으로 고유해야 하고, 버킷에는 리전, 버전 관리, 암호화, 정책, lifecycle 같은 설정이 붙는다.
**Object(객체)**는 S3에 저장되는 데이터 단위다. 파일 내용 자체와 메타데이터(metadata)를 함께 가진다. 메타데이터는 Content-Type, Content-Length, ETag, 사용자 정의 태그처럼 객체를 설명하는 작은 속성이다.
**Key(키)**는 버킷 안에서 객체를 찾는 문자열 이름이다. 파일 경로처럼 보일 수 있지만 실제 디렉터리 경로가 아니다.
**Prefix(접두사)**는 key의 앞부분 문자열이다. images/profile.jpg에서 images/를 prefix로 볼 수 있고, S3의 lifecycle rule, 권한 조건, 대량 요청 분산은 이 prefix를 기준으로 설계되는 경우가 많다. 그래서 prefix는 실제 폴더가 아니라 key 문자열을 묶어 읽는 학습 모델로 잡아야 한다.
s3://my-bucket/images/profile.jpgbucket = my-bucketkey = images/profile.jpgobject = "profile.jpg의 내용" + metadata콘솔에서 images/가 폴더처럼 보이는 이유는 콘솔이 key 안의 /를 기준으로 묶어 보여주기 때문이다. S3 입장에서는 images/profile.jpg, images/banner.jpg, logs/2026/app.log라는 key 세 개가 있을 뿐이다. 이 오해는 권한, lifecycle, prefix 성능 문제까지 이어지므로 초반에 확실히 잡아야 한다.
작은 예시로 확인해보자.
| 보이는 이름 | S3가 실제로 보는 것 | 학습 포인트 |
|---|---|---|
images/profile.jpg | key가 images/profile.jpg인 객체 1개 | images/ 디렉터리 inode가 생긴 것이 아니다 |
images/ | 0바이트 placeholder 객체일 수도, 화면 묶음일 수도 있음 | 콘솔 표시만 보고 실제 객체라고 단정하면 안 된다 |
logs/2026/07/08/app.log | 슬래시가 여러 번 들어간 긴 key | lifecycle/prefix 규칙은 이 문자열 앞부분을 본다 |
이 모델 때문에 “폴더 이름을 바꾼다”는 작업도 파일시스템과 다르다. S3에는 디렉터리 rename이 없으므로, images/ 아래 모든 객체를 새 key로 복사하고 기존 key를 삭제하는 형태가 된다. 객체 수가 많으면 rename처럼 보이는 작업이 실제로는 대량 copy/delete 작업이고, 비용·시간·부분 실패 가능성이 생긴다.
퀴즈
힌트: S3 네임스페이스 구조를 떠올린다.
대부분 아니다. S3는 bucket + key의 flat namespace이고, images/profile.jpg라는 key를 콘솔이 폴더처럼 보여줄 뿐이다. 빈 폴더처럼 보이게 하려고 0바이트 placeholder 객체를 만드는 경우는 예외다.
S3 Standard는 여러 AZ(Availability Zone)에 객체 데이터를 분산 저장하고, 공식적으로 99.999999999%(11-nine) 내구성을 목표로 한다. 내구성(durability)은 “저장한 객체를 잃지 않을 확률”이고, 가용성(availability)은 “지금 요청했을 때 응답받을 수 있는가”다. 둘은 다르다.
내구성의 핵심 직관은 erasure coding이다. erasure coding은 데이터를 여러 조각과 복구용 parity 조각으로 나누어 저장하는 방식이다. 예를 들어 원본을 여러 data shard와 parity shard로 쪼개면, 일부 디스크나 노드가 사라져도 남은 조각으로 원본을 복원할 수 있다. 단순 3중 복제보다 저장 효율이 좋으면서도 일부 장애에 강하다.
반례도 중요하다. 11-nine 내구성은 “실수로 삭제한 객체도 자동으로 되살아난다”는 뜻이 아니다. 사용자가 DeleteObject를 호출하거나 잘못된 lifecycle이 만료를 실행하면 그 삭제 의도 자체가 정상 변경으로 복제된다. 사람 실수와 랜섬웨어성 삭제에 대비하려면 Versioning(버전 관리), Object Lock, 백업 계정 분리 같은 별도 보호가 필요하다.
내구성을 손으로 계산하는 정확한 수식은 AWS 내부 구현 없이는 알 수 없지만, 직관은 만들 수 있다. 디스크 1개에만 파일을 두면 그 디스크가 죽는 순간 데이터가 사라진다. 여러 AZ에 조각을 나눠 두고 일부 조각이 사라져도 복원 가능하게 만들면, “한 장비 장애”가 “객체 손실”로 바로 이어지지 않는다. 그래서 S3의 위험 모델은 로컬 디스크의 “장비가 죽으면 파일이 죽는다”에서 “여러 장애가 동시에 복구 한계를 넘으면 객체가 위험하다”로 바뀐다.
반대로 삭제와 변조는 다른 축이다.
| 상황 | S3 내구성이 막아주는가 | 별도 대비 |
|---|---|---|
| 디스크 일부 손상 | 예, 분산 저장과 복구로 완화 | 기본 S3 내구성에 기대도 된다 |
사용자가 잘못 DeleteObject | 아니오 | Versioning, MFA Delete, Object Lock 검토 |
| 잘못된 lifecycle 만료 규칙 | 아니오 | rule dry-run 사고방식, 좁은 prefix/tag 적용 |
| 단일 AZ 전체 장애 | class에 따라 다름 | Standard 계열과 One Zone 계열의 차이를 확인 |
**Storage Class(스토리지 클래스)**는 같은 객체라도 접근 빈도, 복원 지연, 최소 보관 기간, 저장 단가를 다르게 적용하는 S3의 저장 등급이다. 자주 읽는 객체와 1년에 한 번 볼까 말까 한 감사 로그를 같은 단가로 보관하면 비용이 낭비된다.
자주 접근하는 기본 스토리지 클래스다. 검색 지연과 최소 보관 기간 부담이 없다.
업로드 직후, 활성 사용자 파일, 자주 읽는 정적 자산가끔 접근하는 데이터에 적합하다. 저장 비용은 낮지만 최소 30일 보관 요금과 검색 비용을 고려한다.
한 달 이후 접근 빈도가 떨어지는 로그·백업아카이브 성격이지만 즉시 복원이 필요할 때 쓴다. 최소 90일 보관을 고려한다.
분기별 감사 자료처럼 드물지만 바로 열어야 하는 데이터가장 저렴한 장기 보관용이다. 복원에 시간이 걸리고 최소 180일 보관 요금이 있다.
1년 이상 보관하는 법적 아카이브접근 패턴을 모를 때 객체별로 자동 이동한다. 128KB 미만 객체와 모니터링 비용은 주의한다.
파일별 접근 패턴이 들쭉날쭉한 서비스**Lifecycle Policy(수명 주기 정책)**는 객체가 생성된 뒤 며칠이 지났는지, 어떤 prefix/tag에 속하는지에 따라 스토리지 클래스를 바꾸거나 삭제하는 자동 규칙이다. “업로드 직후에는 Standard, 30일 뒤 IA, 90일 뒤 Glacier, 365일 뒤 삭제”처럼 시간 흐름을 비용 정책으로 바꾼다.
업로드 직후에는 Standard에 둔다. 장애 분석, 재처리, 최근 조회가 아직 많다.
접근 빈도가 낮아지면 Standard-IA로 전환한다. 단 30일 최소 보관 요금을 고려한다.
감사·보관 성격으로 바뀌면 Glacier Instant Retrieval 같은 아카이브 계열을 검토한다.
보관 의무가 남으면 Deep Archive, 의무가 끝났으면 만료 삭제를 적용한다.
주의할 점은 “싸니까 빨리 내린다”가 항상 이득은 아니라는 것이다. IA와 Glacier 계열은 최소 보관 기간, 검색 비용, 복원 지연이 붙는다. 7일 뒤 삭제될 임시 파일을 3일 만에 IA로 내리면 저장 단가는 내려가도 최소 보관 요금 때문에 전체 비용이 늘 수 있다.
Standard를 유지한다. 검색 요금이 없고 빈번 접근에 가장 단순하다.
사용자 프로필 이미지, 활성 정적 파일Standard-IA로 전환한다. 저장 비용은 낮아지지만 최소 30일 보관 요금을 고려한다.
최근 로그나 백업처럼 가끔 다시 열 수 있는 데이터즉시 복원이 필요하면 Glacier Instant Retrieval을, 기다릴 수 있으면 더 깊은 아카이브를 검토한다.
감사 자료, 고객 요청 시 열어야 하는 보관 파일긴 복원 지연을 받아들일 수 있으면 Deep Archive가 후보가 된다.
법적 보관 기간만 충족하면 되는 데이터Intelligent-Tiering이 객체별 접근 패턴을 보고 자동 이동한다.
파일마다 접근 빈도가 크게 다른 서비스Intelligent-Tiering 모니터링 비용이 저장 비용을 넘을 수 있어 Standard 유지가 안전할 수 있다.
작은 썸네일, 이벤트 조각 파일이 매우 많을 때Intelligent-Tiering은 객체 접근 패턴을 모를 때 S3가 계층을 자동 조정하는 스토리지 클래스다. 자주 접근하면 Frequent Access, 한동안 접근하지 않으면 Infrequent Access나 Archive Instant Access 계층으로 옮기고, 다시 접근하면 Frequent Access로 되돌린다.
30일 미접근 → Infrequent Access tier90일 미접근 → Archive Instant Access tier다시 접근 → Frequent Access tier로 복귀제약을 함께 외워야 한다. 128KB 미만 객체는 자동 티어 이동 대상이 아니고, 객체별 모니터링/자동화 비용이 붙는다. 따라서 작은 객체가 수천만 개라면 “자동 최적화”가 아니라 “객체 수만큼 모니터링 비용이 쌓이는 구조”가 될 수 있다.
비용 직관은 객체 수 × 모니터링/자동화 비용으로 잡으면 된다. 객체 하나의 저장 비용 절감은 작을 수 있는데 객체 수가 매우 많으면 관리 비용 합계가 절감액을 넘을 수 있으므로, tiny object 워크로드에서는 총 객체 수를 먼저 본다.
Lifecycle과 Intelligent-Tiering의 선택 기준은 간단하다.
세 가지 worked example로 감을 잡는다.
| 데이터 | 좋은 첫 선택 | 이유 |
|---|---|---|
| 최근 7일 장애 분석 로그 | Standard | 재처리와 검색이 잦고 곧 삭제될 수 있어 IA 최소 보관이 불리함 |
| 30일 이후 거의 안 보는 감사 로그 | Lifecycle로 Standard-IA 또는 Glacier 계열 | 시간 기준 접근 감소가 뚜렷함 |
| 고객별 접근 편차가 큰 첨부파일 | Intelligent-Tiering 후보 | 고객 A 파일은 매일 열리고 고객 B 파일은 1년간 안 열릴 수 있음 |
잘못된 선택의 반례도 있다. “이미지 썸네일은 오래되면 안 보니까 Intelligent-Tiering”이라고 단정하면 위험하다. 썸네일이 20KB이고 수천만 개라면 128KB 미만 객체라 자동 티어링 이점이 제한되고, 객체 수에 비례한 모니터링 비용이 더 눈에 띌 수 있다. 이런 경우에는 썸네일을 Standard에 두고 CloudFront 캐시, 원본 이미지 lifecycle, 오래된 버전 정리부터 보는 편이 더 단순할 수 있다.
📖 출처: Amazon S3 Intelligent-Tiering 공식 문서, AWS S3 storage classes 요금
IAM을 먼저 배운 이유가 여기서 나온다. IAM Policy는 사용자·Role 같은 주체(principal)에 붙는 권한이고, Bucket Policy는 S3 버킷이라는 리소스에 붙는 권한이다. 실제 허용 여부는 IAM의 Allow/Deny, Bucket Policy의 Allow/Deny, 조직 정책, Block Public Access 같은 상위 제어를 함께 평가한 결과다.
작은 평가 예시:
요청: ECS Task Role이 s3:GetObject로 arn:aws:s3:::private-bucket/a.jpg 읽기
IAM Policy: Allow s3:GetObject on private-bucket/*Bucket Policy: Deny s3:GetObject if aws:SecureTransport=false요청 프로토콜: HTTPS결과: Deny 조건이 맞지 않고 IAM Allow가 있으므로 허용 가능
같은 요청이 HTTP라면?Bucket Policy의 Explicit Deny가 이기므로 IAM Allow가 있어도 거부이 예시의 핵심은 S3 권한을 “어디에 Allow가 하나 붙었나”로 보지 않는 것이다. IAM에서 배운 대로 Explicit Deny가 있으면 Allow보다 우선한다. Bucket Policy는 리소스 관점의 Deny를 넣기 좋기 때문에, HTTPS 강제·특정 VPC Endpoint만 허용·특정 CloudFront OAC만 허용 같은 경계 정책에 자주 쓰인다.
**Bucket Policy(버킷 정책)**는 버킷이나 객체 ARN에 대해 누가 어떤 S3 작업을 할 수 있는지 정하는 JSON 문서다. 예를 들어 HTTP 요청을 거부하고 HTTPS만 허용하려면 버킷 정책에 명시적 Deny를 둘 수 있다. 아래는 statement fragment가 아니라 버킷 정책 전체 형태다.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"], "Condition": { "Bool": { "aws:SecureTransport": "false" } } } ]}**ACL(Access Control List)**은 객체나 버킷에 붙는 오래된 권한 모델이다. 지금은 Bucket Policy와 IAM Policy를 중심으로 설계하고, ACL은 legacy 호환이나 특수한 소유권 상황에서만 만난다고 이해하면 된다.
Block Public Access는 실수로 버킷이나 객체가 퍼블릭으로 열리는 것을 막는 안전장치다. 민감 데이터가 있는 버킷은 이 설정을 해제하지 않는 것이 기본이다. 공개 파일 제공이 필요하면 버킷을 직접 public으로 여는 대신 CloudFront 또는 Presigned URL 패턴을 먼저 검토한다.
Block Public Access가 켜져 있는데 퍼블릭 Bucket Policy를 붙이면, 정책 JSON만 보면 열려 있는 것처럼 보여도 실제 public access는 막힐 수 있다. 이 상태를 “정책이 안 먹는다”로만 보면 헤맨다. S3는 public 노출을 막는 상위 안전장치를 별도로 두고 있고, 의도적으로 공개해야 하는 정적 사이트라도 먼저 CloudFront/OAC 구조가 가능한지 확인해야 한다.
S3 보안은 전송 중 암호화, 저장 시 암호화, 이상 접근 탐지를 나누어 본다. 전송 중 암호화는 HTTPS로 클라이언트와 S3 사이의 네트워크 구간을 보호하는 것이고, 위 Bucket Policy의 aws:SecureTransport Deny는 이 층을 강제한다. 저장 시 암호화는 객체가 S3 내부 저장 매체에 기록될 때 서버 측에서 암호화하는 방식이며, SSE-S3는 S3 관리 키를 쓰고 SSE-KMS는 KMS 키 정책·감사·회전 제어를 함께 가져간다. Default encryption은 새 객체 업로드 시 누락 없이 서버 사이드 암호화가 적용되도록 하는 버킷 기본값으로 이해하면 된다. GuardDuty S3 protection은 CloudTrail S3 데이터 이벤트 같은 접근 신호를 분석해 비정상 대량 다운로드, 평소와 다른 위치·주체의 접근처럼 의심스러운 패턴을 탐지하는 층이다. KMS 키 운영과 시크릿 생명주기는 뒤의 Secrets Management 문서에서 더 깊게 다루므로, 여기서는 “S3 권한과 암호화 경계가 분리된다”는 정도만 잡으면 된다.
**Versioning(버전 관리)**은 같은 key로 객체를 덮어쓰거나 삭제해도 이전 버전을 보관하는 기능이다. Versioning이 켜진 버킷에서 삭제하면 실제 데이터가 즉시 사라지는 대신 Delete Marker가 최신 버전처럼 올라올 수 있다. “삭제했는데 목록에는 안 보이지만 비용은 남는” 상황은 대개 이전 버전이나 Delete Marker를 봐야 한다.
**Presigned URL(사전 서명 URL)**은 권한을 가진 서버가 특정 S3 작업에 대해 시간 제한 서명을 붙인 URL이다. 클라이언트는 AWS 자격증명을 직접 받지 않고도, 그 URL이 허용한 key와 method에 대해서만 제한된 시간 동안 업로드나 다운로드를 할 수 있다.
이 패턴의 핵심은 “권한 위임 범위를 URL에 고정한다”는 점이다. URL을 만들 때 Bucket, Key, HTTP method, 만료 시간, 업로드라면 Content-Type 같은 조건이 서명에 들어간다. 클라이언트가 서명에 포함된 조건과 다르게 요청하면 SignatureDoesNotMatch가 난다.
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "ap-northeast-2" });
export async function createUploadUrl(input: { bucket: string; key: string; contentType: string;}) { const command = new PutObjectCommand({ Bucket: input.bucket, Key: input.key, ContentType: input.contentType, });
return getSignedUrl(s3, command, { expiresIn: 300 });}async function uploadWithPresignedUrl(url: string, file: File) { await fetch(url, { method: "PUT", body: file, headers: { "Content-Type": file.type, }, });}두 번째 코드의 Content-Type은 URL 생성 시 지정한 값과 같아야 한다. 다르면 같은 파일이어도 서명이 깨진 요청이 된다.
**CORS(Cross-Origin Resource Sharing)**는 브라우저가 다른 origin의 리소스에 접근할 때 서버가 허용 여부를 알려주는 보안 규칙이다. Presigned URL로 브라우저가 S3에 직접 PUT을 보내려면 버킷 CORS 설정이 해당 origin, method, header를 허용해야 한다. CORS는 IAM 권한을 대체하지 않는다. IAM/Bucket Policy가 허용해도 브라우저 CORS가 막으면 브라우저 요청은 실패하고, CORS가 허용돼도 S3 권한이 없으면 S3가 거부한다.
Presigned URL 실패는 HTTP status보다 “서명된 조건 중 무엇이 깨졌는가”로 읽는다.
| 실패 신호 | 개념적 원인 | 먼저 떠올릴 것 |
|---|---|---|
Request has expired | URL에 서명된 만료 시간이 지남 | 발급 후 사용 지연, 클라이언트 재발급 흐름 |
SignatureDoesNotMatch | 서명된 요청 조건과 실제 요청이 다름 | 서버/클라이언트 시계, method, key, header |
| 업로드 서명 불일치 | 서명 당시 Content-Type과 실제 업로드 값 불일치 | URL 생성 입력과 업로드 header를 같은 값으로 고정 |
Authorization header 추가 | Presigned URL 자체가 인증 정보인데 별도 인증을 섞음 | 브라우저/fetch 공통 header 주입 여부 |
| 브라우저 CORS 실패 | S3 권한이 아니라 browser origin 규칙에서 차단 | allowed origin, method, header |
사용자 업로드를 서버가 전부 받아 S3로 다시 보내면 서버의 네트워크, 메모리, timeout이 병목이 된다. Presigned URL 패턴은 서버가 “이 key에 5분 동안 PUT 가능”이라는 권한만 발급하고, 실제 바이트 전송은 클라이언트와 S3 사이에서 일어나게 한다.
1. 클라이언트 → API 서버: 업로드할 파일 이름과 Content-Type 전달2. API 서버 → S3 서명: bucket/key/method/만료 시간을 넣어 Presigned URL 생성3. 클라이언트 → S3: Presigned URL로 직접 PUT4. S3 Event Notification → API/Queue: 업로드 완료 후 후처리 트리거큰 파일은 **multipart upload(멀티파트 업로드)**를 고려한다. Multipart upload는 하나의 큰 객체를 여러 part로 나누어 병렬 업로드한 뒤 마지막에 합치는 S3 API다. 일반적으로 5MB 이상 part를 사용하고, 최대 5TB 객체까지 다룰 수 있다. 실패한 part만 재시도할 수 있어 대용량 업로드에서 전체 재전송 비용을 줄인다.
CloudFront는 AWS의 CDN(Content Delivery Network)이다. 사용자는 가까운 edge location에서 캐시된 파일을 받고, 캐시에 없을 때만 origin인 S3로 요청이 간다. **OAC(Origin Access Control)**는 CloudFront가 비공개 S3 버킷에 접근할 수 있게 해주는 제어 방식이다.
사용자 → CloudFront(HTTPS, 캐시, edge)CloudFront → S3(OAC로만 접근 허용)S3 버킷 → public access 차단 유지S3 버킷을 public으로 열고 https://bucket.s3.amazonaws.com/key를 직접 배포하면 보안과 비용 양쪽이 약해진다. 캐시가 없어 S3 요청 수가 그대로 늘고, 버킷 정책 실수로 원하지 않는 객체가 노출될 수 있다. 공개 정적 자산은 “S3는 origin, CloudFront가 public entrypoint”로 두는 편이 안전하다.
로그와 분석 원본은 시간이 지날수록 읽기 패턴이 바뀐다. 최근 로그는 장애 대응 때문에 자주 열고, 오래된 로그는 감사나 재처리 때만 연다. 그래서 lifecycle과 prefix 설계를 함께 잡는다.
ECS/EC2 → CloudWatch Logs 또는 수집기 → S3key 예시: logs/service=checkout/dt=2026-07-08/part-0001.jsonLifecycle: Standard → Standard-IA(30일) → Glacier 계열(90일) → 삭제 또는 Deep Archiveservice=.../dt=.../ 같은 key 규칙은 사람이 찾기 쉽고 쿼리 엔진도 partition처럼 사용할 수 있다. 날짜나 순차 이름 자체가 성능상 나쁜 것은 아니며, 일반 목적 버킷에서는 이런 읽기 쉬운 이름을 그대로 사용할 수 있다. 여러 prefix 분산은 요청 수치 하나만 보고 추가하지 않는다. 예측한 고요청률과 엄격한 latency SLO를 재현한 사전 부하 테스트에서 bounded prefix가 필요하다고 확인되거나, S3의 자동 scaling 뒤에도 지속적인 503·tail latency·요청 쏠림 때문에 목표를 충족하지 못할 때 검토한다.
key 설계는 세 요구가 충돌한다.
| 요구 | 좋은 점 | 위험 |
|---|---|---|
| 사람이 읽기 쉬운 날짜 경로 | 콘솔 탐색, Athena partition 추론 쉬움 | 높은 요청률이 활성 날짜 prefix에 집중되면 확장 신호를 관측해야 함 |
| hash/random prefix | 여러 prefix로 요청을 분산하기 쉬움 | LIST 범위와 논리적 인접성이 깨지고 lifecycle rule이 복잡해짐 |
| tag 기반 lifecycle | key 구조와 보관 정책을 분리 | 업로드 시 tag 누락이 비용 정책 누락으로 이어질 수 있음 |
그래서 한 가지 정답보다 워크로드별 균형이 중요하다. 소규모 서비스의 사용자 이미지라면 uploads/user-id/yyyy/mm/...처럼 읽기 쉬운 key가 충분하다. 높은 요청률의 로그 수집 파이프라인도 먼저 날짜 경로를 유지하고 요청률과 503을 측정한다. 분산이 필요하다는 신호가 확인되면 고정 개수의 shard를 날짜 뒤에 넣거나, hash prefix를 앞에 두고 분석용 partition을 뒤쪽 경로 또는 object tag로 표현한다.
**Prefix(접두사)**는 S3 key의 앞부분 문자열이다. logs/2026-07-14/app-001.log에서 logs/2026-07-14/를 prefix로 볼 수 있다. 일반 목적 버킷에서 AWS가 제시하는 최소 성능 기준은 각 partitioned S3 prefix당 초당 최소 3,500 PUT/COPY/POST/DELETE 또는 최소 5,500 GET/HEAD다. 이는 특정 workload의 실제 처리량을 보장하는 SLA가 아니며 요청 크기, 분포, connection, 암호화와 클라이언트 구성에 따라 결과가 달라진다. 버킷 안의 prefix 개수에는 제한이 없고, 여러 prefix로 요청을 병렬화하면 처리량을 더 늘릴 수 있다.
이 수치는 버킷 전체의 고정 상한도, 넘는 즉시 실패하는 quota도 아니다. S3는 지속되는 새 요청률에 맞춰 자동 확장하지만, scaling은 즉시 끝나지 않고 점진적으로 진행된다. 그래서 이전보다 훨씬 높은 요청률로 갑자기 뛰는 동안 일시적인 503 Slow Down이 나타날 수 있고, 확장이 끝나면 보통 사라진다.
여기서 오래된 조언과 현재 경계를 구분해야 한다. 예전에는 성능을 위해 key 앞부분을 hash로 무작위화하라는 지침이 있었지만, AWS의 현재 일반 목적 버킷 지침에서는 날짜 기반 순차 prefix를 사용할 수 있고 성능만을 위해 무조건 randomize할 필요가 없다. 다음과 같은 key는 일반적으로 정상적인 첫 설계다.
logs/service=checkout/dt=2026-07-14/hour=09/part-000001.jsonlogs/service=checkout/dt=2026-07-14/hour=09/part-000002.json이 구조는 같은 날짜의 key가 사전식 LIST에서 모이고, Prefix=logs/service=checkout/dt=2026-07-14/로 범위를 좁히기 쉽다. Athena 같은 분석 계층도 service, dt, hour를 partition column으로 연결하기 쉽다. 목표 처리량을 충족하고 일시적인 503이 scaling 뒤 사라진다면, 이 조회·관리 이점을 버리고 hash나 random prefix를 추가할 이유가 없다.
분산은 이름 규칙의 기본 의무가 아니라 측정에 따른 선택이다. 3,500/5,500은 shard를 추가하는 임계값이나 quota가 아니므로, 요청률이 이 수치를 넘었다는 사실만으로 key 구조를 바꾸지 않는다. 다음 신호를 함께 본다.
5xxErrors, S3 Storage Lens의 503 수, server access log에서 503이 일시적으로 나타났다 사라지지 않고 지속되는가요청률이 높아도 사전 부하 테스트에서 목표 처리량과 latency SLO를 충족하거나, 운영 중 503이 scaling 뒤 사라지면 날짜·순차 prefix를 유지한다. 반대로 사전 테스트에서 bounded prefix만 목표를 충족했거나, 운영에서 지속적인 503·tail latency·분포 skew 때문에 목표를 충족하지 못하고 다른 병목도 아니라면 여러 prefix나 고정 shard를 후보로 검토한다. 새 트래픽은 급격히 전환하지 말고 점진적으로 올리며, 503 Slow Down에는 AWS SDK의 지수 백오프와 jitter를 적용한다.
처리량 감각은 독립적으로 partition된 prefix에 요청이 고르게 분산된다는 전제에서만 선형으로 잡는다. GET이 prefix당 최소 5,500/sec라면 10개 prefix의 55,000 GET/sec, 100개 prefix의 550,000 GET/sec는 aggregate throughput의 산술적 감각을 주지만, 필요한 shard 수를 계산하는 공식은 아니다.
prefix 1개: 5,500 GET/secprefix 10개: 55,000 GET/secprefix 100개: 550,000 GET/sec이는 prefix 수만 곱한 산술 예시이지 workload SLA는 아니다. 실제 성능은 요청 크기, connection 수, 네트워크, SDK retry, SSE-KMS quota, downstream 처리량에 따라 달라진다. 또한 여러 prefix 중 하나에 요청의 90%가 몰리면 prefix 개수만 10개라고 해서 10배가 되지 않는다.
하루치 checkout 로그를 저장하고 날짜별로 LIST·Athena 조회·lifecycle 만료를 수행한다고 하자. 여기서 locality는 S3 내부 디스크 배치를 뜻하지 않고, 관련 key가 사전식 LIST와 논리 partition에서 가까이 모이는 정도를 뜻한다.
| 전략 | key 예시 | 요청 분산 | LIST·query·locality trade-off | 선택 기준 |
|---|---|---|---|---|
| 날짜·순차 prefix | logs/service=checkout/dt=2026-07-14/part-000001.json | 활성 날짜 prefix에 요청이 모임 | 날짜 prefix 한 번으로 LIST 범위를 좁히고 Athena partition pruning과 lifecycle rule을 구성하기 쉬움 | 사전 테스트나 운영에서 목표 처리량·latency SLO를 충족하는 기본 선택 |
| 날짜 뒤 고정 hash shard | logs/service=checkout/dt=2026-07-14/shard=0f/part-000001.json | 00~0f 같은 제한된 prefix에 고르게 분산 가능 | 날짜 상위 prefix로 전체 LIST가 가능하고 Athena에서 dt, shard를 함께 partitioning할 수 있지만 객체 하나를 찾으려면 shard 계산 규칙이 필요함 | 사전 테스트에서 필요성이 재현됐거나 운영의 지속적인 목표 미달 원인이 prefix별 skew로 확인됐을 때 |
| hash-first prefix | 0f/logs/service=checkout/dt=2026-07-14/part-000001.json | 앞쪽 shard로 요청 분산이 명확함 | 날짜별 LIST는 모든 shard를 조회해 합쳐야 하고 prefix 기반 lifecycle·catalog 위치가 여러 갈래가 됨 | bounded prefix 필요성이 확인됐고 소비자가 이미 manifest/catalog를 사용할 때 |
| random/UUID-first prefix | 7b1a3c9e/uploads/2026-07-14/file-001.jpg | 쓰기가 매우 넓게 흩어질 수 있음 | 날짜나 서비스로 시작하는 공통 prefix가 없어 LIST 기반 탐색이 어렵고, 전체 key를 DB·inventory·manifest에 보관해야 함 | 분산 필요성이 확인됐으며 key를 항상 외부 인덱스로 찾고 prefix 조회가 거의 없을 때 |
고정 hash shard는 같은 object id로 shard를 다시 계산할 수 있고 shard 개수도 제한할 수 있다. 반면 object마다 독립 random/UUID를 앞에 붙이면 분산 폭은 넓지만 key를 잃었을 때 의미 조건만으로 찾기 어렵다. 따라서 높은 요청률에서도 random-first를 자동 선택하지 말고, 날짜 뒤에 shard=00..0f를 두는 절충안을 먼저 검토할 수 있다.
요청률을 추정하는 기존 배치 예시도 이 기준으로 다시 읽을 수 있다. 1분에 60만 개 객체를 같은 날짜 prefix로 PUT하면 평균 10,000 PUT/sec다. 이를 3개 prefix로 나누면 평균 약 3,333 PUT/sec라는 계산은 가능하지만, 3개가 필수라는 뜻은 아니다. 출시 전에 10,000 PUT/sec와 예상 burst, latency SLO를 재현하고 단일 prefix와 4개·8개·16개 bounded prefix를 같은 조건에서 비교한다. 단일 prefix도 목표를 충족하면 그대로 유지하고, bounded prefix에서만 목표를 충족할 때 가장 작은 후보를 선택한다. 운영 중이라면 S3가 새 요청률에 맞춰 자동 확장하도록 트래픽을 점진적으로 올린 뒤 실제 처리량과 503의 지속 여부를 측정한다. 확장 뒤에도 목표를 못 맞추고, 실제 peak 14,000 PUT/sec에서 한 prefix나 shard로 40%가 몰려 5,600 PUT/sec를 받는 skew가 지속적인 503·tail latency와 함께 관측될 때 같은 bounded 후보를 다시 검토한다. 두 경로 모두 3,500/5,500으로 shard 수를 계산하지 않고, 목표를 충족하면서 LIST·query 복잡도를 가장 적게 늘리는 수를 고른다.
반대로 같은 날짜 prefix가 900 PUT/sec이고 배치 전환 때도 503이나 retry 증가가 없다면, 16개 hash prefix를 미리 붙이는 것은 성능 개선이 아니라 LIST·query·lifecycle 복잡도만 늘리는 선택이다. 순차 이름도 충분히 가능하다는 경계가 여기서 중요하다.
운영에서 bounded multi-prefix 검토로 이어지는 조건은 자동 scaling 이후에도 목표 처리량 미달이 지속되고, 같은 구간에 tail latency 증가 또는 prefix별 요청·오류 분포의 skew가 함께 확인되는 경우다. 특정 날짜나 upload prefix에 오류가 집중된다는 사실만으로는 충분하지 않으며, 초기의 일시적 503만 보일 때도 key 이름을 보고 hot prefix라고 단정하지 않는다.
📖 공식 출처(확인일: 2026-07-14): Best practices design patterns: optimizing Amazon S3 performance — partitioned prefix당 최소 요청률과 점진적 scaling, Performance guidelines for Amazon S3 — 503 관측 방법, AWS Whitepaper: Best Practices Design Patterns: Optimizing Amazon S3 Performance — hash randomization이 필수가 아니며 순차 날짜 prefix가 가능하다는 경계
대량 삭제는 특히 조심해야 한다. DeleteObjects는 한 번에 여러 key 삭제를 요청하고, 응답 본문에 Deleted와 Errors 배열을 돌려준다. 과거 aws-sdk-go 이슈로 503 재시도 뒤 HTTP 200이 왔지만 Deleted: []처럼 0건 삭제가 보고된 사례가 있다. 위험한 점은 호출자가 HTTP status만 보고 성공 처리할 수 있다는 것이다.
“재시도 후 일부만 성공”은 prefix 분산 신호가 아니라 DeleteObjects 응답과 멱등 재시도 결과를 검증해야 한다는 신호다. 호출자는 요청한 key와 Deleted·Errors를 key별로 대조하고, 재시도 뒤 최종 객체 상태가 의도와 같은지 확인해야 한다. Versioning이 켜져 있으면 현재 목록만 보지 말고 version과 Delete Marker까지 확인해야 같은 삭제를 재시도한 결과를 올바르게 판단할 수 있다.
대량 삭제 코드는 반드시 요청한 객체 수와 응답의 삭제 수를 비교해야 한다.
const requested = objects.length;const deleted = response.Deleted?.length ?? 0;const errors = response.Errors?.length ?? 0;
if (deleted !== requested || errors > 0) { throw new Error( `DeleteObjects incomplete: requested=${requested}, deleted=${deleted}, errors=${errors}`, );}운영 확인도 같은 원리다. 삭제 직후 prefix의 남은 버전 수나 객체 수를 샘플링하고, Versioning이 켜져 있으면 Delete Marker와 이전 버전을 함께 확인한다.
📖 출처: aws-sdk-go Issue #3707 — DeleteObjects fails silently on retry after 503, AWS S3 Performance Design Patterns 공식 문서
긴 진단 절차보다 먼저 원리를 잡는다. S3 문제는 대개 “권한”, “서명 조건”, “브라우저 CORS”, “네임스페이스 오해”, “비용 변수”, “prefix 집중” 중 하나다.
| 증상 | 의미하는 원리 | 먼저 볼 것 |
|---|---|---|
AccessDenied | IAM Allow가 없거나 Bucket Policy/SCP가 Deny | 호출 주체 Role, s3:GetObject/s3:PutObject, 버킷 정책의 Deny |
SignatureDoesNotMatch | Presigned URL에 서명된 조건과 실제 요청 불일치 | 만료 시간, HTTP method, key, Content-Type, 서버 시계 |
| 브라우저 CORS 에러 | 브라우저 origin/method/header 허용 누락 | 버킷 CORS의 AllowedOrigins, AllowedMethods, AllowedHeaders |
| PUT 성공 후 콘솔에서 안 보임 | 다른 bucket/key 또는 Versioning/Delete Marker | 업로드 응답의 bucket/key, 콘솔 key 검색, Show versions |
| S3 요금 급증 | GB 저장량 외 요청 수·전송량·이전 버전 누적 | Cost Explorer의 Requests/Data Transfer, Storage Lens, lifecycle |
간헐적 503 Slow Down | 새 고요청률에 맞춘 scaling 중이거나 요청 분포가 치우침 | prefix별 요청률·503 분포, 트래픽 증가 속도, retry backoff와 jitter |
| 삭제 API 성공인데 객체가 남음 | 응답 본문 확인 누락 또는 Versioning 영향 | Deleted/Errors 개수, 남은 object/version count |
재시도는 즉시 반복보다 지수 백오프(exponential backoff)와 jitter가 기본이다. 대용량 요청은 느린 요청 일부를 재시도하고, 작은 요청은 짧은 지연 후 재시도하는 식으로 client-side 폭주를 줄인다. 중요한 것은 “재시도했다”가 아니라 “재시도 결과가 요청한 객체 수와 맞는지 검증했다”다.
첫 진단은 다음 순서로 좁히면 된다.
이 순서는 운영 절차라기보다 개념 분해다. S3 문제를 “권한이 이상하다” 한 문장으로 뭉개지 않고, principal, resource, request condition, browser policy, storage state로 나누는 연습이다.
S3에서 배운 원리는 GCS, Azure Blob Storage, MinIO 같은 다른 객체 스토리지에도 상당 부분 전이된다. 서비스 이름은 다르지만 질문은 비슷하다.
| 질문 | S3 답변 | 다른 시스템에서 확인할 것 |
|---|---|---|
| 네임스페이스 구조 | Flat namespace, /는 key 안의 문자 | 계층형 namespace가 실제인지, 콘솔 표현인지 |
| Consistency 보장 | 단일 리전 GET/PUT/LIST 강한 일관성, 지역 복제는 지연 가능 | overwrite, delete, list, cross-region 복제의 보장 범위 |
| 대용량 업로드 | Multipart upload | GCS Parallel Composite, Azure Block Blob 같은 청킹/커밋 모델 |
| 접근 권한 위임 | Presigned URL | GCS Signed URL, Azure SAS Token의 만료·권한 범위 |
| 비용 변수 | 저장 GB + 요청 수 + 데이터 전송 + 복원/모니터링 비용 | 요청 과금과 egress 과금이 병목인지 |
| 장애 내성 | Standard는 다중 AZ, 일부 class는 단일 AZ | 단일 AZ/다중 AZ/지역 복제의 내구성·가용성 trade-off |
**Content Addressable Storage(CAS)**와도 연결된다. Git의 .git/objects는 파일 내용을 해시해 key로 삼는다. 내용이 같으면 key가 같아지고, 새 버전은 기존 객체를 덮어쓰기보다 새 객체 그래프로 표현된다. S3의 key는 사람이 정하는 문자열이라는 점이 다르지만, “객체를 key로 찾는다”는 사고방식은 같다.
MinIO는 S3 API와 호환되는 오픈소스 객체 스토리지다. S3 SDK를 그대로 쓰면서 온프레미스나 Kubernetes 환경에 객체 스토리지 모델을 가져오고 싶을 때 만난다. 그래서 S3의 bucket/key/presigned URL/multipart upload 개념을 알면 MinIO를 처음 볼 때도 구조를 빨리 잡을 수 있다.
새 스토리지를 만났을 때는 이름보다 실패 모드를 먼저 묻는다.
rename이 원자적인가, 아니면 copy/delete인가?이 질문들은 S3뿐 아니라 GCS, Azure Blob, MinIO, 사내 객체 스토리지, 심지어 “파일 서버를 계속 써도 되는가”를 판단할 때도 그대로 쓸 수 있다.
S3 Express One Zone은 고성능·저지연 접근을 위해 단일 AZ에 데이터를 두는 S3 스토리지 클래스다. 일반 purpose bucket과 다른 Directory Bucket 타입을 사용하고, 매우 높은 요청률과 단일 자릿수 밀리초 지연을 목표로 설계된 계열로 이해하면 된다. 가격 인하나 “최신” 수치처럼 변하기 쉬운 정보보다, 학습에서는 trade-off를 먼저 기억한다.
Directory Bucket은 S3 Express One Zone에서 쓰는 저지연용 버킷 타입이다. zonal endpoint는 특정 AZ에 붙은 엔드포인트라는 뜻이고, Directory Bucket은 이 엔드포인트와 디렉터리형 key 처리를 전제로 하므로 일반 purpose bucket과 기능·제약이 다를 수 있다.
핵심 질문은 하나다. AZ가 통째로 사라져도 이 데이터는 재생산 가능한가?
S3 Standard는 여러 AZ에 분산 저장되고 11-nine 내구성을 목표로 하지만, Express One Zone은 단일 AZ 장애의 영향을 더 크게 받는다. 따라서 “핫 워킹셋은 Express One Zone, 원본과 장기 보관본은 Standard 계열”처럼 2-tier로 설계하는 편이 안전하다.
📖 더 보기: Amazon S3 Express One Zone 공식 페이지 — 고성능 스토리지 클래스 사양, 가격, 사용 사례
| 개념 A | 개념 B | 차이점 |
|---|---|---|
| S3 | EBS | S3는 객체 스토리지, EBS는 EC2에 붙이는 블록 스토리지 |
| Bucket | Prefix | Bucket은 컨테이너, prefix는 key 앞부분 문자열 |
| Bucket Policy | IAM Policy | Bucket Policy는 버킷에 붙는 리소스 정책, IAM Policy는 사용자/Role에 붙는 정책 |
| ACL | Bucket Policy | ACL은 오래된 객체/버킷 권한 모델, Bucket Policy는 JSON 기반 리소스 정책 |
| Public Access | Presigned URL | Public은 누구나 접근, Presigned URL은 시간 제한 임시 접근 |
| Lifecycle Policy | Intelligent-Tiering | Lifecycle은 규칙 기반 전환, Intelligent-Tiering은 객체별 접근 패턴 기반 자동화 |
| Durability | Availability | Durability는 데이터 손실 방지, Availability는 지금 요청에 응답할 수 있는지 |
aws s3 ls s3://bucket-name --recursive | headaws s3api get-bucket-policy-status --bucket bucket-nameaws s3api list-object-versions --bucket bucket-name --prefix path/to/keyAccessDenied가 나면 먼저 호출 주체가 어떤 IAM Role인지 확인한다. 그 다음 IAM Policy, Bucket Policy의 Explicit Deny, Block Public Access, SCP/Permission Boundary 순서로 좁힌다.
[ { "AllowedHeaders": ["*"], "AllowedMethods": ["GET", "PUT", "POST"], "AllowedOrigins": ["https://app.example.com"], "ExposeHeaders": ["ETag"] }]Presigned URL 직접 업로드에서 브라우저가 막히면 CORS를 본다. 같은 요청을 서버나 CLI에서 보내면 성공하는데 브라우저에서만 실패한다면 권한보다 CORS일 가능성이 높다.
S3 Event Notification, Lifecycle Policy, Cross-Region Replication, Transfer Acceleration, CloudFront + S3, Intelligent-Tiering, S3 Prefix Partitioning, Content Addressable Storage, Multipart Upload, MinIO, Object Lock
bucket + key → object로 조회하는 AWS 객체 스토리지다.