Chapter 05 - 강사용 답안

S3 스토리지 + DynamoDB

"리뷰 사진은 S3에, 리뷰 데이터는 DynamoDB에 저장하자"

강사 전용 문서

이 문서는 강사용 답안입니다. 학생에게 공유하지 마세요. 각 단계의 정확한 답과 해설, 트러블슈팅 가이드가 포함되어 있습니다.

Step 1: S3 버킷 생성

답안

웹 콘솔 방법

  1. AWS 콘솔에서 S3 서비스로 이동
  2. 버킷 만들기 클릭
  3. 버킷 이름: shopeasy-images-{ACCOUNT_ID} 입력
    • 예: shopeasy-images-123456789012
    • 버킷 이름은 전 세계에서 고유해야 하므로 계정 ID를 붙여 중복 방지
  4. AWS 리전: 아시아 태평양(서울) ap-northeast-2 선택
  5. 객체 소유권: ACL 비활성화됨 (권장) 유지
  6. "모든 퍼블릭 액세스 차단" 체크박스를 해제 (체크 풀기)
    • 4개 항목 모두 체크 해제
    • "현재 설정으로 인해 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있음을 알고 있습니다" 체크
  7. 나머지 설정은 기본값 유지
  8. 버킷 만들기 클릭

AWS CLI 방법

bash
# 계정 ID 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "Account ID: $ACCOUNT_ID"

# S3 버킷 생성 (서울 리전)
aws s3api create-bucket \
  --bucket shopeasy-images-${ACCOUNT_ID} \
  --region ap-northeast-2 \
  --create-bucket-configuration LocationConstraint=ap-northeast-2

# 퍼블릭 액세스 차단 해제
aws s3api put-public-access-block \
  --bucket shopeasy-images-${ACCOUNT_ID} \
  --public-access-block-configuration \
  "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"

# 버킷 생성 확인
aws s3 ls | grep shopeasy
해설: 왜 퍼블릭 액세스 차단을 해제하나?

S3는 보안을 위해 기본적으로 모든 퍼블릭 액세스를 차단합니다. 이것은 "이중 잠금" 개념입니다:

  • 1차 잠금: 퍼블릭 액세스 차단 설정 (버킷 레벨)
  • 2차 잠금: 버킷 정책 (어떤 객체를 공개할지 세부 제어)

