S3 버킷 하나 붙이는 데 난리가 나는 이유

게시물에 이미지 업로드 기능을 추가하려고 AWS S3를 처음 붙이면 예상보다 훨씬 많은 설정을 건드리게 된다. 버킷 생성, IAM 사용자, 정책, 액세스 키, CORS, 그리고 Mixed Content 에러까지. 이 글은 그 흐름을 개념 중심으로 정리한다

S3 버킷이란

S3(Simple Storage Service)는 객체(Object) 단위로 파일을 저장하는 클라우드 스토리지다. 디렉토리 구조 대신 버킷(Bucket) 안에 객체를 담는 방식으로 동작한다. 버킷은 전 세계에서 고유한 이름을 가져야 하며, 객체마다 고유 URL이 자동으로 생성된다

주요 특징은 다음과 같다. 용량 제한이 사실상 없고, 가용성이 매우 높으며, 인프라 관리가 필요 없다. 비용은 저장 용량과 요청 수 기준으로 과금된다. RDS(데이터베이스) 비용보다 훨씬 저렴하다

버킷 접근 모드는 두 가지다

모드동작
PublicURL만 알면 누구나 접근 가능
Private인증된 요청만 허용. 임시 접근은 Presigned URL 사용

실제 서비스에서는 읽기는 Public, 쓰기는 Private으로 구성하는 방식이 일반적이다. 결제 사용자에게만 파일을 제공해야 한다면 버킷 전체를 Private으로 운영하고 Presigned URL을 발급한다

IAM – 권한을 부여하는 구조

S3 버킷은 수동적인 접근 대상이다. 실제로 버킷에 파일을 올리거나 읽는 주체는 API 서버(EC2)다. 이 주체에게 권한을 부여하는 체계가 IAM(Identity and Access Management)이다

개념을 사원증에 비유하면 이렇다

IAM 사용자  →  사원 (행위 주체, 예: EC2)
IAM 롤      →  사원증 (권한 묶음)
IAM 정책    →  사원증에 적힌 출입 가능 구역 목록

정책을 먼저 만들고 → 롤에 정책을 연결하고 → EC2에 롤을 부여하는 순서다

