왜 이미지 자동 처리 파이프라인을 만들까?
여러분이 커머스 플랫폼의 개발자라고 상상해 보세요. 판매자가 상품 이미지를 올리면 다음 작업이 필요합니다:

- 원본 보관: 고해상도 원본을 안전하게 저장
- 썸네일 생성: 상품 목록에 표시할 300x300px 이미지
- 워터마크 삽입: 무단 도용 방지를 위한 반투명 텍스트 오버레이
- 포맷 변환: JPEG를 WebP로 변환하여 용량 30-50% 절감 (사용자 로딩 속도 향상)
처음에는 "이미지 올릴 때 서버에서 처리하면 되지 않나?"라고 생각할 수 있습니다. 하지만:
- 이미지 처리는 CPU 집약적 작업 — 웹 서버 응답 시간이 크게 늘어남
- 갑자기 이미지 100장이 동시에 올라오면 서버 과부하 발생
- 처리 도중 서버가 죽으면 어디까지 처리했는지 알 수 없음
이 문제를 해결하는 패턴이 이벤트 기반 파이프라인입니다:
- S3에 이미지가 업로드되면 자동으로 파이프라인이 시작됩니다
- 각 단계(리사이징, 워터마크, 변환)를 독립된 Lambda가 처리합니다
- Step Functions가 순서 제어와 에러 핸들링을 담당합니다
- 처리 완료 시 SNS로 알림을 보냅니다
서버를 관리할 필요 없이, 이미지 1장이든 1만 장이든 자동으로 스케일링됩니다. 이것이 서버리스 파이프라인의 힘입니다.
실습을 시작하기 전에 AWS 콘솔에 로그인되어 있는지 확인하세요. 리전은 ap-northeast-2 (서울) 을 사용합니다.
아키텍처 개요

