콘텐츠로 이동

S3 Basics

분류: Layer 3 - AWS 인프라 & 보안

S3(Simple Storage Service)는 AWS의 객체 스토리지 서비스로, 파일을 무제한으로 저장하고 꺼낼 수 있는 클라우드 저장소이다.

이미지, 로그, 백업, 정적 파일, 데이터 아카이브 등 거의 모든 종류의 파일 저장에 S3가 쓰인다. AWS에서 가장 기본적이고 가장 많이 쓰이는 서비스 중 하나.

Bucket (버킷)

파일을 담는 최상위 컨테이너. 이름이 전 세계적으로 고유해야 한다.

Object (객체)

S3에 저장된 파일 하나. 파일 자체 + 메타데이터로 구성. Key(경로)로 식별한다.

  • s3://my-bucket/images/profile.jpg → Bucket: my-bucket, Key: images/profile.jpg

S3가 내부적으로 어떻게 동작하는가

비유: S3는 “무한히 확장되는 클라우드 창고”이다. 일반 파일시스템(폴더 구조)과 달리, S3는 Flat(평탄한) 구조이다.

핵심 차이:

  • 일반 파일시스템: 실제 폴더가 존재함
  • S3: “폴더”는 없음. images/profile.jpgimages/라는 폴더가 아니라, images/profile.jpg라는 이름의 Key를 가진 객체 1개

콘솔에서 폴더처럼 보이는 건 AWS 콘솔이 /를 기준으로 시각적으로 분리해서 표시해주는 것이다. 실제로는 Key가 긴 객체일 뿐이다.

데이터 내구성: S3는 내부적으로 데이터를 여러 AZ에 분산 저장한다. Erasure Coding으로 일부 디스크가 손상돼도 데이터를 복구할 수 있다. 공식 내구성 99.999999999%(11-nine).

📖 더 보기: Object Storage Architecture of AWS S3 — S3의 Flat 구조, Erasure Coding, 분산 저장 메커니즘을 그림과 함께 설명 (중급)

Storage Class (스토리지 클래스)

  • Standard: 자주 접근하는 데이터 (기본값)
  • Infrequent Access (IA): 가끔 접근. Standard보다 저렴. 단, 최소 30일 보관 요구
  • Glacier Instant Retrieval: 아카이브 + 즉시 복원 가능. 최소 90일 보관
  • Glacier Deep Archive: 장기 아카이브. 매우 저렴하지만 복원에 12시간 소요. 최소 180일 보관
  • Intelligent-Tiering: 접근 패턴을 자동 분석하여 최적 클래스로 자동 이동. 예측이 어려운 데이터에 유용

Lifecycle Policy — 비용 절감의 핵심

비유: Lifecycle Policy는 “자동 파일 정리 규칙”이다. “업로드한 지 30일이 지난 로그 파일은 IA로 이동하고, 90일이 지나면 Glacier로, 365일이 지나면 삭제”처럼 규칙을 설정하면 S3가 알아서 처리한다.

일반적인 Lifecycle 전략으로 스토리지 비용을 30~70% 절감할 수 있다:

Day 0: Standard (업로드 직후)
Day 30: Standard-IA로 전환 (접근 빈도 감소)
Day 90: Glacier Instant Retrieval로 전환
Day 365: Glacier Deep Archive로 전환 (또는 삭제)

AWS 콘솔에서 설정하는 방법:

S3 → 버킷 선택 → Management 탭 → Lifecycle rules → Create lifecycle rule
→ Transition actions: 원하는 날짜에 클래스 전환 설정
→ Expiration actions: 특정 날짜 이후 자동 삭제 설정

주의: IA/Glacier 전환 시 최소 보관 기간이 있어, 너무 빨리 삭제하면 잔여 기간에 대한 요금이 부과된다.

Intelligent-Tiering — 접근 패턴을 모를 때의 베스트 선택 (2025)

접근 패턴이 불규칙하거나 예측이 어려운 데이터라면 Intelligent-Tiering이 최선이다. 내부적으로 Frequent Access / Infrequent Access / Archive Instant Access 세 계층을 자동으로 이동하며, 별도 검색 비용이 없다.

30일 미접근 → Infrequent Access tier (Standard-IA보다 저렴)
90일 미접근 → Archive Instant Access tier
→ 다시 접근하면 Frequent Access tier로 즉시 복귀 (검색 지연 없음)

제약: 128KB 미만 객체는 자동 티어 이동이 안 됨. 객체당 월 $0.0025의 모니터링 요금 발생.

실제 절감 효과: AWS 발표에 따르면 S3 Intelligent-Tiering 사용자들은 Standard 대비 평균 67% 스토리지 비용 절감을 달성했다.

S3 보안 하드닝 — 2025년 체크리스트

  1. HTTPS 강제 적용 (Bucket Policy)

    HTTP로 오는 요청을 차단한다. 데이터 전송 중 암호화를 강제하는 버킷 정책이다.

    {
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:*",
    "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
    "Condition": {
    "Bool": {
    "aws:SecureTransport": "false"
    }
    }
    }
  2. 서버 사이드 암호화 강제 (SSE)

    모든 업로드 시 암호화를 강제하려면 버킷의 Default Encryption을 활성화한다.

    S3 → 버킷 → Properties → Default encryption → SSE-S3 또는 SSE-KMS 선택
    → 이후 업로드되는 모든 객체는 자동으로 암호화
  3. GuardDuty S3 위협 탐지 활성화

    GuardDuty는 S3 데이터 접근 이벤트를 지속적으로 분석하여 이상 접근(비정상적인 IP에서 대량 다운로드 등)을 자동 탐지한다.

    경로: GuardDuty → Settings → Enable → S3 Protection 활성화
    → 이상 접근 감지 시 CloudWatch Events로 알림 수신 가능

Bucket Policy / ACL

버킷 레벨의 접근 권한 설정. IAM Policy와 함께 “누가 이 버킷의 파일을 읽기/쓰기 가능한지”를 정의.

⚠️ 퍼블릭 접근 주의

기본적으로 S3 버킷은 비공개다. “Block Public Access” 설정이 기본으로 켜져 있다.

이 설정을 끄면 전 세계 누구나 버킷 내용에 접근할 수 있다.

민감 데이터(개인정보, 로그, 백업)가 있는 버킷은 절대 퍼블릭으로 열지 않는다.

퍼블릭 파일 제공이 필요하면 Presigned URL 또는 CloudFront를 사용한다.

Versioning (버전 관리)

같은 Key로 파일을 덮어써도 이전 버전을 보관. 실수로 삭제해도 복구 가능.

주의: Versioning 활성화 후 삭제하면 실제 삭제가 아닌 “Delete Marker”가 생성된다. 완전 삭제하려면 Delete Marker도 함께 제거해야 한다.

Presigned URL — 왜 필요한가

비유: Presigned URL은 “일회용 입장권”이다. S3 버킷은 잠겨 있지만(비공개), 이 티켓을 가진 사람은 지정된 시간 동안 특정 파일에 접근할 수 있다.

사용 시나리오: 사용자가 파일을 업로드하거나 다운로드할 때, 서버가 Presigned URL을 생성해서 클라이언트에게 전달한다. 클라이언트는 이 URL로 S3에 직접 업로드/다운로드 — 서버를 거치지 않아 트래픽 절약.

NestJS에서 Presigned URL 생성 예시:

// npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
import {
S3Client,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3Client = new S3Client({ region: "ap-northeast-2" });
// 다운로드 URL (10분)
async function getPresignedDownloadUrl(
bucket: string,
key: string,
): Promise<string> {
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
return getSignedUrl(s3Client, command, { expiresIn: 600 });
}
// 업로드 URL (5분) — 클라이언트가 직접 S3에 업로드
async function getPresignedUploadUrl(
bucket: string,
key: string,
): Promise<string> {
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: "image/jpeg", // 클라이언트도 반드시 이 Content-Type으로 업로드해야 함
});
return getSignedUrl(s3Client, command, { expiresIn: 300 });
}
// 사용 예시
const url = await getPresignedDownloadUrl("my-bucket", "images/profile.jpg");
// 출력 예시:
// https://my-bucket.s3.ap-northeast-2.amazonaws.com/images/profile.jpg
// ?X-Amz-Algorithm=AWS4-HMAC-SHA256
// &X-Amz-Credential=...
// &X-Amz-Expires=600
// &X-Amz-Signature=...

