AWS SAA 실습 문제: EventBridge와 Step Functions를 활용한 이벤트 기반 워크플로우 아키텍처
📋 문제 시나리오
📋 문제 시나리오
당신은 글로벌 물류 회사의 시스템 아키텍트로 근무하고 있습니다. 회사에서는 기존의 모놀리식 주문 처리 시스템을 마이크로서비스 기반의 이벤트 기반 아키텍처로 전환하려고 합니다. 복잡한 비즈니스 워크플로우를 처리하면서도 각 서비스 간의 느슨한 결합을 유지해야 합니다.
비즈니스 프로세스
주문이 접수되면 다음과 같은 복잡한 워크플로우가 실행되어야 합니다:
- 주문 검증 → 재고 확인 → 결제 처리
- 배송 준비 → 창고 할당 → 포장 지시
- 배송 시작 → 운송업체 선택 → 추적번호 생성
- 고객 알림 → 이메일/SMS 발송 → 앱 푸시 알림
기술적 요구사항
- 이벤트 기반 아키텍처: 서비스 간 비동기 통신
- 워크플로우 관리: 복잡한 비즈니스 로직 처리
- 오류 처리: 실패 시 재시도 및 복구 메커니즘
- 모니터링: 실시간 워크플로우 상태 추적
- 확장성: 주문량 증가에 따른 자동 확장
- 내구성: 메시지 손실 방지 및 정확히 한 번 처리
제약 조건
- 처리 시간: 주문 접수부터 배송 시작까지 30분 이내
- 가용성: 99.9% 서비스 가용성 보장
- 일관성: 분산 트랜잭션 처리
- 비용: 기존 시스템 대비 30% 비용 절감
🎯 해결 방안
1. 전체 아키텍처 설계
[API Gateway] → [Lambda] → [EventBridge] → [Step Functions]
↓ ↓
[DynamoDB] ← [Lambda] ← [SQS/SNS] ← [Multiple Services]
↓
[CloudWatch] → [X-Ray] → [Monitoring Dashboard]
2. 이벤트 기반 아키텍처 설계
A. EventBridge 이벤트 스키마
{
"source": "ecommerce.orders",
"detail-type": "Order Placed",
"detail": {
"orderId": "ORD-2025-001234",
"customerId": "CUST-567890",
"items": [
{
"productId": "PROD-12345",
"quantity": 2,
"price": 29.99
}
],
"totalAmount": 59.98,
"currency": "USD",
"shippingAddress": {
"street": "123 Main St",
"city": "Seoul",
"country": "KR",
"zipCode": "12345"
},
"timestamp": "2025-07-09T10:30:00Z"
}
}
B. 이벤트 라우팅 규칙
EventBridge Rules:
OrderValidationRule:
EventPattern:
source: ["ecommerce.orders"]
detail-type: ["Order Placed"]
Targets:
- StepFunction: OrderProcessingWorkflow
- Lambda: OrderValidationFunction
- SQS: InventoryCheckQueue
PaymentProcessedRule:
EventPattern:
source: ["payment.service"]
detail-type: ["Payment Completed"]
Targets:
- StepFunction: FulfillmentWorkflow
- SNS: OrderConfirmationTopic
ShippingStartedRule:
EventPattern:
source: ["shipping.service"]
detail-type: ["Shipment Created"]
Targets:
- Lambda: CustomerNotificationFunction
- SQS: TrackingUpdateQueue
3. Step Functions 워크플로우 설계
A. 주문 처리 워크플로우
{
"Comment": "주문 처리 워크플로우",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ValidateOrderFunction",
"Payload.$": "$"
},
"Next": "CheckInventory",
"Retry": [
{
"ErrorEquals": ["States.TaskFailed"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "OrderValidationFailed"
}
]
},
"CheckInventory": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "CheckInventoryFunction",
"Payload.$": "$"
},
"Next": "InventoryAvailable?"
},
"InventoryAvailable?": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.inventoryStatus",
"StringEquals": "AVAILABLE",
"Next": "ProcessPayment"
},
{
"Variable": "$.inventoryStatus",
"StringEquals": "PARTIAL",
"Next": "HandlePartialInventory"
}
],
"Default": "InventoryNotAvailable"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ProcessPaymentFunction",
"Payload.$": "$"
},
"Next": "PaymentSuccessful?",
"Retry": [
{
"ErrorEquals": ["PaymentDeclined"],
"IntervalSeconds": 1,
"MaxAttempts": 1
}
]
},
"PaymentSuccessful?": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.paymentStatus",
"StringEquals": "SUCCESS",
"Next": "StartFulfillment"
}
],
"Default": "PaymentFailed"
},
"StartFulfillment": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "AllocateWarehouse",
"States": {
"AllocateWarehouse": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "AllocateWarehouseFunction",
"Payload.$": "$"
},
"End": true
}
}
},
{
"StartAt": "GenerateShippingLabel",
"States": {
"GenerateShippingLabel": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "GenerateShippingLabelFunction",
"Payload.$": "$"
},
"End": true
}
}
}
],
"Next": "NotifyCustomer"
},
"NotifyCustomer": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:region:account:customer-notifications",
"Message.$": "$.notificationMessage",
"MessageAttributes": {
"customerId": {
"DataType": "String",
"StringValue.$": "$.customerId"
}
}
},
"End": true
},
"HandlePartialInventory": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "HandlePartialInventoryFunction",
"Payload.$": "$"
},
"Next": "ProcessPayment"
},
"InventoryNotAvailable": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "HandleInventoryShortageFunction",
"Payload.$": "$"
},
"End": true
},
"PaymentFailed": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "HandlePaymentFailureFunction",
"Payload.$": "$"
},
"End": true
},
"OrderValidationFailed": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "HandleOrderValidationFailureFunction",
"Payload.$": "$"
},
"End": true
}
}
}
4. Lambda 함수 구현 예제
A. 주문 검증 함수
import json
import boto3
from decimal import Decimal
dynamodb = boto3.resource('dynamodb')
eventbridge = boto3.client('events')
def lambda_handler(event, context):
try:
# 주문 데이터 추출
order_data = event['detail'] if 'detail' in event else event
# 주문 검증 로직
validation_result = validate_order(order_data)
if validation_result['isValid']:
# DynamoDB에 주문 저장
save_order_to_db(order_data)
# 검증 성공 이벤트 발행
publish_event('order.validation', 'Order Validated', {
'orderId': order_data['orderId'],
'status': 'VALIDATED'
})
return {
'statusCode': 200,
'orderId': order_data['orderId'],
'status': 'VALIDATED',
'validationDetails': validation_result
}
else:
# 검증 실패 이벤트 발행
publish_event('order.validation', 'Order Validation Failed', {
'orderId': order_data['orderId'],
'status': 'VALIDATION_FAILED',
'errors': validation_result['errors']
})
raise Exception(f"Order validation failed: {validation_result['errors']}")
except Exception as e:
print(f"Error processing order: {str(e)}")
raise
def validate_order(order_data):
errors = []
# 필수 필드 검증
required_fields = ['orderId', 'customerId', 'items', 'totalAmount']
for field in required_fields:
if field not in order_data or not order_data[field]:
errors.append(f"Missing required field: {field}")
# 상품 검증
if 'items' in order_data:
for item in order_data['items']:
if item['quantity'] <= 0:
errors.append(f"Invalid quantity for product {item['productId']}")
if item['price'] <= 0:
errors.append(f"Invalid price for product {item['productId']}")
# 총액 검증
if 'items' in order_data and 'totalAmount' in order_data:
calculated_total = sum(item['quantity'] * item['price'] for item in order_data['items'])
if abs(calculated_total - order_data['totalAmount']) > 0.01:
errors.append("Total amount mismatch")
return {
'isValid': len(errors) == 0,
'errors': errors
}
def save_order_to_db(order_data):
table = dynamodb.Table('Orders')
# Decimal 변환
order_item = json.loads(json.dumps(order_data), parse_float=Decimal)
order_item['status'] = 'VALIDATED'
order_item['createdAt'] = order_data.get('timestamp')
table.put_item(Item=order_item)
def publish_event(source, detail_type, detail):
eventbridge.put_events(
Entries=[
{
'Source': source,
'DetailType': detail_type,
'Detail': json.dumps(detail)
}
]
)
B. 재고 확인 함수
import json
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
sqs = boto3.client('sqs')
def lambda_handler(event, context):
try:
order_data = event['detail'] if 'detail' in event else event
inventory_result = check_inventory_availability(order_data['items'])
# 재고 상태에 따른 처리
if inventory_result['status'] == 'AVAILABLE':
# 재고 예약
reserve_inventory(order_data['items'])
return {
'statusCode': 200,
'orderId': order_data['orderId'],
'inventoryStatus': 'AVAILABLE',
'reservedItems': inventory_result['availableItems']
}
elif inventory_result['status'] == 'PARTIAL':
# 부분 재고 처리를 위한 SQS 메시지 전송
send_partial_inventory_message(order_data, inventory_result)
return {
'statusCode': 200,
'orderId': order_data['orderId'],
'inventoryStatus': 'PARTIAL',
'availableItems': inventory_result['availableItems'],
'unavailableItems': inventory_result['unavailableItems']
}
else:
# 재고 부족 알림
send_inventory_shortage_notification(order_data, inventory_result)
return {
'statusCode': 400,
'orderId': order_data['orderId'],
'inventoryStatus': 'UNAVAILABLE',
'unavailableItems': inventory_result['unavailableItems']
}
except Exception as e:
print(f"Error checking inventory: {str(e)}")
raise
def check_inventory_availability(items):
table = dynamodb.Table('Inventory')
available_items = []
unavailable_items = []
for item in items:
response = table.get_item(
Key={'productId': item['productId']}
)
if 'Item' in response:
inventory_item = response['Item']
available_quantity = inventory_item['availableQuantity']
if available_quantity >= item['quantity']:
available_items.append(item)
else:
unavailable_items.append({
**item,
'availableQuantity': available_quantity,
'shortfall': item['quantity'] - available_quantity
})
else:
unavailable_items.append({
**item,
'availableQuantity': 0,
'shortfall': item['quantity']
})
if len(unavailable_items) == 0:
status = 'AVAILABLE'
elif len(available_items) > 0:
status = 'PARTIAL'
else:
status = 'UNAVAILABLE'
return {
'status': status,
'availableItems': available_items,
'unavailableItems': unavailable_items
}
def reserve_inventory(items):
table = dynamodb.Table('Inventory')
for item in items:
table.update_item(
Key={'productId': item['productId']},
UpdateExpression='SET reservedQuantity = reservedQuantity + :qty, availableQuantity = availableQuantity - :qty',
ExpressionAttributeValues={':qty': item['quantity']}
)
def send_partial_inventory_message(order_data, inventory_result):
queue_url = 'https://sqs.region.amazonaws.com/account/partial-inventory-queue'
message = {
'orderId': order_data['orderId'],
'customerId': order_data['customerId'],
'availableItems': inventory_result['availableItems'],
'unavailableItems': inventory_result['unavailableItems']
}
sqs.send_message(
QueueUrl=queue_url,
MessageBody=json.dumps(message)
)
5. 메시징 및 알림 시스템
A. SQS 큐 구성
SQS Queues:
InventoryCheckQueue:
VisibilityTimeout: 300
MessageRetentionPeriod: 1209600 # 14 days
DelaySeconds: 0
ReceiveMessageWaitTimeSeconds: 20 # Long polling
PartialInventoryQueue:
VisibilityTimeout: 600
MessageRetentionPeriod: 1209600
ReddrivePolicy:
deadLetterTargetArn: "arn:aws:sqs:region:account:partial-inventory-dlq"
maxReceiveCount: 3
PaymentProcessingQueue:
VisibilityTimeout: 180
MessageRetentionPeriod: 345600 # 4 days
ShippingQueue:
VisibilityTimeout: 900
MessageRetentionPeriod: 1209600
B. SNS 토픽 구성
SNS Topics:
CustomerNotificationTopic:
Subscriptions:
- Protocol: email
Endpoint: notifications@company.com
- Protocol: sqs
Endpoint: arn:aws:sqs:region:account:email-queue
- Protocol: sqs
Endpoint: arn:aws:sqs:region:account:sms-queue
OrderStatusTopic:
Subscriptions:
- Protocol: lambda
Endpoint: arn:aws:lambda:region:account:function:UpdateOrderStatus
- Protocol: sqs
Endpoint: arn:aws:sqs:region:account:analytics-queue
InventoryAlertTopic:
Subscriptions:
- Protocol: email
Endpoint: inventory@company.com
- Protocol: lambda
Endpoint: arn:aws:lambda:region:account:function:AutoReorderFunction
6. 오류 처리 및 복구 메커니즘
A. Dead Letter Queue 설정
# DLQ 메시지 처리 Lambda 함수
import json
import boto3
cloudwatch = boto3.client('cloudwatch')
sns = boto3.client('sns')
def lambda_handler(event, context):
for record in event['Records']:
try:
# DLQ에서 받은 메시지 분석
message_body = json.loads(record['body'])
# 실패 유형 분석
failure_type = analyze_failure(message_body)
# CloudWatch 메트릭 전송
send_failure_metric(failure_type)
# 운영팀 알림
if failure_type == 'CRITICAL':
send_alert_notification(message_body)
# 재처리 가능한 경우 재시도 큐로 전송
if is_retryable(failure_type):
schedule_retry(message_body)
except Exception as e:
print(f"Error processing DLQ message: {str(e)}")
def analyze_failure(message):
# 실패 패턴 분석 로직
error_patterns = {
'PaymentTimeout': 'RETRYABLE',
'InventoryServiceDown': 'RETRYABLE',
'InvalidOrderData': 'PERMANENT',
'CustomerNotFound': 'PERMANENT'
}
return error_patterns.get(message.get('errorType'), 'UNKNOWN')
B. 회로 차단기 패턴
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = 1
OPEN = 2
HALF_OPEN = 3
class CircuitBreaker:
def __init__(self, failure_threshold=5, timeout=60):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failure_count = 0
self.last_failure_time = None
self.state = CircuitState.CLOSED
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.timeout:
self.state = CircuitState.HALF_OPEN
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func(*args, **kwargs)
self.on_success()
return result
except Exception as e:
self.on_failure()
raise e
def on_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
# 사용 예제
payment_circuit_breaker = CircuitBreaker(failure_threshold=3, timeout=30)
def process_payment_with_circuit_breaker(payment_data):
return payment_circuit_breaker.call(process_payment, payment_data)
7. 모니터링 및 관찰성
A. CloudWatch 대시보드
Dashboard Widgets:
WorkflowMetrics:
- Step Functions Executions (Success/Failed)
- Average Execution Duration
- State Transition Counts
EventMetrics:
- EventBridge Rule Invocations
- Failed Events
- Event Processing Latency
LambdaMetrics:
- Function Duration
- Error Rate
- Throttles
- Concurrent Executions
QueueMetrics:
- SQS Message Counts
- DLQ Message Counts
- Processing Time
BusinessMetrics:
- Orders per Hour
- Order Success Rate
- Average Order Processing Time
- Revenue per Hour
B. X-Ray 트레이싱
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all
# AWS 서비스 자동 트레이싱
patch_all()
@xray_recorder.capture('order_validation')
def validate_order(order_data):
# 커스텀 메타데이터 추가
xray_recorder.current_segment().put_metadata('order_info', {
'orderId': order_data['orderId'],
'itemCount': len(order_data['items']),
'totalAmount': order_data['totalAmount']
})
# 검증 로직
return perform_validation(order_data)
@xray_recorder.capture('inventory_check')
def check_inventory(items):
subsegment = xray_recorder.current_subsegment()
subsegment.put_annotation('item_count', len(items))
# 재고 확인 로직
return perform_inventory_check(items)
8. 성능 최적화
A. Lambda 최적화
# 연결 풀링을 위한 전역 변수
import boto3
# 함수 외부에서 클라이언트 초기화 (재사용)
dynamodb = boto3.resource('dynamodb')
eventbridge = boto3.client('events')
# Provisioned Concurrency 설정
PROVISIONED_CONCURRENCY = {
'ValidateOrderFunction': 10,
'CheckInventoryFunction': 15,
'ProcessPaymentFunction': 20
}
# 메모리 최적화
MEMORY_CONFIGURATIONS = {
'ValidateOrderFunction': 512, # CPU 집약적
'CheckInventoryFunction': 1024, # 메모리 집약적
'ProcessPaymentFunction': 256 # 간단한 처리
}
B. EventBridge 최적화
EventBridge Optimization:
BatchSettings:
MaximumBatchingWindowInSeconds: 5
MaximumRecordAgeInSeconds: 300
ArchiveSettings:
ArchiveName: order-events-archive
RetentionDays: 365
ReplaySettings:
ReplayName: failed-orders-replay
EventSourceArn: arn:aws:events:region:account:event-bus/default
💡 고급 패턴 및 모범 사례
1. Saga 패턴 구현
{
"Comment": "Saga 패턴을 활용한 분산 트랜잭션",
"StartAt": "BeginTransaction",
"States": {
"BeginTransaction": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "ReserveInventory",
"States": {
"ReserveInventory": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ReserveInventoryFunction"
},
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "CompensateInventory"
}
],
"Next": "InventoryReserved"
},
"InventoryReserved": {
"Type": "Succeed"
},
"CompensateInventory": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ReleaseInventoryFunction"
},
"End": true
}
}
},
{
"StartAt": "ProcessPayment",
"States": {
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ProcessPaymentFunction"
},
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "RefundPayment"
}
],
"Next": "PaymentProcessed"
},
"PaymentProcessed": {
"Type": "Succeed"
},
"RefundPayment": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "RefundPaymentFunction"
},
"End": true
}
}
}
],
"Next": "CommitTransaction",
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "RollbackTransaction"
}
]
},
"CommitTransaction": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "CommitTransactionFunction"
},
"End": true
},
"RollbackTransaction": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "RollbackTransactionFunction"
},
"End": true
}
}
}
2. CQRS 패턴 적용
# Command 모델
class CreateOrderCommand:
def __init__(self, customer_id, items, shipping_address):
self.customer_id = customer_id
self.items = items
self.shipping_address = shipping_address
self.timestamp = datetime.utcnow()
# Event 모델
class OrderCreatedEvent:
def __init__(self, order_id, customer_id, items, total_amount):
self.order_id = order_id
self.customer_id = customer_id
self.items = items
self.total_amount = total_amount
self.timestamp = datetime.utcnow()
# Command Handler
def handle_create_order_command(command):
# 비즈니스 로직 실행
order = create_order(command)
# 이벤트 발행
event = OrderCreatedEvent(
order.id,
command.customer_id,
command.items,
order.total_amount
)
publish_event(event)
return order
# Query 모델 (읽기 전용)
def get_order_status(order_id):
table = dynamodb.Table('OrderStatusView')
response = table.get_item(Key={'orderId': order_id})
return response.get('Item')
📊 비용 분석 및 최적화
1. 서비스별 비용 예상
월간 예상 비용 (10,000 주문 기준):
Step Functions:
- State Transitions: $25 (25,000 transitions)
- Express Workflows: $10 (추가 워크플로우)
EventBridge:
- Custom Events: $1 (1M events)
- Cross-region Replication: $5
Lambda:
- Invocations: $20 (10M invocations)
- Duration: $30 (GB-seconds)
SQS/SNS:
- SQS Messages: $5 (1M messages)
- SNS Messages: $2 (200K messages)
DynamoDB:
- Read/Write Units: $50
- Storage: $10
CloudWatch/X-Ray:
- Logs: $15
- Metrics: $10
- Tracing: $5
총 예상 비용: $188/월 (기존 시스템 대비 40% 절감)
2. 비용 최적화 전략
Optimization Strategies:
Lambda:
- ARM-based Graviton2 프로세서 사용 (20% 비용 절감)
- 적절한 메모리 크기 설정
- Provisioned Concurrency 최적화
Step Functions:
- Express Workflows 활용 (짧은 워크플로우)
- 불필요한 state transition 최소화
DynamoDB:
- On-Demand vs Provisioned 모드 선택
- DynamoDB Accelerator (DAX) 적용
- Auto Scaling 설정
🚀 실습 과제
1. 기본 구현 (1-2주)
- EventBridge 이벤트 버스 생성 및 기본 라우팅 규칙 설정
- Step Functions 워크플로우 구현 (주문 처리 플로우)
- Lambda 함수 개발 (주문 검증, 재고 확인)
- DynamoDB 테이블 설계 및 생성
2. 고급 기능 구현 (2-3주)
- 오류 처리 및 재시도 메커니즘 구현
- Dead Letter Queue 처리 로직 개발
- 회로 차단기 패턴 적용
- Saga 패턴을 활용한 분산 트랜잭션 처리
3. 모니터링 및 최적화 (1-2주)
- CloudWatch 대시보드 구성
- X-Ray 트레이싱 설정 및 분석
- 성능 테스트 수행 및 최적화
- 비용 분석 및 최적화 방안 도출
4. 고도화 작업 (2-3주)
- CQRS 패턴 적용
- 이벤트 소싱 구현
- Multi-region 배포 및 재해 복구
- 보안 강화 (IAM, KMS, VPC)
이 아키텍처는 현대적인 이벤트 기반 시스템의 모범 사례를 보여주며, AWS SAA 시험에서 중요하게 다뤄지는 분산 시스템 설계, 오류 처리, 모니터링 등의 핵심 개념들을 실제 비즈니스 시나리오와 연결하여 제시합니다.
관련 포스트: