기준: DVA-C02 시험 가이드오류 제보
지금까지 본 SQS·SNS·Kinesis는 컴포넌트를 떼어놓는 도구였다. 그런데 현실의 비즈니스 프로세스는 떼어놓은 조각들을 다시 순서대로 엮어야 한다. "주문을 검증하고 → 재고를 확인하고 → 결제하고 → 실패하면 환불하고 → 성공하면 알림을 보낸다." 이를 Lambda 안에 if-else와 try-catch로 짜 넣으면 그 함수는 곧 비대해지고, 상태는 코드 깊숙이 숨어 "지금 어느 단계에서 멈췄는지" 알 수 없게 된다. AWS Step Functions는 이 흐름(workflow)을 코드 밖으로 꺼내 명시적인 상태 기계로 만든다. AppSync는 비슷한 정신을 데이터 계층에 적용해, 여러 백엔드에서 데이터를 모으는 "흐름"을 GraphQL 한 쿼리로 선언하게 한다.
DVA-C02에서 Step Functions는 "Lambda를 어떻게 오케스트레이션하나", "Standard와 Express의 차이", "외부 시스템 응답을 어떻게 기다리나"라는 형태로 나오기 쉽다. AppSync는 "실시간 구독", "다중 데이터 소스", "API Gateway와의 차이"로 출제된다. 이번 글은 워크플로를 코드에서 분리하는 것이 왜 가치 있는지, 상태 기계라는 이론적 토대, 서비스 통합 패턴(.sync, .waitForTaskToken)이 푸는 문제, GraphQL이 REST와 근본적으로 다른 지점을 다룬다.
Lambda 함수 5개를 순서대로 실행해야 하는 경우를 생각한다. 가장 단순한 방법은 함수 A가 끝나면 함수 B를 직접 호출(invoke)하고, B가 C를 호출하는 체인이다. 이를 코레오그래피(choreography) 라 부른다. 중앙 지휘자 없이 각자가 다음을 안다. 이 방식은 빠르게 무너진다. C에서 에러가 나면 A, B를 어떻게 롤백하나? 지금 워크플로가 어느 단계인가? B를 재시도하려면? 이 로직이 각 함수에 흩어져 코드가 얽힌다.
오케스트레이션(orchestration) 은 정반대다. 중앙 지휘자(Step Functions)가 전체 흐름을 알고, 각 단계를 호출하며, 에러·재시도·분기·병렬을 모두 관리한다. 각 Lambda는 자기 일만 하고 "다음에 뭘 할지"를 모른다. 흐름의 로직이 코드가 아니라 선언적 정의(Amazon States Language, JSON) 로 한곳에 모여, 시각화되고 실행 이력이 남는다.
{
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:ValidateOrder",
"Next": "CheckInventory",
"Catch": [{"ErrorEquals": ["ValidationError"], "Next": "OrderFailed"}]
},
"CheckInventory": {
"Type": "Choice",
"Choices": [{"Variable": "$.inStock", "BooleanEquals": false, "Next": "Refund"}],
"Default": "ProcessPayment"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:ProcessPayment",
"Retry": [{"ErrorEquals": ["Lambda.ServiceException"],
"IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0}],
"Next": "OrderSuccess"
},
"OrderSuccess": {"Type": "Succeed"},
"OrderFailed": {"Type": "Fail", "Error": "OrderError"},
"Refund": {"Type": "Task", "Resource": "arn:aws:lambda:...:Refund", "Next": "OrderFailed"}
}
}Step Functions가 구현하는 것은 컴퓨터 과학의 기본 개념인 유한 상태 기계(Finite State Machine, FSM) 다. 시스템이 유한한 상태들 중 하나에 있고, 입력에 따라 상태 간 전이가 일어나는 모델이다. FSM은 1950년대(Mealy, Moore 기계)부터 오토마타 이론의 토대였고, 정규 표현식 엔진·네트워크 프로토콜·UI 상태 관리가 모두 FSM이다. Step Functions는 이 추상화를 분산 워크플로에 적용해, 각 "상태(State)"가 작업·분기·병렬·대기를 표현하고 전이가 Next로 선언된다. 흐름을 FSM으로 모델링하면 "도달 불가능한 상태", "빠진 전이", "무한 루프" 같은 결함을 시각적으로 잡아낼 수 있다. 코드에 묻혀 있을 때는 보이지 않던 것들이다.
오케스트레이션 대 코레오그래피는 마이크로서비스 설계의 오래된 논쟁이다.
Step Functions에는 두 가지 워크플로 타입이 있고, 시험에서 둘 중 무엇을 고르느냐로 갈리는 문제가 나온다. 둘은 같은 상태 언어를 쓰지만 실행 엔진과 보장이 다르다.
| 항목 | Standard | Express |
|---|---|---|
| 최대 실행 시간 | 1년 | 5분 |
| 실행 모델 | 정확히 1회(exactly-once) | At-least-once |
| 처리량 | 2,000 starts/s | 100,000 starts/s |
| 실행 이력 | 콘솔 + CloudWatch(90일 조회) | CloudWatch Logs만 |
| 과금 | 상태 전환당 | 실행 시간 + 메모리 |
| 용도 | 긴/중요한 워크플로(인간 승인, 배포, 주문) | 짧고 빈번한 이벤트 처리(IoT, 스트림) |
핵심 직관은 이렇다. Standard는 "오래 걸리고 한 번이 중요한" 흐름에 쓴다. 인간 승인을 며칠 기다리는 배포 파이프라인, 정확히 한 번 결제해야 하는 주문이 예다. Express는 "짧고 폭발적이고 멱등한" 흐름에 쓴다. 초당 수만 건의 이벤트를 빠르게 변환하는 경우가 예다. Express가 at-least-once라는 것은 중복 실행 가능성을 뜻하므로 소비자가 멱등해야 한다(SQS 표준에서 본 것과 같은 원리).
⚠️ 함정: "초당 수만 건의 짧은 이벤트 처리"에 Standard를 고르면 처리량(2,000/s)과 비용(상태 전환당 과금이 누적)에서 막힌다. 답은 Express다. 반대로 "결제를 정확히 한 번, 인간 승인을 기다리며 며칠"이면 Express의 5분 한도와 at-least-once 때문에 부적합하고 Standard가 답이다. 실행 시간(1년 vs 5분)과 실행 보장(exactly-once vs at-least-once)이 두 결정 축이다.
Task 상태가 외부 서비스를 호출할 때 "언제 다음으로 넘어가나"는 패턴마다 다르다. 시험에서 구분을 묻기 쉬운 세 패턴이다.
| 패턴 | URI 형식 | 동작 |
|---|---|---|
| Request-Response | arn:aws:states:::lambda:invoke | 호출하고 즉시 응답받아 다음으로 |
Run a Job (.sync) | arn:aws:states:::ecs:runTask.sync | 작업이 완료될 때까지 대기 |
Wait for Callback (.waitForTaskToken) | arn:aws:states:::sqs:sendMessage.waitForTaskToken | 외부가 토큰으로 응답할 때까지 대기 |
Request-Response는 가장 단순하다. 호출하고 응답이 오면 끝이다. .sync 는 "오래 걸리는 작업을 시작하고 그것이 끝날 때까지 워크플로를 멈춰 기다린다"는 패턴이다. ECS 태스크나 Glue 작업은 몇 분~몇 시간 걸리는데, .sync를 쓰면 Step Functions가 그 완료를 폴링해 기다렸다가 다음으로 넘어간다.
.waitForTaskToken 은 기계가 아니라 사람이나 외부 시스템의 응답을 기다리는 경우를 위한 것이다. Step Functions가 작업과 함께 태스크 토큰을 발급하고 워크플로를 일시정지한다. 외부(승인자가 누른 버튼, 외부 결제사의 콜백 등)가 그 토큰과 함께 SendTaskSuccess/SendTaskFailure를 호출하면 워크플로가 재개된다. 며칠이 걸려도 상관없다(Standard의 1년 한도 안에서).
.waitForTaskToken의 전형적 사용처가 인간 승인(human-in-the-loop) 워크플로다. 환불 요청이 일정 금액을 넘으면 워크플로가 관리자에게 승인 이메일/Slack을 보내고(토큰 포함) 멈춘다. 관리자가 승인 링크를 클릭하면 그 토큰으로 콜백이 와 워크플로가 재개되어 환불이 진행된다. 인간의 응답 시간(분~일)을 워크플로에 자연스럽게 통합하는 이 패턴은 코드 기반 체인으로는 구현하기 어렵지만, Step Functions에서는 한 상태로 표현된다. 외부 결제 게이트웨이나 서드파티 API의 비동기 콜백을 기다리는 데도 같은 패턴을 쓴다.
대용량 병렬 처리를 위한 Map 상태도 알아 둘 가치가 있다.
Step Functions가 작업의 흐름을 선언적으로 만들었다면, AppSync는 데이터 수집의 흐름을 선언적으로 만든다. 모바일 화면 하나가 "사용자 정보 + 최근 주문 + 추천 상품"을 보여 줘야 하는 경우를 생각한다. REST라면 클라이언트가 세 엔드포인트를 각각 호출하고(over-fetching/under-fetching, 여러 왕복) 결과를 조합해야 한다. GraphQL은 클라이언트가 필요한 데이터의 모양을 한 쿼리로 선언하면, 서버가 여러 데이터 소스에서 모아 정확히 그 모양으로 돌려준다.
type Query {
getOrder(orderId: ID!): Order # → DynamoDB 리졸버
}
type Mutation {
updateOrderStatus(orderId: ID!, status: String!): Order # → Lambda 리졸버
}
type Subscription {
onOrderStatusUpdate(orderId: ID!): Order
@aws_subscribe(mutations: ["updateOrderStatus"]) # 실시간 구독
}AppSync의 핵심 개념은 리졸버(resolver) 다. GraphQL 필드 하나를 특정 데이터 소스(DynamoDB, Lambda, RDS, HTTP, OpenSearch)에 매핑하는 함수다. getOrder는 DynamoDB를 직접 읽고, updateOrderStatus는 Lambda로 비즈니스 로직을 태우는 식으로, 한 GraphQL 스키마 안에서 필드마다 다른 백엔드를 쓸 수 있다. "다중 데이터 소스를 한 쿼리로"라는 GraphQL의 강점을 AWS에서 구현한 것이다.
| 항목 | AppSync (GraphQL) | API Gateway (REST) |
|---|---|---|
| 데이터 페칭 | 한 쿼리로 필요한 만큼 정확히 | 엔드포인트별 고정 응답 |
| 다중 소스 | 한 쿼리에서 여러 백엔드 | 보통 단일 백엔드 |
| 실시간 | 내장 구독(WebSocket) | WebSocket API 별도 구성 |
| 캐싱 | 응답 수준 | 메서드 수준 |
| 적합 | 모바일·복잡한 그래프 쿼리 | 일반 REST API |
GraphQL(Facebook, 2015 공개)이 등장한 동기는 모바일의 제약이었다. 모바일은 네트워크 왕복이 비싸고 데이터 요금이 들어, REST의 "엔드포인트마다 고정된 큰 응답을 여러 번 호출"하는 방식이 비효율적이었다.
AppSync의 실시간 구독(Subscription) 은 @aws_subscribe 디렉티브로 특정 뮤테이션에 묶인다. updateOrderStatus 뮤테이션이 실행되면, 그 주문을 구독 중인 모든 클라이언트에게 AppSync가 WebSocket으로 변경을 푸시한다. 폴링 없이 서버가 변경을 밀어 주는 이 모델은 채팅·실시간 대시보드·협업 앱의 기반이다. 폴링에는 "변화가 없어도 계속 묻는" 낭비와 "변화 후 다음 폴링까지의 지연"이 있는데, 구독은 변화 시점에 정확히 한 번 푸시되어 둘 다 없앤다. Day 1에서 본 SQS 롱 폴링이 풀려던 문제를 아예 푸시로 뒤집은 셈이다. 인증은 API_KEY/IAM/Cognito User Pool/OIDC/Lambda Authorizer를 지원한다.
이번 글을 관통하는 한 문장은 "흐름을 코드에서 꺼내 선언적으로 만든다"이다. Step Functions는 작업의 흐름을 유한 상태 기계로 명시해 가시성·재시도·에러 처리·인간 승인을 코드 밖으로 꺼냈고, Standard와 Express는 "오래·정확히 1회"와 "짧고·빈번·멱등"으로 갈린다. .sync는 작업 완료를, .waitForTaskToken은 외부/인간의 응답을 워크플로 안에서 기다리게 하며, Distributed Map은 대량 병렬을 푼다. AppSync는 같은 정신을 데이터 계층에 적용해, 다중 백엔드에서 데이터를 모으는 흐름을 GraphQL 한 쿼리로 선언하고 실시간 구독으로 변경을 푸시한다. 오케스트레이션도 GraphQL도 "흩어진 조각을 다시 엮되, 엮는 로직을 코드가 아닌 선언으로 한곳에 모은다"는 같은 사상의 두 적용이다.
다음 글에서는 Week 11에서 다룬 SQS·SNS·Kinesis·Step Functions·AppSync 전체를 시나리오 중심으로 종합 복습한다.
선택지를 클릭하면 정답·해설이 펼쳐집니다.
문제 1
5개의 Lambda를 순서대로 실행하되, 중간 단계 실패 시 이전 작업을 보상(롤백)하고 전체 흐름을 시각적으로 추적해야 한다. 가장 적절한 방법은?
문제 2
초당 약 5만 건의 짧은 IoT 이벤트를 변환하는 워크플로가 필요하다. 각 실행은 수 초 내 끝나고 중복 실행은 멱등 처리로 허용된다. 적절한 Step Functions 타입은?
문제 3
환불 워크플로에서 일정 금액 초과 시 관리자 승인을 받아야 하며, 승인까지 며칠이 걸릴 수 있다. 승인 후 워크플로를 재개하려면?
문제 4
S3 버킷의 수십만 개 객체를 각각 변환하는 대량 병렬 처리가 필요하다. 적절한 Step Functions 상태는?
문제 5
모바일 앱이 한 화면에 사용자 정보(DynamoDB), 주문 내역(Lambda 경유), 추천(OpenSearch)을 한 번에 효율적으로 가져오고, 주문 상태 변경을 실시간으로 받아야 한다. 적절한 서비스는?
문제 6
Step Functions Standard와 Express의 차이로 옳은 것은?
문제 7
GraphQL이 모바일 환경에서 REST보다 효율적인 핵심 이유는?