📖 더 보기: NestJS에서 S3 Presigned URL 구현 — 업로드/다운로드 Presigned URL 생성부터 프론트엔드 연동까지 단계별 가이드 (중급)

  • 사용자 업로드 파일 저장 (이미지, 문서 등)
  • 애플리케이션 로그 아카이브
  • 정적 웹사이트 호스팅
  • DB 백업 저장
  • CI/CD 빌드 아티팩트 저장
  • 데이터 레이크 (분석용 원시 데이터)
  • 서비스에서 업로드된 파일이 어디에 저장되는지 이해
  • 로그 아카이브 확인 시 S3에서 찾아야 함
  • 배포 아티팩트가 S3에 있을 수 있음
  • 비용 최적화 시 스토리지 클래스 전환 검토

5.5 객체 스토리지 원리의 전이 가능성

섹션 제목: “5.5 객체 스토리지 원리의 전이 가능성”

S3에서 배운 핵심 원리는 벤더에 종속되지 않는다. GCS, Azure Blob Storage, MinIO 모두 동일한 객체 스토리지 아키텍처를 따른다.

항목AWS S3Google Cloud StorageAzure Blob StorageMinIO
키 구조Flat namespace — /는 시각적 구분자, 실제 폴더 없음Flat namespace (동일)Flat namespace. Azure Data Lake Gen2에서는 계층형 NS 선택 가능S3 호환 Flat namespace
Consistency강한 일관성 (2020년 이후 — PutObject 즉시 반영)강한 일관성 (전역)강한 일관성 (단일 리전). 지역 복제 시 eventual강한 일관성 (단일 노드/클러스터)
멀티파트 업로드Multipart Upload API (5MB~5TB 파트)Parallel Composite UploadBlock Blob 청킹 (블록 단위 커밋)S3 Multipart API 완전 호환
요청 한도3,500 PUT / 5,500 GET per prefix/sec자동 스케일 (quota 없음)2,000 req/sec per blob하드웨어 의존
Presigned URLX-Amz-Signature 서명 방식Signed URL (V4 호환)SAS Token (비슷한 역할)S3 Presigned URL 완전 호환
VersioningObject Versioning + Delete MarkerObject Versioning (유사)Blob Versioning / Soft DeleteVersioning 지원
비용 구조$0.023/GB (Standard)$0.020/GB (Standard)$0.018/GB (Hot tier)자체 호스팅 (S3 스토리지 비용 없음)

📖 출처: AWS S3 공식 문서, Airbyte — S3 vs GCS vs Azure Blob 비교

S3 원리 → 타 플랫폼 전이 공통 개념

섹션 제목: “S3 원리 → 타 플랫폼 전이 공통 개념”

Flat namespace / Key-Value 조회는 객체 스토리지의 보편 원리다. S3의 bucket + key → object 패턴은 GCS와 MinIO에서 동일하게 동작한다. 플랫폼이 바뀌어도 “폴더는 없고 Key 이름에 /가 포함될 뿐”이라는 메커니즘은 그대로 적용된다.

Eventual Consistency → 강한 일관성으로 수렴: 2020년 이전 S3는 overwrite PUT / DELETE에 대해 eventual consistency였다. 현재 S3, GCS, Azure Blob 모두 강한 일관성을 제공하지만, 지역 복제(Cross-Region Replication)를 사용하는 경우 복제 지연(보통 수 초~수 분) 동안은 여전히 eventual하다.

Presigned URL / SAS Token 패턴: “서버가 자격증명 없이 클라이언트에게 임시 접근권을 위임”하는 패턴은 클라우드 스토리지 전반에 동일하게 적용된다. S3는 X-Amz-Signature, GCS는 Signed URL, Azure는 SAS Token이라는 이름을 쓰지만 원리는 동일하다.

Content Addressable Storage(CAS)와의 연결: Git의 .git/objects는 내용의 SHA-1 해시를 Key로 사용하는 객체 스토리지다. “콘텐츠 = Key” 구조는 S3의 Key-Value 패턴과 동일한 아키텍처다. 파일 내용이 같으면 동일한 Key → 중복 저장 방지. Versioning도 같은 원리 — Git의 commit history와 S3 Object Versioning 모두 “변경 불가능한 객체 + 새 버전 추가” 패턴이다.

