목차
21 장

Step Functions 입문

AWS 워크플로우 엔진을 정리합니다. State machine의 역할, Task / Choice / Parallel / Map 네 가지 상태, Standard vs Express, Amazon States Language(ASL), Lambda / ECS / SDK 통합, Retry / Catch 에러 처리, Saga·Human-in-the-loop 같은 패턴까지 다룹니다.

17장 Lambda 기초, 18장 API Gateway + Lambda, 19장 EventBridge / SQS / SNS, 20장 Secrets Manager / Parameter Store까지 한 축은 함수 / 메시지 / 시크릿이었습니다. 한 단계 더 올라가면 여러 단계의 함수 호출 / 분기 / 병렬을 묶어 하나의 워크플로우로 만드는 방식이 남았습니다.

전통적으로 이런 코드는 한 Lambda 안에 try / except와 if로 짰습니다. 그런데 단계가 5개를 넘어가면 가시성도 디버깅도 힘들어집니다. Step Functions가 그 방식을 대신합니다.

본 챕터는 3부 컨테이너 · 서버리스의 마지막입니다. 끝나면 4부 22장 ECS Fargate 배포 골격으로 넘어가 — 콘솔에서 잡은 멘탈 모델을 Terraform 코드로 옮기고, 진짜 백엔드를 ECS Fargate 위에 운영하기 시작합니다.

Step Functions가 하는 일 #

AWS Step Functions는 매니지드 워크플로우 (state machine) 엔진입니다. 여러 단계를 JSON으로 정의하면, AWS가 단계 진행 / 재시도 / 실패 처리 / 시각화를 책임집니다.

한 Lambda가 모든 걸 하던 방식 #

모놀리식 Lambda — 5단계 처리
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가 푸는 문제 #

Step Functions의 그림
입력
┌──────────────────┐
│ FetchUser        │  Lambda 호출
└──────┬───────────┘
       ├ "pro" → SendProEmail
       └ "basic" → SendBasicEmail
┌──────────────────┐
│ RunBilling       │  retry: 3 회, backoff 60s
└──────┬───────────┘
┌─ Parallel ───────────────┐
│ NotifySlack │ UpdateDash │
└─────────────┴────────────┘
완료

각 단계가 시각화되고, 실행마다 콘솔에서 어디서 멈췄는지 한눈에 보입니다. retry는 선언적이고, 분기는 데이터 기반입니다.

Standard vs Express #

Step Functions의 두 모드입니다.

StandardExpress
실행 시간최대 1 년최대 5분
가격 모델단계 전환 (state transition) 당호출 + 메모리 + 시간 (Lambda와 비슷)
실행 이력90일 보관, 시각화짧음, CloudWatch Logs만
처리량~25,000 / 초~100,000 / 초
At-least-once vs Exactly-onceExactly-onceAt-least-once
적합한 경우인간이 추적해야 할 비즈니스 워크플로우 (주문, 환불)짧은 / 고처리량 (이벤트 처리, 데이터 변환)

처음에는 Standard를 씁니다. 짧은 처리 + 빈번한 호출이 명확해지면 Express로 갑니다.

Amazon States Language (ASL) #

워크플로우는 JSON으로 정의합니다. ASL이라 부릅니다.

