AWS 고급 #7 Step Functions 입문
#3 Lambda, #4 API Gateway, #5 EventBridge / SQS / SNS, #6 Secrets Manager까지 한 축은 함수 / 메시지 / 시크릿이었습니다. 한 단계 더 올라가면 여러 단계의 함수 호출 / 분기 / 병렬을 묶어 하나의 워크플로우로 만드는 방식이 남았습니다.
전통적으로 이런 코드는 한 Lambda 안에 try / except와 if로 짰습니다. 그런데 단계가 5개를 넘어가면 가시성도 디버깅도 힘들어집니다. Step Functions가 그 일을 대신합니다.
이 글이 AWS 고급 시리즈의 마지막입니다. 끝나면 실전 6편으로, 진짜 백엔드를 ECS Fargate에 운영합니다.
Step Functions가 하는 일 #
AWS Step Functions는 매니지드 워크플로우 (state machine) 엔진입니다. 여러 단계를 JSON으로 정의하면, AWS가 단계 진행 / 재시도 / 실패 처리 / 시각화 를 책임집니다.
한 Lambda가 모든 걸 하던 방식 #
def handler(event, context):
user = fetch_user(event["userId"])
if user.plan == "pro":
send_pro_email(user)
else:
send_basic_email(user)
try:
run_billing(user)
except RateLimitError:
time.sleep(60)
run_billing(user)
notify_slack(user)
update_dashboard(user)문제:
- 한 Lambda의 15분 한도 위에 운영됨
- 한 단계가 실패하면 어디서 실패했는지 로그 헤매기
- 단계별 retry 정책이 코드 안에 포함되어 있음
- 같은 흐름의 다른 변형을 추가하면 if 폭증
- 운영자가 “지금 어디까지 됐지?” 알기 어려움
Step Functions가 푸는 문제 #
입력
│
▼
┌──────────────────┐
│ FetchUser │ Lambda 호출
└──────┬───────────┘
├ "pro" → SendProEmail
└ "basic" → SendBasicEmail
│
▼
┌──────────────────┐
│ RunBilling │ retry: 3회, backoff 60s
└──────┬───────────┘
▼
┌─ Parallel ───────────────┐
│ NotifySlack │ UpdateDash │
└─────────────┴────────────┘
│
▼
완료각 단계가 시각화되고, 실행마다 콘솔에서 어디서 멈췄는지 한눈에 파악할 수 있습니다. retry는 선언적으로, 분기는 데이터 기반으로 작동합니다.
Standard vs Express #
Step Functions에는 두 가지 모드가 있습니다.
| Standard | Express | |
|---|---|---|
| 실행 시간 | 최대 1년 | 최대 5분 |
| 가격 모델 | 단계 전환 (state transition)당 | 호출 + 메모리 + 시간 (Lambda와 비슷) |
| 실행 이력 | 90일 보관, 시각화 | 짧음, CloudWatch Logs만 |
| 처리량 | ~25,000 / 초 | ~100,000 / 초 |
| At-least-once vs Exactly-once | Exactly-once | At-least-once |
| 적합한 경우 | 인간이 추적해야 할 비즈니스 워크플로우 (주문, 환불) | 짧은 / 고처리량 (이벤트 처리, 데이터 변환) |
처음에는 Standard. 짧은 처리 + 빈번한 호출이 명확해지면 Express.
Amazon States Language (ASL) #
워크플로우는 JSON으로 정의. ASL이라 부릅니다.
{
"Comment": "첫 state machine",
"StartAt": "SayHello",
"States": {
"SayHello": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:hello-fn",
"End": true
}
}
}StartAt으로 시작하고, States 안의 각 상태 / 단계 정의. 각 상태는 Type과 다음 상태 (Next 또는 End: true)를 가짐.
만들기 + 실행 #
SM_ARN=$(aws stepfunctions create-state-machine \
--name hello-flow \
--definition file://hello.asl.json \
--role-arn arn:aws:iam::123456789012:role/stepfn-role \
--type STANDARD \
--query stateMachineArn --output text)
# 실행
aws stepfunctions start-execution \
--state-machine-arn $SM_ARN \
--input '{"name":"world"}'콘솔의 시각화. 노드들이 그래프로 그려지고, 실행마다 노드가 색이 입혀집니다.
4가지 핵심 상태 #
1) Task: 실제 일 #
가장 자주 쓰는 상태. Lambda / ECS Task / SDK / SNS / SQS 등 외부 자원을 호출.
{
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:fetch-user",
"InputPath": "$",
"OutputPath": "$",
"ResultPath": "$.user",
"Next": "BranchByPlan"
}InputPath: 들어오는 데이터에서 어느 부분을 함수에 보낼지OutputPath: 다음 상태로 넘길 부분ResultPath: 함수의 결과를 입력 데이터의 어느 위치에 합칠지
Service Integration: 직접 통합 #
Lambda 외에도 AWS SDK를 직접 호출. Lambda 안에서 boto3 호출하던 일을 ASL로 바로:
{
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:dynamodb:putItem",
"Parameters": {
"TableName": "users",
"Item": {
"id": {"S.$": "$.user.id"},
"name": {"S.$": "$.user.name"}
}
},
"Next": "Done"
}Lambda 한 개가 줄어듭니다. 코드 / 배포 / 콜드 스타트가 사라집니다.
Optimized Integration #
자주 쓰이는 패턴은 단축 ARN. .sync가 끝의 결과까지 기다림:
{
"Type": "Task",
"Resource": "arn:aws:states:::ecs:runTask.sync",
"Parameters": {
"Cluster": "prod-cluster",
"TaskDefinition": "myapp:42",
"LaunchType": "FARGATE",
...
},
"Next": "AfterEcs"
}ECS Task가 끝날 때까지 (혹은 실패할 때까지) 대기. Step Functions는 폴링을 자동.
2) Choice: 분기 #
데이터 값을 보고 다음 상태 결정.
{
"Type": "Choice",
"Choices": [
{
"Variable": "$.user.plan",
"StringEquals": "pro",
"Next": "SendProEmail"
},
{
"Variable": "$.user.plan",
"StringEquals": "basic",
"Next": "SendBasicEmail"
}
],
"Default": "SendDefaultEmail"
}3) Parallel: 병렬 가지 #
여러 분기를 동시에 실행하고 결과 모두 받아서 합침.
{
"Type": "Parallel",
"Branches": [
{
"StartAt": "NotifySlack",
"States": {
"NotifySlack": { "Type": "Task", "Resource": "...", "End": true }
}
},
{
"StartAt": "UpdateDashboard",
"States": {
"UpdateDashboard": { "Type": "Task", "Resource": "...", "End": true }
}
}
],
"Next": "Done"
}각 가지가 독립으로 실행 + retry. 한 가지 실패하면 (catch 안 하면) 전체 실패.
4) Map: 컬렉션 처리 #
배열의 각 아이템에 대해 같은 흐름을 반복. for-each의 분산 버전.
{
"Type": "Map",
"ItemsPath": "$.orders",
"MaxConcurrency": 10,
"ItemProcessor": {
"ProcessorConfig": { "Mode": "INLINE" },
"StartAt": "ProcessOrder",
"States": {
"ProcessOrder": { "Type": "Task", "Resource": "...", "End": true }
}
},
"End": true
}100개 주문을 동시 10개씩 처리, 모두 끝나면 종료. Distributed Map 모드는 1만 ~ 100만 아이템 규모도 가능 (S3 객체 일괄 처리, ETL 등).
보조 상태들 #
- Pass. 데이터 가공만, 외부 호출 없음
- Wait. 일정 시간 / 특정 시각까지 대기
- Succeed / Fail. 명시적 종료
에러 처리: Retry / Catch #
워크플로우의 가치 중 큰 부분입니다. 단계마다 선언적으로.
Retry #
{
"Type": "Task",
"Resource": "arn:aws:lambda:...:run-billing",
"Retry": [
{
"ErrorEquals": ["States.TaskFailed", "BillingRateLimitError"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2.0
},
{
"ErrorEquals": ["States.Timeout"],
"IntervalSeconds": 30,
"MaxAttempts": 1
}
],
"Next": "AfterBilling"
}States.TaskFailed는 일반 실패, States.Timeout은 타임아웃, 또는 Lambda가 throw 한 사용자 정의 에러 이름. 5초 → 10초 → 20초 으로 backoff.
Catch #
{
"Type": "Task",
"Resource": "...",
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "HandleFailure"
}
],
"Next": "AfterTask"
}retry까지 다 실패하면 HandleFailure로. 보상 트랜잭션 (롤백) / 알림 / 사람 개입 큐로.
자주 쓰는 패턴 #
1) Saga: 보상 트랜잭션 #
분산 시스템에서 트랜잭션이 안 통할 때. 각 단계의 정방향 + 실패 시 보상.
주문생성 → 결제 → 재고차감 → 배송예약
↓ ↓ ↓ ↓ (실패!)
주문취소 결제환불 재고원복 X각 Task에 Catch를 두고, 실패 시 보상 단계들을 역순으로 실행.
2) Human-in-the-loop #
대기하다가 사람이 승인 / 거부하면 진행:
{
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish.waitForTaskToken",
"Parameters": {
"TopicArn": "...",
"Message": {
"TaskToken.$": "$$.Task.Token",
"OrderId.$": "$.orderId"
}
},
"Next": "AfterApproval"
}waitForTaskToken. 토큰을 외부 (이메일, 슬랙 봇 등)로 보내고, 누가 SendTaskSuccess / SendTaskFailure API를 호출할 때까지 대기. 최대 1년.
3) Polling 패턴 #
긴 외부 작업의 완료 대기:
StartJob → WaitState(30s) → CheckJob → Choice
↓
(계속) WaitState로 회귀
(완료) Next4) Express 워크플로우: 이벤트 처리 #
EventBridge / SQS가 트리거 → 짧은 처리 (Lambda 1~3개) → 결과를 DynamoDB / S3.
Express의 빠른 처리량과 짧은 시간 한도가 자연스럽게 맞음.
Lambda와 비교: 언제 무엇 #
Lambda 한 개로 충분한 경우 #
- 단계 1~2개의 짧은 처리
- 시각화 / 사람 추적이 필요 없음
- 매우 자주 호출 + 매우 짧음 (Step Functions의 단계 전환 비용이 부담)
Step Functions가 어울리는 경우 #
- 단계 3개 이상 + 분기 / 재시도 / 병렬
- 실패 / 진행 상황을 사람이 봐야 함
- 외부 시스템과의 긴 상호작용 (사람 승인, 외부 API)
- 워크플로우 자체가 비즈니스 자산 (수정 이력 추적)
같이 쓰는 방식 #
대부분은 둘이 함께 씁니다. Step Functions가 흐름을 통제하고, 각 단계는 Lambda / ECS / SDK 호출을 맡습니다.
자주 만나는 함정 #
1) JSONPath 오타 #
"Variable": "$.user.plan"의 점 / $ 한 자만 틀리면 매칭 0. 콘솔의 input/output 검사기로 한 단계씩 확인.
2) Lambda의 출력이 너무 큼 #
Step Functions의 한 상태 입력 / 출력 페이로드 한도 256KB. 큰 데이터는 S3에 두고 키만 전달.
{
"s3Bucket": "myapp-pipeline",
"s3Key": "jobs/abc123/input.json"
}3) 매 단계 전환마다 비용 #
Standard 모드 단계 전환 1,000회당 $0.025. 단계가 많은 워크플로우의 전체 비용은 단계 수 × 호출 수. 짧은 단계 (Pass)를 너무 많이 두면 의외로 큼.
4) Lambda의 cold start가 단계마다 #
각 Task가 별도 Lambda면 Cold Start가 단계마다. Express + Provisioned Concurrency, 또는 한 Lambda에 여러 단계 합치기.
5) Retry의 BackoffRate 폭주 #
MaxAttempts: 10, BackoffRate: 3.0이면 1 → 3 → 9 → 27 → 81초… 사용자가 기다리지 못하는 시간. 합계가 합리적인지 계산.
6) Catch가 한 번만 실행 #
retry가 다 끝난 후 catch가 한 번. catch 안에서 또 실패하면 워크플로우 실패. catch 안의 task도 retry 옵션 검토.
7) 시각화에 안 보이는 외부 호출 #
Lambda 안에서 boto3로 호출한 부분은 시각화 / 추적에 안 잡힘. 가능하면 Service Integration으로 ASL의 Task 상태로 끌어내기.
정리 #
이번 글에서 잡은 것:
- Step Functions의 역할. 여러 단계의 함수 / SDK 호출을 JSON 워크플로우로. 시각화 / retry / 분기 / 병렬이 선언적
- Standard vs Express. Standard는 길고 비싼 비즈니스 / Express는 짧고 고처리량 이벤트
- ASL. JSON으로 정의.
StartAt+States+ 각 상태의Type/Next - 4 핵심 상태. Task (일) / Choice (분기) / Parallel (병렬) / Map (컬렉션)
- Service Integration. Lambda 없이 SDK 직접. ECS
.sync, DynamoDB, SNS 등 - Retry / Catch. 단계마다 선언적. backoff와 catch 흐름
- 자주 쓰는 패턴. Saga (보상 트랜잭션), Human-in-the-loop (
waitForTaskToken), Polling, Express 이벤트 처리 - Lambda vs Step Functions. 단계 1~2개 → Lambda 한 개. 3개 이상 + 분기 / 재시도 / 시각화 필요 → Step Functions
- 함정. JSONPath 오타, 256KB payload (S3 우회), 단계 전환 비용, Lambda 다단계 cold start, BackoffRate 폭주, Catch 한 번, Service Integration 안 쓴 외부 호출
시리즈를 마무리하며 #
#1 ECS / Fargate 부터 7편, 컨테이너 (ECS / ECR), 서버리스 (Lambda / API Gateway), 메시지 (EventBridge / SQS / SNS), 시크릿 (Secrets Manager / Parameter Store), 워크플로우 (Step Functions) 까지 AWS 운영의 도구상자가 모였습니다.
기초 7편의 IAM / 비용 / 보안과 중급 7편의 EC2 / VPC / S3 / RDS / ALB / CloudFront 위에 이 7개를 얹으면, **백엔드를 AWS에 올려 운영하기 위해 필요한 것의 90%**가 갖춰집니다.
다음 시리즈: AWS 실전 #
이론은 다 나왔습니다. 이제 진짜 백엔드를 한 프로젝트로 만들 차례입니다.
AWS 실전 #1: ECS Fargate에 백엔드 배포에서는 모던 파이썬 실전 (FastAPI) / 장고 실전 (DRF)에서 만든 API를 ECS Fargate 위에 운영 가능 형태로 올립니다. RDS, ALB, ACM, Route 53, Secrets Manager가 한곳에 모이는 6편짜리 트랙의 시작입니다.