MinIO: S3 API를 완전히 구현한 오픈소스 객체 스토리지. S3 SDK를 그대로 사용해 온프레미스에 배포 가능하다. Kubernetes 기반 분산 배포를 지원하며, 엔터프라이즈 환경에서 멀티클라우드/하이브리드 아키텍처에 자주 사용된다.

개념 A개념 B차이점
S3EBSS3는 객체 스토리지(파일 단위), EBS는 블록 스토리지(EC2에 붙이는 디스크)
Bucket PolicyIAM PolicyBucket Policy는 버킷에 붙는 권한, IAM Policy는 사용자/역할에 붙는 권한
StandardGlacierStandard는 즉시 접근, Glacier는 아카이브용(접근에 시간 소요)
Public AccessPresigned URLPublic은 누구나 접근, Presigned URL은 시간 제한 임시 접근
Lifecycle PolicyIntelligent-TieringLifecycle은 수동 규칙 기반 전환, Intelligent-Tiering은 접근 패턴 자동 분석 전환

🔧 S3 접두사 집중으로 인한 503 SlowDown — 성능 병목

섹션 제목: “🔧 S3 접두사 집중으로 인한 503 SlowDown — 성능 병목”

증상: 대량 업로드/다운로드 작업 중 503 Slow Down 에러가 간헐적으로 발생. 재시도해도 반복됨

원인: S3는 내부적으로 요청을 prefix(접두사) 단위로 파티셔닝한다. 파티션당 처리 한도는 AWS 공식 기준으로 초당 3,500 PUT/COPY/POST/DELETE, 5,500 GET/HEAD이다. 동일한 prefix로 요청이 집중되면 파티션이 포화 상태에 이르러 503 Slow Down 응답을 반환한다.

자주 발생하는 패턴:

❌ 날짜 순차 prefix — 특정 날짜에 요청 집중
logs/2024-01-15/app-001.log
logs/2024-01-15/app-002.log
logs/2024-01-15/app-003.log
→ "logs/2024-01-15/" prefix 1개에 모든 요청 집중 → 503 SlowDown
❌ 순차 번호 prefix
uploads/00001.jpg, uploads/00002.jpg, uploads/00003.jpg
→ "uploads/" prefix 1개에 집중

AWS 공식 권고 파티셔닝 전략:

Terminal window
# 전략 1: 해시 기반 prefix 분산
# 원본 Key에서 SHA-256 앞 4자리를 prefix로 사용 → 최대 65,536개 파티션
original_key="logs/2024-01-15/app-001.log"
hash_prefix=$(echo -n "$original_key" | sha256sum | cut -c1-4)
s3_key="${hash_prefix}/${original_key}"
# 예: a3f2/logs/2024-01-15/app-001.log
# 전략 2: 타임스탬프 역순 prefix
# 최신 데이터가 다른 prefix에 분산됨
# 2024-01-15 → 5102-98-40 (역순) → prefix로 사용
# 전략 3: UUID/랜덤 prefix 추가
import uuid
s3_key = f"{uuid.uuid4().hex[:8]}/logs/2024-01-15/app-001.log"
# 예: a1b2c3d4/logs/2024-01-15/app-001.log

prefix 분산으로 달성 가능한 처리량:

prefix 1개: 5,500 GET/sec
prefix 10개: 55,000 GET/sec (10배 선형 스케일)
prefix 100개: 550,000 GET/sec

S3는 높은 요청률이 지속되면 내부적으로 파티션을 자동으로 추가 분할하지만, 이 프로세스는 점진적이며 분할 완료 전까지 503이 발생할 수 있다.

재시도 전략 (AWS 권고):

대용량 요청(>128MB): 가장 느린 5% 요청 재시도
소용량 요청(<512KB): 2초 후 재시도 → 4초 후 재시도 (exponential backoff)
503 발생 시: 즉시 재시도 X — 지수 백오프 + jitter 적용

📖 출처: AWS S3 Performance Design Patterns 공식 문서

🔧 “403 Forbidden / AccessDenied” — S3 파일 접근 거부

섹션 제목: “🔧 “403 Forbidden / AccessDenied” — S3 파일 접근 거부”

증상: aws s3 cp 또는 SDK에서 AccessDenied 에러. 콘솔에서는 보이는데 코드에서 안 됨

원인 1: IAM Policy에 s3:GetObject 또는 s3:PutObject 권한이 없음