정책은 JSON 형식으로 작성한다. 아래는 특정 버킷의 PUT/GET을 허용하는 정책 예시다

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::my-bucket-name/*"
    }
  ]
}

Resource의 버킷 이름과 경로 패턴(/*)은 반드시 직접 입력해야 한다. AWS가 내 버킷 이름을 알 리 없다

IAM 사용자 생성 순서

  1. IAM 콘솔에서 정책 생성 → JSON 탭에 정책 입력
  2. 사용자 생성 → 생성한 정책과 연결
  3. 사용자 보안 자격증명 탭 → 액세스 키 생성
  4. 액세스 키와 시크릿 키는 이 화면에서만 확인 가능하다. 반드시 저장해둔다.

액세스 키는 API 서버의 환경 변수나 application-local.yml에 설정한다. application.yml에 직접 넣으면 GitHub에 올라갈 위험이 있다. 반드시 로컬 전용 설정 파일로 분리한다

# application-local.yml (Git에 포함하지 않는다)
cloud:
  aws:
    credentials:
      access-key: YOUR_ACCESS_KEY
      secret-key: YOUR_SECRET_KEY
    s3:
      bucket: my-bucket-name
    region:
      static: ap-northeast-2

CORS – 왜 설정해야 하는가

문제의 출발점 – CSRF

CORS 설정이 필요한 이유를 이해하려면 CSRF(Cross-Site Request Forgery) 공격을 알아야 한다

CSRF는 사용자가 로그인된 상태를 악용하는 공격이다. 공격자가 만든 악성 페이지에 숨겨진 <form>이 있고, 페이지가 로드되는 순간 이 폼이 자동으로 제출된다. 로그인 상태의 세션 쿠키가 함께 전송되므로, 서버는 정상 요청으로 처리한다

이 공격을 브라우저 수준에서 차단하기 위해 등장한 정책이 SOP(Same Origin Policy)다

SOP – 출처가 같아야 허용

SOP는 출처(Origin)가 다른 리소스 접근을 차단하는 브라우저 정책이다. 출처는 프로토콜 + 도메인 + 포트 조합으로 정의된다

https://www.example.com:443  →  기준 출처

https://www.example.com/path    같은 출처 (포트 동일, 기본 443)
http://www.example.com          다른 출처 (프로토콜 다름)
https://api.example.com         다른 출처 (서브도메인 다름)
https://www.example.com:8080    다른 출처 (포트 다름)

여기서 문제가 생긴다. 프론트(www.example.com)에서 API 서버(api.example.com)로 요청을 보내면 SOP가 차단한다. 이걸 선택적으로 허용하는 메커니즘이 CORS(Cross-Origin Resource Sharing)다.

CORS 설정 방법

S3 콘솔 → 버킷 → 권한 탭 → CORS 편집에서 JSON을 입력한다

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
    "AllowedOrigins": ["http://localhost:3000"],
    "ExposeHeaders": []
  }
]

로컬 개발 환경이라면 AllowedOriginshttp://localhost:포트번호를 넣는다. 운영 환경 배포 시에는 실제 도메인으로 교체한다

AllowedOrigins["*"]으로 열어두면 모든 출처가 허용되어 보안이 취약해진다. 반드시 허용할 출처를 명시한다

Mixed Content 에러

CORS 설정을 마쳐도 Mixed Content 에러가 뜨는 경우가 있다. 원인은 단순하다. 프론트가 HTTPS인데 API 서버가 HTTP일 때(또는 그 반대일 때) 발생한다

프론트: https://www.example.com   (HTTPS)
API:    http://api.example.com    (HTTP)
                                  ← Mixed Content 에러

브라우저는 HTTPS 페이지에서 HTTP 리소스 로딩을 차단한다. 해결 방법은 프론트와 백엔드의 프로토콜을 통일하는 것이다

전체 연동 흐름 정리

[프론트] → 이미지 업로드 요청
    ↓
[API 서버] → IAM 권한으로 S3에 PutObject
    ↓
[S3] → 파일 저장 + 고유 URL 생성
    ↓
[API 서버] → URL을 DB에 저장 후 응답
    ↓
[프론트] → URL로 이미지 렌더링

새 게시물 작성 시 이미지를 포함하려면 임시 저장(Temp ID) 방식이 필요하다. 게시물 ID가 없으면 S3 저장 경로를 지정할 수 없기 때문이다. 먼저 임시 저장 후 이미지를 업로드하고, 게시물 취소 시 임시 데이터를 삭제하는 로직이 함께 구현되어야 한다

트러블슈팅 체크리스트

에러가 발생했을 때 확인 순서다

403 Forbidden (이미지 로딩 실패)

  • 버킷 퍼블릭 액세스 차단 설정 확인
  • 버킷 정책의 Resource ARN 경로(/*) 확인
  • 허용된 Action(s3:GetObject) 확인

CORS 에러

  • S3 CORS 설정의 AllowedOrigins 확인
  • 요청 Origin과 설정값 일치 여부 확인
  • API 서버의 Spring Security CORS 설정 확인

업로드 실패

  • IAM 사용자의 s3:PutObject 권한 확인
  • 액세스 키 설정이 application-local.yml에 제대로 주입되었는지 확인
  • IntelliJ 실행 프로파일이 local로 지정되어 있는지 확인

정책 편집기 오류 (비ASCII 문자)

  • AI가 생성한 JSON을 메모장에 붙여넣었다가 다시 복사한 뒤 입력한다. 보이지 않는 특수문자가 제거된다

AWS 공동 책임 모델

AWS와 사용자는 보안에 대해 각자의 책임 영역을 가진다

책임 주체범위
AWS클라우드 인프라 자체의 보안
사용자IAM 설정, 네트워크 접근 제어, 애플리케이션 보안

IAM 정책을 느슨하게 열어두거나 보안 그룹 설정을 잘못해서 발생한 침해는 사용자 책임이다. 최소 권한 원칙(Least Privilege)을 기본으로 삼는다

한 줄로 요약하면, S3 버킷 연동은 버킷 설정 하나가 아니라 IAM·CORS·프로토콜 일관성을 함께 챙겨야 완성된다

출처 – 인프런 [유행 말고 내공. 30년차 개발자의 실전 바이브 코딩]