파이프라인 처리 흐름
비용 예측
비용 계산기
GB/월
100만 요청당 $0.20 + 실행시간
상태 전환 1,000당 $0.025
요청 100만당 $0.50
* 실제 비용은 AWS 요금 정책에 따라 달라질 수 있습니다.
Step 1: S3 버킷 생성 (Input / Output)
원본 이미지를 받을 Input 버킷과, 처리된 이미지를 저장할 Output 버킷 두 개를 만듭니다.
- AWS 콘솔 → S3 검색 → 버킷 만들기 클릭
- 버킷 이름:
image-pipeline-input-{계정ID}입력 (S3 버킷 이름은 전세계에서 고유해야 합니다) - 리전: 아시아 태평양(서울) 확인
- 나머지 설정은 기본값 → 버킷 만들기 클릭
- 동일한 방식으로
image-pipeline-output-{계정ID}버킷 생성 - Input 버킷 클릭 → 폴더 만들기 → 이름:
uploads→ 생성 (이 폴더에 업로드된 파일만 파이프라인이 처리) - Output 버킷에도
thumbnails,watermarked,webp폴더를 각각 생성
aws s3 mb s3://image-pipeline-input-$(aws sts get-caller-identity --query Account --output text)
aws s3 mb s3://image-pipeline-output-$(aws sts get-caller-identity --query Account --output text)버킷 이름에 계정 ID를 포함하면 전세계에서 고유한 이름을 쉽게 만들 수 있습니다. 계정 ID는 AWS 콘솔 우측 상단 사용자명 클릭 → "계정 ID" 에서 확인할 수 있습니다.
S3 버킷 이름 규칙: 소문자, 숫자, 하이픈(-)만 사용 가능합니다. 밑줄(_), 대문자, 공백은 허용되지 않습니다. 또한 3-63자 사이여야 하며, 한 번 삭제한 버킷 이름은 바로 재사용할 수 없을 수 있습니다.
Step 2: Lambda 함수 작성 (리사이징 / 워터마크 / 변환)
파이프라인의 각 단계를 담당할 Lambda 함수 3개를 만듭니다. Python의 Pillow 라이브러리를 사용합니다.
- 1먼저 Pillow Lambda Layer를 준비합니다: mkdir -p python && cd python pip install Pillow -t . cd .. && zip -r pillow-layer.zip python/
- 2Lambda 콘솔 → 왼쪽 메뉴 계층 → 계층 생성 → 이름: pillow-layer, ZIP 업로드, 런타임: Python 3.12
- 3Lambda 콘솔 → 함수 생성 → image-resize (런타임: Python 3.12)
- 4구성 탭 → 일반 구성 → 메모리: 512MB, 타임아웃: 30초로 변경 (이미지 처리에 충분한 자원)
- 5계층 섹션 → pillow-layer 추가
- 6image-resize 함수 코드 작성: S3에서 원본 이미지를 다운로드 → Pillow로 300x300 썸네일 생성 → Output 버킷의 thumbnails/ 폴더에 저장
- 7image-watermark 함수 생성: 리사이징된 이미지에 Pillow의 ImageDraw로 반투명 텍스트 워터마크 삽입
- 8image-convert 함수 생성: 최종 이미지를 Pillow의 save(format='WEBP', quality=80)으로 WebP 변환 후 webp/ 폴더에 저장
- 9세 함수 모두 실행 역할에 AmazonS3FullAccess 정책 연결
- 10환경 변수 추가: INPUT_BUCKET, OUTPUT_BUCKET 에 각각 버킷 이름 설정
# image-resize Lambda 핵심 코드 (Python 3.12)
import boto3, io, os
from PIL import Image
s3 = boto3.client('s3')
OUTPUT_BUCKET = os.environ['OUTPUT_BUCKET']
def lambda_handler(event, context):
bucket = event['bucket']
key = event['key']
# S3에서 원본 이미지 다운로드
response = s3.get_object(Bucket=bucket, Key=key)
img = Image.open(io.BytesIO(response['Body'].read()))
# 300x300 썸네일 생성 (비율 유지)
img.thumbnail((300, 300), Image.LANCZOS)
# Output 버킷에 저장
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=85)
buffer.seek(0)
output_key = f"thumbnails/{key.split('/')[-1]}"
s3.put_object(Bucket=OUTPUT_BUCKET, Key=output_key, Body=buffer, ContentType='image/jpeg')
return {**event, 'resizedKey': output_key, 'resizedBucket': OUTPUT_BUCKET}Step 3: Step Functions 상태 머신 정의
세 Lambda 함수를 순서대로 실행하고, 각 단계에서 에러가 발생하면 에러 핸들러로 분기하는 상태 머신을 만듭니다.
- Step Functions 콘솔 → 상태 머신 생성 클릭
- 코드로 작성 선택 → ASL(Amazon States Language) 정의 입력
- 이름:
image-processing-pipeline - 각 단계(Resize → Watermark → Convert)를 Task 상태로 연결
- 에러 처리: 각 Task에 Catch 블록 추가하여 실패 시 에러 핸들러로 이동
- 마지막에 SNS 알림 전송 단계 추가
- 실행 역할: 새 역할 생성 선택 (Lambda 호출 + SNS 발행 권한 자동 부여)
{
"StartAt": "ResizeImage",
"States": {
"ResizeImage": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:ACCOUNT:function:image-resize",
"Next": "WatermarkImage",
"Retry": [{"ErrorEquals": ["States.TaskFailed"], "MaxAttempts": 2, "BackoffRate": 2}],
"Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "HandleError" }]
},
"WatermarkImage": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:ACCOUNT:function:image-watermark",
"Next": "ConvertFormat",
"Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "HandleError" }]
},
"ConvertFormat": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:ACCOUNT:function:image-convert",
"Next": "NotifyComplete"
},
"NotifyComplete": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:ap-northeast-2:ACCOUNT:image-pipeline-notify",
"Message": "이미지 처리가 완료되었습니다."
},
"End": true
},
"HandleError": {
"Type": "Pass",
"Result": "처리 중 오류 발생",
"End": true
}
}
}ASL의 Retry 블록에서 BackoffRate: 2는 재시도 간격을 점점 늘리는 설정입니다.
첫 번째 재시도는 1초 후, 두 번째는 2초 후에 실행됩니다.
이를 지수 백오프(Exponential Backoff) 라고 하며, 일시적 장애에서의 복구율을 높입니다.
Step 4: S3 이벤트 트리거 연결
이미지가 S3에 업로드되면 자동으로 Step Functions가 시작되도록 이벤트를 연결합니다.
- 1Input 버킷 클릭 → 속성 탭 → 페이지 하단 이벤트 알림 섹션 → 이벤트 알림 생성 클릭
- 2이벤트 알림 이름: trigger-pipeline
- 3접두사: uploads/ (이 폴더에 업로드된 파일만 트리거)
- 4접미사: .jpg (JPG 파일만 처리 — PNG도 원하면 별도 규칙 추가)
- 5이벤트 유형: s3:ObjectCreated:Put 체크
- 6대상: Step Functions 상태 머신 선택 → image-processing-pipeline 선택
- 7변경 사항 저장 클릭
- 8테스트: 아무 JPG 이미지를 uploads/ 폴더에 업로드하고, Step Functions 콘솔에서 실행이 시작되는지 확인
Step 5: SNS 완료 알림 설정
파이프라인 처리가 완료되면 관리자에게 이메일 알림을 보내도록 설정합니다.
- 1SNS 콘솔 → 토픽 생성 → 유형: 표준 → 이름: image-pipeline-notify → 토픽 생성
- 2구독 생성 → 프로토콜: 이메일 → 본인 이메일 입력 → 구독 생성
- 3이메일에서 확인 링크 클릭 → 구독 상태가 확인됨으로 변경 확인
- 4Step Functions 상태 머신의 ASL에서 NotifyComplete 단계의 TopicArn을 실제 ARN으로 교체
- 5Step Functions 실행 역할에 AmazonSNSFullAccess 정책 연결 (SNS 발행 권한)
SNS 토픽 ARN은 SNS 콘솔에서 토픽을 클릭하면 상단에 표시됩니다.
형식: arn:aws:sns:ap-northeast-2:{계정ID}:image-pipeline-notify
Step 6: 완성 후 통합 테스트
파이프라인 전체를 테스트합니다.
- 1테스트용 JPG 이미지를 준비합니다 (1MB 이하 권장)
- 2S3 콘솔 → Input 버킷 → uploads/ 폴더 → 업로드 클릭 → 이미지 선택 → 업로드
- 3Step Functions 콘솔 → image-processing-pipeline → 최근 실행 클릭
- 4실행 그래프에서 각 단계가 초록색(성공)으로 표시되는지 확인
- 5각 단계를 클릭하면 입력/출력 JSON을 볼 수 있습니다 — 데이터가 단계마다 어떻게 전달되는지 확인
- 6Output 버킷 확인: thumbnails/ 폴더에 리사이즈된 이미지가 있는지 watermarked/ 폴더에 워터마크가 삽입된 이미지가 있는지 webp/ 폴더에 WebP 형식으로 변환된 이미지가 있는지
- 7이메일로 SNS 완료 알림을 수신했는지 확인
트러블슈팅
S3 이벤트 트리거가 동작하지 않으면:
- 이벤트 알림에서 접두사(
uploads/)와 접미사(.jpg)가 정확한지 확인 - 대상(Step Functions)의 ARN이 올바른지 확인
- S3가 Step Functions을 호출할 수 있는 리소스 기반 정책이 필요합니다 — EventBridge를 통한 트리거로 변경하는 것을 권장합니다
Lambda에서 "Unable to import module 'PIL'" 에러:
Pillow Lambda Layer가 올바르게 연결되어 있는지 확인하세요. Layer의 디렉토리 구조가 중요합니다:
python/PIL/ 또는 python/lib/python3.12/site-packages/PIL/ 경로에 파일이 있어야 합니다.
Lambda 타임아웃 에러 (Task timed out after X seconds): 이미지 크기가 크면 처리 시간이 길어집니다. Lambda 함수의 타임아웃을 30초 이상으로 늘리고, 메모리를 512MB 이상으로 설정하세요. 일반적으로 5MB 이하 이미지는 512MB 메모리에서 10초 이내에 처리됩니다.
Step Functions에서 "Access Denied" 에러: Step Functions 실행 역할에 Lambda 호출 권한과 SNS 발행 권한이 모두 필요합니다. 상태 머신 생성 시 "새 역할 생성"을 선택하면 자동으로 부여되지만, Lambda ARN을 나중에 변경한 경우 역할 업데이트가 필요합니다.
완성 후 추가 검증
파이프라인의 안정성을 검증하는 추가 테스트를 수행합니다.
- 1다양한 이미지 크기 테스트: 100KB, 1MB, 5MB 이미지를 각각 업로드하여 모두 처리되는지 확인
- 2지원하지 않는 형식 테스트: .png 파일을 uploads/에 업로드 → 접미사 필터(.jpg)로 인해 파이프라인이 트리거되지 않음을 확인
- 3에러 핸들링 테스트: Lambda에서 의도적 에러 발생 → Step Functions 실행 그래프에서 HandleError 상태로 전환 확인
- 4Output 버킷 파일 비교: 원본 이미지 크기와 처리된 이미지 크기를 비교하여 리사이징과 WebP 변환이 실제로 용량을 줄였는지 확인
핵심 개념 확인
확장 아이디어
기본 파이프라인이 완성되었으니, 더 발전시켜 보세요:
- 다중 해상도 생성: Parallel 상태로 300x300, 600x600, 1200x1200 세 가지 크기를 동시에 생성
- 이미지 메타데이터 추출: Pillow의
exif모듈로 촬영 날짜, 카메라 정보 등을 DynamoDB에 저장 - Amazon Rekognition 연동: AI로 이미지에 부적절한 콘텐츠가 있는지 자동 감지
- CloudFront CDN 배포: Output 버킷에 CloudFront를 연결하여 전세계에서 빠르게 접근
- PNG/GIF 지원 확장: 접미사 필터를 추가하고 Lambda에서 포맷별 분기 처리
학습 정리
핵심 치트시트
리소스 정리
실습 완료 후 반드시 아래 순서대로 리소스를 정리하여 불필요한 과금을 방지하세요.
- S3 버킷 내 모든 객체 삭제 (Input, Output 버킷) — 버킷이 비어있어야 삭제 가능
- S3 버킷 삭제 (2개)
- Step Functions 상태 머신 삭제
- Lambda 함수 삭제 (image-resize, image-watermark, image-convert)
- Lambda Layer 삭제 (Pillow)
- IAM 역할 삭제 (Lambda 실행 역할, Step Functions 실행 역할)
- SNS 주제 삭제
- CloudWatch 로그 그룹 삭제