원인 2: Bucket Policy가 해당 IAM Role을 명시적으로 Deny하고 있음

원인 3 (가장 흔한 함정): Bucket에 Block Public Access가 켜져 있는데 퍼블릭 접근을 시도함

해결:

  1. IAM Policy 확인: 해당 Role에 s3:GetObject, s3:PutObject 등이 Allow되어 있는지
  2. Bucket Policy 확인: S3 → 버킷 → Permissions 탭 → Bucket Policy에 Deny 구문이 있는지
  3. 콘솔에서 aws s3 ls s3://bucket-name으로 접근 테스트
Terminal window
aws s3 ls s3://my-bucket/
# AccessDenied 시: 위 3단계 확인
# 정상 시:
# 2024-01-15 10:00:00 1024 images/profile.jpg
# 2024-01-15 10:01:00 2048 logs/app.log

🔧 Presigned URL 관련 에러 — 서명 불일치 또는 만료

섹션 제목: “🔧 Presigned URL 관련 에러 — 서명 불일치 또는 만료”

증상: 클라이언트에서 Presigned URL로 접근 시 403 Request has expired, SignatureDoesNotMatch, 또는 400 Bad Request 에러

원인별 정리:

에러원인해결
Request has expired만료 시간 초과클라이언트에서 URL 발급 후 빠르게 사용하도록 안내, 또는 만료 전 재발급 로직 추가
SignatureDoesNotMatch서버 시계가 AWS와 5분 이상 차이EC2/컨테이너의 NTP 시간 동기화 확인
400 Bad Request업로드 시 Authorization 헤더를 추가로 붙인 경우Presigned URL 요청에는 Authorization 헤더를 추가하면 안 됨
SignatureDoesNotMatch (업로드)URL 생성 시 지정한 ContentType과 실제 업로드 ContentType이 다름클라이언트가 Content-Type 헤더를 URL 생성 시와 동일하게 보내야 함

2025년 AWS 보안 권고: Presigned URL의 유효 시간을 최대 15분으로 제한하는 것이 권장된다. 유출 시 피해를 최소화하기 위함이다.

🔧 S3 파일 업로드 후 파일이 안 보이는 경우

섹션 제목: “🔧 S3 파일 업로드 후 파일이 안 보이는 경우”

증상: PutObject 성공 응답을 받았지만 콘솔에서 파일이 보이지 않음. 또는 이전 버전의 파일이 계속 반환됨

원인 1: 버킷 이름 또는 Key(경로)를 잘못 지정해서 다른 위치에 저장됨

원인 2: Versioning이 활성화된 버킷에서 삭제 마커(Delete Marker)가 생성되어 최신 파일이 숨겨짐

해결:

  1. 업로드 응답에서 실제 저장된 Key 확인 후 콘솔에서 직접 검색
  2. Versioning이 켜진 버킷이면 “Show versions” 토글을 켜서 삭제 마커 확인

🔧 S3 CORS 에러 — 브라우저에서 파일 업로드/다운로드 실패

섹션 제목: “🔧 S3 CORS 에러 — 브라우저에서 파일 업로드/다운로드 실패”

증상: 브라우저 콘솔에서 Access to fetch at 'https://...' from origin '...' has been blocked by CORS policy 에러. Presigned URL로 직접 업로드하거나 S3 파일을 프론트엔드에서 직접 가져올 때 발생

원인: S3 버킷의 CORS 설정에 현재 도메인의 HTTP 메서드가 허용되지 않음

해결:

S3 → 버킷 → Permissions 탭 → Cross-origin resource sharing (CORS) → Edit
아래 설정 추가:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST"], ← 필요한 메서드 모두 추가
"AllowedOrigins": ["https://my-frontend.com"], ← 실제 도메인으로 변경
"ExposeHeaders": ["ETag"]
}
]

🔧 S3 비용이 예상보다 많이 나오는 경우

섹션 제목: “🔧 S3 비용이 예상보다 많이 나오는 경우”

증상: AWS Cost Explorer에서 S3 요금이 급증. 스토리지 요금이 아닌 “Requests” 또는 “Data Transfer” 항목이 주요 원인

원인 1: API 요청 수가 많아 PutObject/GetObject 요청 과금 (S3는 요청 건수로도 과금)