hello.asl.json
{
  "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)를 가집니다.

만들기 + 실행 #

state machine 생성
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 등 외부 자원을 호출합니다.

Task — Lambda 호출
{
  "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로 바로 합니다.

DynamoDB에 직접 put
{
  "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가 끝의 결과까지 기다립니다.

ECS Task 동기 실행
{
  "Type": "Task",
  "Resource": "arn:aws:states:::ecs:runTask.sync",
  "Parameters": {
    "Cluster": "prod-cluster",
    "TaskDefinition": "myapp:42",
    "LaunchType": "FARGATE",
    ...
  },
  "Next": "AfterEcs"
}

ECS Task가 끝날 때까지 (혹은 실패할 때까지) 대기합니다. Step Functions가 폴링을 자동으로 합니다 (15장 ECS와 Fargate와 연결됩니다).

2) Choice — 분기 #

데이터 값을 보고 다음 상태를 결정합니다.

Choice
{
  "Type": "Choice",
  "Choices": [
    {
      "Variable": "$.user.plan",
      "StringEquals": "pro",
      "Next": "SendProEmail"
    },
    {
      "Variable": "$.user.plan",
      "StringEquals": "basic",
      "Next": "SendBasicEmail"
    }
  ],
  "Default": "SendDefaultEmail"
}

3) Parallel — 병렬 가지 #

여러 분기를 동시에 실행하고 결과를 모두 받아서 합칩니다.

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의 분산 버전입니다.

Map
{
  "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 #

Retry — 3회까지 backoff로 재시도
{
  "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 #

Catch — 실패 시 별도 흐름
{
  "Type": "Task",
  "Resource": "...",
  "Catch": [
    {
      "ErrorEquals": ["States.ALL"],
      "ResultPath": "$.error",
      "Next": "HandleFailure"
    }
  ],
  "Next": "AfterTask"
}

retry까지 다 실패하면 HandleFailure로 갑니다 — 보상 트랜잭션 (롤백) / 알림 / 사람 개입 큐로 보냅니다.

자주 쓰는 패턴 #

1) Saga — 보상 트랜잭션 #

분산 시스템에서 트랜잭션이 안 통할 때 씁니다. 각 단계의 정방향 + 실패 시 보상입니다.

Saga
주문생성 → 결제 → 재고차감 → 배송예약
   ↓        ↓        ↓        ↓ (실패!)
   주문취소  결제환불  재고원복   X

각 Task에 Catch를 두고, 실패 시 보상 단계들을 역순으로 실행합니다.

2) Human-in-the-loop #

대기하다가 사람이 승인 / 거부하면 진행합니다.

Wait for callback
{
  "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로 회귀
                         (완료) Next

4) Express 워크플로우 — 이벤트 처리 #

EventBridge / SQS가 트리거하고 → 짧은 처리 (Lambda 1~3개) → 결과를 DynamoDB / S3에 둡니다.

Express의 빠른 처리량과 짧은 시간 한도가 자연스럽게 맞습니다 (19장 EventBridge / SQS / SNS와 연결됩니다).

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에 여러 단계를 합칩니다 (17장 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 상태로 끌어냅니다.

연습문제 #

  1. 본인이 떠올린 다단계 처리(예: 주문 → 결제 → 배송)를 §“Lambda와 비교 — 언제 무엇"의 기준에 비춰, 한 Lambda로 충분한지 Step Functions가 필요한지 판단하고 이유를 한 줄로 적어 보세요.
  2. Saga 패턴을 본인의 워크플로우에 적용한다고 가정하고, 각 정방향 단계와 그에 대응하는 보상 단계를 §“자주 쓰는 패턴"의 그림처럼 표로 그려 보세요. 어느 단계에 Catch를 달아야 하는지도 함께 표시합니다.
  3. §“자주 만나는 함정"에서 단계 전환 비용과 단계마다의 콜드 스타트가 모두 단계 수에 비례하는 이유를 설명하고, 단계를 줄이기 위해 17장 Lambda 기초의 어떤 선택(한 Lambda에 합치기 / Service Integration)을 쓸 수 있는지 한 단락으로 정리해 보세요.

한 줄 요약: Step Functions는 여러 단계의 함수·SDK 호출을 JSON(ASL)으로 묶는 매니지드 워크플로우 엔진으로, 시각화·retry·분기·병렬이 선언적이다. 길고 추적이 필요한 비즈니스 워크플로우는 Standard, 짧고 고처리량 이벤트는 Express를 쓴다. 핵심 상태는 Task·Choice·Parallel·Map이고 Service Integration으로 Lambda 없이 SDK를 직접 부른다. 단계 1~2개는 Lambda 한 개로 충분하고, 3개 이상에 분기·재시도·시각화가 필요하면 Step Functions가 맞으며, 256KB payload·단계 전환 비용·다단계 콜드 스타트가 흔한 함정이다.

다음 챕터 #

이론은 다 나왔습니다. 3부에서 컨테이너(ECS / ECR), 서버리스(Lambda / API Gateway), 메시지(EventBridge / SQS / SNS), 시크릿(Secrets Manager / Parameter Store), 워크플로우(Step Functions)까지 AWS 운영의 도구상자를 모았습니다. 다음 4부 22장 ECS Fargate 배포 골격부터는 콘솔에서 잡은 멘탈 모델을 Terraform 코드로 옮기고, 진짜 백엔드를 ECS Fargate 위에 운영 가능한 형태로 올리기 시작합니다.

X