1차 잠금이 켜져 있으면 2차에서 아무리 공개해도 차단됩니다. 리뷰 이미지를 브라우저에서 표시하려면 1차 잠금을 풀고, 2차(버킷 정책)에서 uploads/* 경로만 선별적으로 공개합니다.

실무에서는 CloudFront를 앞에 두고 S3는 비공개로 유지하는 것이 더 안전하지만, 이번 실습에서는 간단히 직접 공개 방식을 사용합니다.

Step 2: S3 버킷 CORS 설정

답안

웹 콘솔 방법

  1. S3 → shopeasy-images-{ACCOUNT_ID} 버킷 클릭
  2. 권한 탭 클릭
  3. CORS(Cross-origin resource sharing) 섹션으로 스크롤
  4. 편집 클릭
  5. 아래 JSON을 붙여넣기:
json
[
  {
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST"],
    "AllowedHeaders": ["*"],
    "MaxAgeSeconds": 3000
  }
]
  1. 변경 사항 저장 클릭

AWS CLI 방법

bash
# CORS 설정 JSON 파일 생성
cat > /tmp/cors-config.json << 'EOF'
{
  "CORSRules": [
    {
      "AllowedOrigins": ["*"],
      "AllowedMethods": ["GET", "PUT", "POST"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 3000
    }
  ]
}
EOF

# CORS 설정 적용
aws s3api put-bucket-cors \
  --bucket shopeasy-images-${ACCOUNT_ID} \
  --cors-configuration file:///tmp/cors-config.json

# CORS 설정 확인
aws s3api get-bucket-cors \
  --bucket shopeasy-images-${ACCOUNT_ID}
해설: CORS 설정 항목 설명
설정 의미
AllowedOrigins ["*"] 모든 도메인에서의 요청 허용. 실무에서는 프론트엔드 도메인만 지정
AllowedMethods ["GET","PUT","POST"] GET(이미지 조회), PUT(업로드), POST(멀티파트 업로드) 허용
AllowedHeaders ["*"] 모든 HTTP 헤더 허용 (Content-Type, Authorization 등)
MaxAgeSeconds 3000 CORS Preflight 응답을 3000초(50분) 캐시. 반복 요청 시 성능 향상

실무 팁: 프로덕션에서는 AllowedOrigins["http://your-frontend-domain.com"]처럼 구체적인 도메인으로 제한해야 합니다. "*"는 개발/실습 용도입니다.

Step 3: S3 버킷 정책 설정

답안

웹 콘솔 방법

  1. S3 → shopeasy-images-{ACCOUNT_ID} 버킷 → 권한
  2. 버킷 정책 섹션 → 편집
  3. 아래 JSON을 붙여넣기 ({ACCOUNT_ID}를 본인의 계정 ID로 교체):
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadUploads",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::shopeasy-images-{ACCOUNT_ID}/uploads/*"
    }
  ]
}
  1. 변경 사항 저장 클릭

AWS CLI 방법

bash
# 계정 ID 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# 버킷 정책 JSON 파일 생성
cat > /tmp/bucket-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadUploads",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::shopeasy-images-${ACCOUNT_ID}/uploads/*"
    }
  ]
}
EOF

# 버킷 정책 적용
aws s3api put-bucket-policy \
  --bucket shopeasy-images-${ACCOUNT_ID} \
  --policy file:///tmp/bucket-policy.json

# 버킷 정책 확인
aws s3api get-bucket-policy \
  --bucket shopeasy-images-${ACCOUNT_ID} \
  --output text | python3 -m json.tool
해설: 버킷 정책 분석
항목 의미
Effect Allow 허용 정책
Principal "*" 누구나 (모든 사용자, 비로그인 포함)
Action s3:GetObject 객체 읽기(다운로드)만 허용. 업로드/삭제는 불가
Resource .../uploads/* uploads/ 폴더 안의 모든 객체만 대상. 다른 경로는 비공개

보안 핵심: Resourceuploads/*로 제한했기 때문에 버킷 루트에 있는 다른 파일(설정 파일, 로그 등)은 퍼블릭으로 노출되지 않습니다. 만약 "Resource": "arn:aws:s3:::shopeasy-images-xxxx/*"로 설정하면 버킷의 모든 파일이 공개되어 보안 사고가 발생할 수 있습니다.

Step 4: DynamoDB 테이블 생성

답안

웹 콘솔 방법

  1. AWS 콘솔에서 DynamoDB 서비스로 이동
  2. 테이블 만들기 클릭
  3. 테이블 세부 정보:
    • 테이블 이름: Reviews
    • 파티션 키: productId (문자열)
  4. 정렬 키 추가 체크:
    • 정렬 키: createdAt#userId (문자열)
  5. 테이블 설정: 설정 사용자 지정 선택
  6. 읽기/쓰기 용량 설정:
    • 용량 모드: 온디맨드 선택
  7. 나머지 설정(암호화, 태그 등)은 기본값 유지
  8. 테이블 생성 클릭
  9. 상태가 활성(Active)이 될 때까지 대기 (보통 10~30초)

AWS CLI 방법

bash
# DynamoDB 테이블 생성
aws dynamodb create-table \
  --table-name Reviews \
  --attribute-definitions \
    AttributeName=productId,AttributeType=S \
    AttributeName="createdAt#userId",AttributeType=S \
  --key-schema \
    AttributeName=productId,KeyType=HASH \
    AttributeName="createdAt#userId",KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --region ap-northeast-2

# 테이블 생성 완료 대기
aws dynamodb wait table-exists \
  --table-name Reviews \
  --region ap-northeast-2

# 테이블 상태 확인
aws dynamodb describe-table \
  --table-name Reviews \
  --region ap-northeast-2 \
  --query "Table.{Name:TableName, Status:TableStatus, KeySchema:KeySchema, BillingMode:BillingModeSummary.BillingMode}"
해설: DynamoDB CLI 파라미터 설명
파라미터 설명
--attribute-definitions productId (S), createdAt#userId (S) 키에 사용할 속성 정의. S = String, N = Number
--key-schema HASH + RANGE HASH = 파티션 키, RANGE = 정렬 키
--billing-mode PAY_PER_REQUEST 온디맨드 모드 (사용량 기반 과금). 프리 티어에 포함

주의: --attribute-definitions에는 키로 사용하는 속성만 정의합니다. 리뷰 내용(content), 별점(rating) 등 나머지 속성은 여기서 정의하지 않습니다. DynamoDB는 스키마리스이므로 데이터를 넣을 때 자유롭게 속성을 추가할 수 있습니다.

키 설계의 이유

파티션 키: productId

  • "PROD-001 상품의 리뷰를 모두 보여줘" 같은 쿼리가 가장 빈번
  • 같은 상품의 리뷰가 하나의 파티션에 모여있으므로 효율적으로 조회 가능

정렬 키: createdAt#userId

  • createdAt을 앞에 두어 최신 리뷰부터 정렬
  • #userId를 뒤에 붙여 동일 시각에 작성된 리뷰를 구분 (유일성 보장)
  • 예: 2024-12-01T10:30:00Z#USER-042

Step 5: API 서버 .env 수정

답안

nano 편집기로 수정

bash
# EC2 접속
ssh -i your-key.pem ec2-user@{EC2_PUBLIC_IP}

# API 서버 디렉토리로 이동
cd ~/shopeasy/api-server

# .env 파일 편집
nano .env

파일 끝에 아래 내용을 추가합니다 ({ACCOUNT_ID}를 본인 계정 ID로 교체):

env
# S3 설정
STORAGE_TYPE=s3
S3_BUCKET=shopeasy-images-{ACCOUNT_ID}
S3_REGION=ap-northeast-2

# DynamoDB 설정
REVIEW_STORE=dynamodb
DYNAMODB_TABLE=Reviews
DYNAMODB_REGION=ap-northeast-2

저장: Ctrl+OEnter, 종료: Ctrl+X

CLI로 한 번에 추가

bash
# EC2 접속
ssh -i your-key.pem ec2-user@{EC2_PUBLIC_IP}

# 계정 ID 확인
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# .env 파일에 환경변수 추가
cat >> ~/shopeasy/api-server/.env << EOF

# S3 설정
STORAGE_TYPE=s3
S3_BUCKET=shopeasy-images-${ACCOUNT_ID}
S3_REGION=ap-northeast-2

# DynamoDB 설정
REVIEW_STORE=dynamodb
DYNAMODB_TABLE=Reviews
DYNAMODB_REGION=ap-northeast-2
EOF

# 추가된 내용 확인
cat ~/shopeasy/api-server/.env
해설: 각 환경변수의 역할
환경변수 설명
STORAGE_TYPE s3 이미지 저장소를 S3로 설정 (기본: local 파일시스템)
S3_BUCKET 버킷 이름 이미지를 업로드할 S3 버킷
S3_REGION ap-northeast-2 S3 버킷이 위치한 리전
REVIEW_STORE dynamodb 리뷰 저장소를 DynamoDB로 설정 (기본: SQLite)
DYNAMODB_TABLE Reviews 리뷰 데이터를 저장할 DynamoDB 테이블
DYNAMODB_REGION ap-northeast-2 DynamoDB 테이블이 위치한 리전

핵심: AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY를 넣지 않습니다. EC2에 연결된 IAM Role이 AWS SDK에 자동으로 임시 자격 증명을 제공합니다. Access Key를 .env에 하드코딩하면 보안 사고의 원인이 됩니다.

Step 6: API 서버 재시작

답안

pm2로 재시작

bash
# API 서버 재시작
pm2 restart all

# 상태 확인
pm2 status

# 로그 확인 (에러 없는지)
pm2 logs --lines 20

직접 실행

bash
# 기존 Node.js 프로세스 종료
pkill -f "node server.js" 2>/dev/null

# 잠시 대기
sleep 2

# API 서버 백그라운드 실행
cd ~/shopeasy/api-server
nohup node server.js > ~/shopeasy-api.log 2>&1 &

# 프로세스 확인
ps aux | grep "node server"

# 로그 확인
tail -20 ~/shopeasy-api.log
해설: 재시작이 필요한 이유

Node.js 앱은 .env 파일을 서버 시작 시점에 한 번만 읽습니다 (dotenv 라이브러리 사용). 따라서 환경변수를 변경한 후에는 반드시 서버를 재시작해야 새로운 설정이 적용됩니다.

pm2의 restart는 프로세스를 종료하고 다시 시작하므로, .env가 다시 로드됩니다.

Step 7: 리뷰 작성 테스트

답안

기본 리뷰 작성 테스트

bash
# 테스트용 이미지 생성 (1x1 픽셀 PNG)
echo -e '\x89PNG\r\n\x1a\n' > /tmp/test-image.png

# 리뷰 작성 API 호출
curl -s -X POST http://{EC2_PUBLIC_IP}:5000/api/reviews \
  -F "productId=PROD-001" \
  -F "userId=USER-042" \
  -F "userName=testuser" \
  -F "rating=5" \
  -F "content=배송이 빠르고 품질이 좋습니다!" \
  -F "image=@/tmp/test-image.png" | python3 -m json.tool

정상 응답 예시:

json
{
  "success": true,
  "review": {
    "productId": "PROD-001",
    "createdAt#userId": "2024-12-01T10:30:00Z#USER-042",
    "rating": 5,
    "content": "배송이 빠르고 품질이 좋습니다!",
    "imageUrl": "https://shopeasy-images-123456789012.s3.ap-northeast-2.amazonaws.com/uploads/review-xxxx.png",
    "userName": "testuser"
  }
}

상세 테스트 (여러 리뷰 작성 + 조회)

bash
# 테스트 이미지 생성
echo -e '\x89PNG\r\n\x1a\n' > /tmp/test-image.png

# 리뷰 1: 이미지 포함
curl -s -X POST http://{EC2_PUBLIC_IP}:5000/api/reviews \
  -F "productId=PROD-001" \
  -F "userId=USER-042" \
  -F "userName=김철수" \
  -F "rating=5" \
  -F "content=배송이 빠르고 품질이 좋습니다!" \
  -F "image=@/tmp/test-image.png"

echo ""

# 리뷰 2: 이미지 없이 텍스트만
curl -s -X POST http://{EC2_PUBLIC_IP}:5000/api/reviews \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "PROD-001",
    "userId": "USER-099",
    "userName": "이영희",
    "rating": 4,
    "content": "가격 대비 괜찮습니다. 다음에도 구매할 의향이 있습니다."
  }'

echo ""

# 리뷰 3: 다른 상품
curl -s -X POST http://{EC2_PUBLIC_IP}:5000/api/reviews \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "PROD-002",
    "userId": "USER-042",
    "userName": "김철수",
    "rating": 3,
    "content": "보통입니다."
  }'

echo ""
echo "=== PROD-001 리뷰 조회 ==="

# 특정 상품의 리뷰 조회
curl -s http://{EC2_PUBLIC_IP}:5000/api/reviews/PROD-001 | python3 -m json.tool
해설: curl 옵션 설명
  • -F: Form 데이터 (multipart/form-data). 파일 업로드 시 사용
  • -F "image=@파일경로": @는 파일을 첨부한다는 의미
  • -H "Content-Type: application/json": JSON 형식으로 전송
  • -d: 요청 본문 (Request Body)
  • -s: Silent 모드 (진행률 표시 숨김)
  • | python3 -m json.tool: JSON 응답을 보기 좋게 포맷팅

Step 8: S3에 이미지 저장 확인

답안

웹 콘솔에서 확인

  1. AWS 콘솔 → S3
  2. shopeasy-images-{ACCOUNT_ID} 버킷 클릭
  3. uploads/ 폴더 클릭
  4. 업로드된 이미지 파일 확인
  5. 이미지 파일 클릭 → 객체 URL 복사
  6. 브라우저 새 탭에서 URL 열기 - 이미지가 표시되면 성공

AWS CLI로 확인

bash
# S3 버킷의 uploads/ 폴더 내용 확인
aws s3 ls s3://shopeasy-images-${ACCOUNT_ID}/uploads/

# 또는 재귀적으로 전체 확인
aws s3 ls s3://shopeasy-images-${ACCOUNT_ID}/ --recursive

# 이미지 URL 확인 (브라우저에서 직접 접근 테스트)
echo "이미지 URL 테스트:"
echo "https://shopeasy-images-${ACCOUNT_ID}.s3.ap-northeast-2.amazonaws.com/uploads/"

# curl로 이미지 접근 테스트 (HTTP 200이면 성공)
FIRST_IMAGE=$(aws s3 ls s3://shopeasy-images-${ACCOUNT_ID}/uploads/ --recursive | head -1 | awk '{print $4}')
if [ -n "$FIRST_IMAGE" ]; then
  curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" \
    "https://shopeasy-images-${ACCOUNT_ID}.s3.ap-northeast-2.amazonaws.com/${FIRST_IMAGE}"
fi
해설: 확인 포인트
  • aws s3 ls 결과에 파일이 보이면 S3 업로드 성공
  • 브라우저에서 이미지 URL이 열리면 버킷 정책(퍼블릭 읽기) 설정 성공
  • 403 Forbidden 에러가 나오면 버킷 정책 또는 퍼블릭 액세스 차단 설정 확인 필요

Step 9: DynamoDB에 리뷰 데이터 확인

답안

웹 콘솔에서 확인

  1. AWS 콘솔 → DynamoDB
  2. 좌측 메뉴 → 테이블Reviews 클릭
  3. 항목 탐색 탭 클릭
  4. 항목 검색 또는 스캔 실행스캔 선택 → 실행
  5. 저장된 리뷰 데이터가 표시되면 성공
  6. 특정 상품의 리뷰만 보려면:
    • 쿼리 선택
    • 파티션 키: productId = PROD-001
    • 실행 클릭

AWS CLI로 확인

bash
# 전체 스캔 (모든 리뷰 조회)
aws dynamodb scan \
  --table-name Reviews \
  --region ap-northeast-2

# 결과를 보기 좋게 포맷팅
aws dynamodb scan \
  --table-name Reviews \
  --region ap-northeast-2 \
  --output table

# 특정 상품의 리뷰만 조회 (Query - 더 효율적)
aws dynamodb query \
  --table-name Reviews \
  --key-condition-expression "productId = :pid" \
  --expression-attribute-values '{":pid": {"S": "PROD-001"}}' \
  --region ap-northeast-2

# 테이블 통계 확인 (항목 수)
aws dynamodb describe-table \
  --table-name Reviews \
  --region ap-northeast-2 \
  --query "Table.{ItemCount:ItemCount, TableSizeBytes:TableSizeBytes, Status:TableStatus}"
해설: Scan vs Query
방법 동작 비용 용도
Scan 테이블 전체를 읽음 높음 (전체 데이터 읽기) 개발/디버깅, 소량 데이터
Query 파티션 키로 특정 데이터만 읽음 낮음 (필요한 데이터만) 프로덕션 (권장)

프로덕션에서는 항상 Query를 사용해야 합니다. Scan은 데이터가 많아지면 매우 느리고 비용이 많이 듭니다. 이번 실습에서는 데이터가 적으므로 확인 용도로 Scan을 사용했습니다.

트러블슈팅 가이드

자주 발생하는 문제와 해결 방법

CORS 에러: "Access to XMLHttpRequest has been blocked by CORS policy"

원인: S3 버킷의 CORS 설정이 없거나 잘못 설정됨

해결 방법:

  1. S3 버킷 → 권한 → CORS 섹션에서 설정이 올바른지 확인
  2. JSON 형식 오류가 없는지 확인 (배열 []로 감싸야 함)
  3. AllowedOrigins에 프론트엔드 URL이 포함되어 있는지 확인
bash
# CORS 설정 확인
aws s3api get-bucket-cors --bucket shopeasy-images-${ACCOUNT_ID}

# 설정이 없으면 다시 적용
aws s3api put-bucket-cors \
  --bucket shopeasy-images-${ACCOUNT_ID} \
  --cors-configuration '{"CORSRules":[{"AllowedOrigins":["*"],"AllowedMethods":["GET","PUT","POST"],"AllowedHeaders":["*"],"MaxAgeSeconds":3000}]}'
S3 업로드 실패: "Access Denied" 또는 "403 Forbidden"

원인: IAM Role에 S3 권한이 없거나, 버킷 이름이 잘못됨

해결 방법:

  1. EC2에 연결된 IAM Role에 AmazonS3FullAccess 또는 커스텀 S3 정책이 있는지 확인
  2. .env의 S3_BUCKET 값이 실제 버킷 이름과 정확히 일치하는지 확인
  3. 버킷의 리전이 ap-northeast-2인지 확인
bash
# EC2에서 IAM Role 확인
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/

# S3 권한 테스트 (EC2에서 실행)
aws s3 cp /tmp/test-image.png s3://shopeasy-images-${ACCOUNT_ID}/test-upload.png
aws s3 rm s3://shopeasy-images-${ACCOUNT_ID}/test-upload.png

# 버킷 존재 확인
aws s3api head-bucket --bucket shopeasy-images-${ACCOUNT_ID}
S3 이미지 브라우저 접근 시 403 Forbidden

원인: 퍼블릭 액세스 차단이 해제되지 않았거나, 버킷 정책이 잘못됨

해결 방법:

  1. 버킷의 "퍼블릭 액세스 차단" 설정 4개 항목이 모두 꺼짐인지 확인
  2. 버킷 정책의 Resource ARN이 올바른지 확인 (버킷 이름 오타 체크)
  3. 업로드된 이미지가 uploads/ 경로 아래에 있는지 확인
bash
# 퍼블릭 액세스 차단 설정 확인
aws s3api get-public-access-block --bucket shopeasy-images-${ACCOUNT_ID}

# 모두 false여야 함:
# {
#   "PublicAccessBlockConfiguration": {
#     "BlockPublicAcls": false,
#     "IgnorePublicAcls": false,
#     "BlockPublicPolicy": false,
#     "RestrictPublicBuckets": false
#   }
# }

# 버킷 정책 확인
aws s3api get-bucket-policy --bucket shopeasy-images-${ACCOUNT_ID} --output text | python3 -m json.tool
DynamoDB 에러: "ResourceNotFoundException" 또는 "Cannot do operations on a non-existent table"

원인: DynamoDB 테이블이 존재하지 않거나, 테이블 이름/리전이 잘못됨

해결 방법:

  1. .env의 DYNAMODB_TABLE 값이 Reviews인지 확인 (대소문자 구분)
  2. .env의 DYNAMODB_REGIONap-northeast-2인지 확인
  3. DynamoDB 콘솔에서 테이블 상태가 Active인지 확인
bash
# 테이블 존재 확인
aws dynamodb describe-table --table-name Reviews --region ap-northeast-2

# 테이블 목록 확인
aws dynamodb list-tables --region ap-northeast-2

# .env 확인
cat ~/shopeasy/api-server/.env | grep -i dynamo
DynamoDB 키 스키마 에러: "ValidationException: One of the required keys was not given a value"

원인: 데이터 저장 시 파티션 키(productId)나 정렬 키(createdAt#userId)가 누락됨

해결 방법:

  1. API 서버 코드에서 리뷰 저장 시 productIdcreatedAt#userId를 모두 포함하는지 확인
  2. 정렬 키 이름이 createdAt#userId인지 확인 (# 포함, 대소문자 정확히)
  3. 값이 빈 문자열이 아닌지 확인 (DynamoDB는 빈 문자열 키를 허용하지 않음)
bash
# 테이블 키 스키마 확인
aws dynamodb describe-table \
  --table-name Reviews \
  --region ap-northeast-2 \
  --query "Table.KeySchema"

# 예상 결과:
# [
#   { "AttributeName": "productId", "KeyType": "HASH" },
#   { "AttributeName": "createdAt#userId", "KeyType": "RANGE" }
# ]
IAM Role 권한 부족: "is not authorized to perform: s3:PutObject" 또는 "dynamodb:PutItem"

원인: EC2에 연결된 IAM Role에 S3 또는 DynamoDB 접근 권한이 없음

해결 방법:

  1. IAM 콘솔 → 역할 → ShopEasy-EC2-Role 클릭
  2. 권한 정책에 다음이 포함되어 있는지 확인:
    • AmazonS3FullAccess (또는 커스텀 S3 정책)
    • AmazonDynamoDBFullAccess (또는 커스텀 DynamoDB 정책)
  3. 없으면 권한 추가정책 연결에서 추가
bash
# IAM Role에 S3 정책 추가
aws iam attach-role-policy \
  --role-name ShopEasy-EC2-Role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

# IAM Role에 DynamoDB 정책 추가
aws iam attach-role-policy \
  --role-name ShopEasy-EC2-Role \
  --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess

# 연결된 정책 확인
aws iam list-attached-role-policies --role-name ShopEasy-EC2-Role
참고

FullAccess 정책은 실습용입니다. 프로덕션에서는 필요한 최소 권한만 부여하는 커스텀 정책을 사용해야 합니다 (예: 특정 버킷에 대한 PutObject, GetObject만 허용).

API 서버 시작 실패: "Cannot find module 'aws-sdk'" 또는 "@aws-sdk/client-s3"

원인: AWS SDK 패키지가 설치되지 않음

해결 방법:

bash
# API 서버 디렉토리로 이동
cd ~/shopeasy/api-server

# AWS SDK v3 패키지 설치 (Node.js 프로젝트)
npm install @aws-sdk/client-s3 @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

# 또는 AWS SDK v2 사용 시
npm install aws-sdk

# 서버 재시작
pm2 restart all
DynamoDB 테이블을 잘못 만들어서 다시 만들고 싶다

원인: 키 스키마나 테이블 이름을 잘못 설정

해결 방법: DynamoDB 테이블의 키 스키마는 생성 후 변경이 불가합니다. 삭제 후 재생성해야 합니다.

bash
# 기존 테이블 삭제
aws dynamodb delete-table \
  --table-name Reviews \
  --region ap-northeast-2

# 삭제 완료 대기
aws dynamodb wait table-not-exists \
  --table-name Reviews \
  --region ap-northeast-2

# 올바른 스키마로 재생성
aws dynamodb create-table \
  --table-name Reviews \
  --attribute-definitions \
    AttributeName=productId,AttributeType=S \
    AttributeName="createdAt#userId",AttributeType=S \
  --key-schema \
    AttributeName=productId,KeyType=HASH \
    AttributeName="createdAt#userId",KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --region ap-northeast-2

# 생성 완료 대기
aws dynamodb wait table-exists \
  --table-name Reviews \
  --region ap-northeast-2

echo "테이블 재생성 완료!"
강사 참고: 학생들이 자주 실수하는 포인트
  1. S3 버킷 이름에 대문자 사용 - S3 버킷 이름은 소문자, 숫자, 하이픈만 허용됩니다
  2. 퍼블릭 액세스 차단 해제 후 확인 체크 안 함 - "알고 있습니다" 체크를 빼먹으면 저장이 안 됩니다
  3. 버킷 정책에서 ACCOUNT_ID 교체를 잊음 - {ACCOUNT_ID}를 실제 계정 ID로 바꿔야 합니다
  4. DynamoDB 정렬 키에 # 빼먹음 - createdAt#userId에서 #은 속성 이름의 일부입니다
  5. .env 수정 후 서버 재시작을 잊음 - Node.js는 환경변수를 시작 시 한 번만 읽습니다
  6. 리전 불일치 - S3 버킷, DynamoDB 테이블, .env 설정이 모두 ap-northeast-2여야 합니다