원인 2: 같은 리전 내 서비스 간 Data Transfer는 무료이나, 리전 간 또는 인터넷으로 나가는 트래픽은 과금

원인 3: Versioning이 켜진 버킷에 오래된 버전 데이터가 누적

해결:

Terminal window
# S3 Storage Lens로 버킷별 비용 분석
# 경로: S3 → Storage Lens → Create dashboard
# → 버킷별 스토리지 크기, 요청 수, 복제 데이터 분석 가능
# Versioning이 켜진 버킷의 오래된 버전 정리 (Lifecycle Rule)
# S3 → 버킷 → Management → Lifecycle rules → Create rule
# → "Previous versions" 대상, Expiration: 30일 후 삭제 설정
# AWS CLI로 특정 버킷의 버전 개수 확인
aws s3api list-object-versions \
--bucket my-bucket \
--query 'length(Versions)' \
--output text
# 예상 출력: 15234 ← 버전이 너무 많으면 정리 필요
  • S3의 Bucket과 Object 개념을 설명할 수 있다
  • Storage Class의 차이를 설명할 수 있다
  • S3 접근 권한이 어디서 관리되는지 안다 (Bucket Policy + IAM)
  • Presigned URL이 뭔지, 언제 쓰는지 설명할 수 있다
  • Lifecycle Policy로 비용을 절감하는 방법을 설명할 수 있다
  • 동일 prefix 집중 시 503 SlowDown이 발생하는 이유와 파티셔닝 전략을 설명할 수 있다
  • S3 Flat namespace 원리가 GCS, Azure Blob, MinIO에서도 동일하게 적용됨을 설명할 수 있다

S3 Event Notification, Lifecycle Policy, Cross-Region Replication, Transfer Acceleration, CloudFront + S3, Intelligent-Tiering, S3 Prefix Partitioning, Content Addressable Storage, MinIO

  • AWS 콘솔에서 팀 S3 버킷 목록 확인
    경로: S3 → Buckets
    확인: 버킷 이름, 리전, 접근 권한 (Public access blocked 여부)
  • 버킷 하나를 열어보고 폴더 구조와 파일 확인
    경로: S3 → <버킷 이름> → Objects 탭
    주의: "폴더"로 보이는 것은 실제로 Key에 /가 포함된 객체들의 그룹
  • AWS CLI로 버킷 파일 목록 확인
    Terminal window
    aws s3 ls s3://bucket-name --recursive | head -10
    # 예상 출력:
    # 2024-01-15 10:00:00 1024 images/profile.jpg
    # 2024-01-15 10:01:00 2048 uploads/doc.pdf
    # 2024-01-15 10:02:00 1048576 backups/db-2024-01-15.sql.gz
  • Bucket Policy가 어떻게 설정되어 있는지 확인
    경로: S3 → <버킷> → Permissions 탭 → Bucket Policy 섹션
  1. S3는 파일을 무제한으로 저장할 수 있는 AWS 객체 스토리지이며, 99.999999999%(11-nine) 내구성을 여러 AZ에 분산 저장으로 보장한다
  2. Bucket(컨테이너) + Object(파일) + Key(경로) 구조이며, “폴더”는 시각적 표현일 뿐 실제로는 Key 이름에 /가 포함된 것이다
  3. Lifecycle Policy + Intelligent-Tiering으로 스토리지 비용을 30~67% 절감할 수 있다 — 파일 특성에 따라 선택한다
  4. 접근 권한은 Bucket Policy(버킷 레벨) + IAM Policy(사용자/역할 레벨)의 교집합으로 결정된다, Presigned URL은 비공개 파일의 임시 접근에 사용한다
  5. S3 버킷은 기본 비공개이며, Block Public Access는 절대 해제하지 않는다 — 퍼블릭 파일이 필요하면 CloudFront를 앞에 둔다

CDN Static File Hosting과 S3의 관계

프론트엔드 개발 시 Netlify나 Vercel에 배포하면 알아서 CDN을 통해 전 세계 사용자에게 빠르게 정적 파일을 제공해준다. AWS에서는 이것을 S3 + CloudFront 조합으로 직접 구성한다:

[Netlify/Vercel 내부 동작 (추상화됨)]
git push → 빌드 → CDN 자동 배포
[AWS에서 동일한 구성 (직접 제어)]
git push → GitHub Actions 빌드 → S3 업로드 (버킷이 원본 저장소)
→ CloudFront Invalidation (캐시 갱신)
사용자 → CloudFront (엣지 서버, 캐시) → S3 (원본, 직접 접근 차단)

S3 버킷 자체는 비공개로 유지하고, CloudFront만 Origin Access Control(OAC)로 접근 권한을 갖는다. 이 구조가 “S3 + CloudFront”의 핵심이다.

프론트에서 파일 업로드 구현 시 두 가지 방법

React 앱에서 사용자가 이미지를 업로드할 때:

방법 1: 서버 경유 (간단하지만 서버 부하)
React → multipart/form-data → NestJS API → S3 PutObject
단점: 대용량 파일이면 NestJS 서버 메모리/CPU 사용
방법 2: Presigned URL (권장 — 서버 부하 없음)
React → NestJS API (Presigned URL 요청)
NestJS → Secrets Manager에서 자격증명 → S3 Presigned URL 생성 → React에 반환
React → Presigned URL로 S3에 직접 업로드 (NestJS 불경유)
NestJS ← S3 Event Notification (업로드 완료 알림)
// React 업로드 코드 예시
const uploadFile = async (file: File) => {
// 1. 백엔드에서 Presigned URL 받기
const { uploadUrl, fileKey } = await fetch('/api/upload-url', {
method: 'POST',
body: JSON.stringify({ fileName: file.name, contentType: file.type })
}).then(r => r.json());
// 2. S3에 직접 업로드 (서버 불경유)
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type } // Presigned URL 생성 시 지정한 것과 동일해야 함
});
};

왜 S3 파일을 CloudFront 없이 직접 serving하면 안 되나

S3 버킷을 Public으로 열어서 직접 URL(https://my-bucket.s3.amazonaws.com/...)로 서빙하면:

  • S3 요청 비용이 직접 청구됨 (CloudFront 캐시 없음)
  • 리전이 한국(ap-northeast-2)이면 해외 사용자는 느림
  • 버킷이 퍼블릭이라 잘못된 버킷 정책으로 전체 데이터가 노출될 위험

CloudFront를 앞에 두면: 캐시 히트율 높아 S3 요청 비용 90% 절감 + 전 세계 엣지 서버로 빠른 응답 + S3 버킷은 비공개 유지.

실무 아키텍처 패턴 — S3 사용 시나리오별 구성

섹션 제목: “실무 아키텍처 패턴 — S3 사용 시나리오별 구성”
[파일 업로드 패턴 — 서버 트래픽 최소화]
클라이언트 → NestJS API (Presigned URL 발급, 5~15분 유효)
클라이언트 → S3 (직접 업로드, 서버 불경유)
NestJS API ← S3 Event Notification (업로드 완료 알림)
[정적 파일 서빙 패턴 — 보안 + 성능]
클라이언트 → CloudFront (CDN, HTTPS, 캐시)
CloudFront → S3 (Origin Access Control, S3 버킷 비공개 유지)
→ S3 버킷에 퍼블릭 접근 없이 CloudFront를 통해서만 파일 제공
[로그 아카이브 패턴]
ECS/EC2 → CloudWatch Logs → S3 (Export, 30일 이후 자동 이동)
S3 Lifecycle: Standard → Standard-IA (30일) → Glacier (90일) → 삭제 (365일)

S3 Express One Zone — 2025년 주목할 신규 스토리지 클래스

2024년 말 출시 후 2025년 4월에 대폭 가격 인하(스토리지 31%, GET 요청 85%)된 고성능 스토리지 클래스이다. 단일 AZ에 저장되어 내구성은 낮지만, S3 Standard 대비 10배 빠른 접근 속도단일 자릿수 밀리초 지연시간을 제공한다.

  • 최대 처리량: 초당 200만 요청
  • 사용 사례: AI/ML 학습 데이터, 실시간 분석, 미디어 렌더링, HPC
  • 버킷 타입: 일반 S3 버킷과 다른 Directory Bucket 타입으로 생성
  • 주의: 단일 AZ이므로 AZ 장애 시 데이터 손실 위험이 있음 — 중요 데이터에는 사용하지 않는다

📖 더 보기: Amazon S3 Express One Zone 공식 페이지 — 고성능 스토리지 클래스 사양, 가격, 사용 사례 (중급)