Skip to content
Published on

Serverless 아키텍처 패턴 완전 가이드 2025: Lambda, Step Functions, 이벤트 소싱, 비용 최적화

Authors

목차

1. Serverless란 무엇인가

Serverless는 서버를 직접 관리하지 않고 클라우드 제공자가 인프라를 완전히 추상화하는 컴퓨팅 모델이다. 개발자는 비즈니스 로직에만 집중하고, 프로비저닝/스케일링/패칭은 클라우드가 담당한다.

1.1 Serverless의 4대 원칙

원칙설명예시
서버 관리 불필요OS, 패치, 스케일링 신경 안 씀Lambda, Cloud Functions
자동 스케일링트래픽에 따라 0에서 수천 인스턴스까지초당 0건에서 10만건까지
사용한 만큼 과금유휴 시간 비용 없음100ms 단위 과금
이벤트 기반요청/이벤트가 함수를 트리거HTTP, S3, SQS, 스케줄

1.2 Serverless 컴퓨팅의 역사

2014: AWS Lambda 출시 (최초의 FaaS)
2016: Azure Functions, Google Cloud Functions
2017: AWS Step Functions, SAM 출시
2018: Lambda Layers, ALB 지원
2019: Provisioned Concurrency, RDS Proxy
2020: Lambda Container Image, EventBridge
2021: Lambda URL, Graviton2 지원
2022: Lambda SnapStart (Java), 스트리밍 응답
2023: Lambda Advanced Logging, Step Functions 개선
2024: Lambda 성능 최적화, ARM64 전면 지원
2025: Lambda 최대 메모리 10GB, Step Functions Distributed Map 강화

1.3 주요 클라우드의 Serverless 서비스

카테고리AWSAzureGCP
FaaSLambdaFunctionsCloud Functions
워크플로Step FunctionsDurable FunctionsWorkflows
APIAPI GatewayAPI ManagementAPI Gateway
메시징SQS/SNSService BusPub/Sub
스트리밍KinesisEvent HubsDataflow
DBDynamoDBCosmos DBFirestore
스토리지S3Blob StorageCloud Storage
이벤트 버스EventBridgeEvent GridEventarc

2. Lambda 설계 패턴

Lambda 함수를 어떻게 구조화하느냐에 따라 유지보수성, 성능, 비용이 크게 달라진다.

2.1 단일 목적 함수 (Single Purpose Function)

하나의 Lambda가 하나의 작업만 수행한다. 가장 권장되는 패턴이다.

# order_create.py - 주문 생성만 담당
import json
import boto3
import os
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['ORDERS_TABLE'])
sns = boto3.client('sns')

def handler(event, context):
    body = json.loads(event['body'])

    order = {
        'orderId': context.aws_request_id,
        'userId': body['userId'],
        'items': body['items'],
        'total': calculate_total(body['items']),
        'status': 'CREATED',
        'createdAt': datetime.utcnow().isoformat()
    }

    table.put_item(Item=order)

    # 이벤트 발행
    sns.publish(
        TopicArn=os.environ['ORDER_TOPIC'],
        Message=json.dumps(order),
        MessageAttributes={
            'eventType': {
                'DataType': 'String',
                'StringValue': 'OrderCreated'
            }
        }
    )

    return {
        'statusCode': 201,
        'body': json.dumps(order)
    }

def calculate_total(items):
    return sum(item['price'] * item['quantity'] for item in items)

장점:

  • 함수 크기가 작아 콜드 스타트가 빠름
  • 독립 배포 가능
  • IAM 권한을 최소화할 수 있음
  • 디버깅이 쉬움

단점:

  • 함수 수가 많아질 수 있음
  • 공통 코드 관리를 위해 Layer 필요

2.2 모놀리식 Lambda (Lambda-lith)

하나의 Lambda가 여러 라우트를 처리한다. Express나 FastAPI 같은 프레임워크를 사용한다.

// app.ts - 모놀리식 Lambda
import express from 'express';
import serverless from 'serverless-http';

const app = express();
app.use(express.json());

// 여러 라우트를 하나의 Lambda에서 처리
app.get('/orders', async (req, res) => {
  const orders = await getOrders(req.query);
  res.json(orders);
});

app.post('/orders', async (req, res) => {
  const order = await createOrder(req.body);
  res.status(201).json(order);
});

app.get('/orders/:id', async (req, res) => {
  const order = await getOrder(req.params.id);
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order);
});

app.put('/orders/:id/status', async (req, res) => {
  const order = await updateOrderStatus(req.params.id, req.body.status);
  res.json(order);
});

app.delete('/orders/:id', async (req, res) => {
  await cancelOrder(req.params.id);
  res.status(204).send();
});

export const handler = serverless(app);

장점:

  • 기존 웹 프레임워크 코드를 그대로 마이그레이션 가능
  • 함수 수 관리가 단순
  • 로컬 개발이 편리

단점:

  • 패키지 크기가 커서 콜드 스타트 느림
  • IAM 권한이 과도하게 넓어짐
  • 하나의 라우트 문제가 전체에 영향

2.3 Fan-out / Fan-in 패턴

하나의 이벤트가 여러 Lambda를 동시에 트리거하고, 결과를 집계하는 패턴이다.

# serverless.yml - Fan-out 아키텍처
service: order-processing

provider:
  name: aws
  runtime: nodejs20.x

functions:
  orderReceiver:
    handler: src/receiver.handler
    events:
      - http:
          path: /orders
          method: post
    environment:
      FAN_OUT_TOPIC: !Ref OrderFanOutTopic

  inventoryCheck:
    handler: src/inventory.handler
    events:
      - sns:
          arn: !Ref OrderFanOutTopic
          filterPolicy:
            eventType:
              - OrderCreated

  paymentProcess:
    handler: src/payment.handler
    events:
      - sns:
          arn: !Ref OrderFanOutTopic
          filterPolicy:
            eventType:
              - OrderCreated

  notificationSend:
    handler: src/notification.handler
    events:
      - sns:
          arn: !Ref OrderFanOutTopic
          filterPolicy:
            eventType:
              - OrderCreated

resources:
  Resources:
    OrderFanOutTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: order-fan-out

2.4 Lambda 설계 패턴 비교

패턴함수 수콜드 스타트배포 단위권장 상황
단일 목적많음빠름개별마이크로서비스
Lambda-lith적음느림전체마이그레이션
Fan-out중간빠름개별병렬 처리
Lambda Layer중간보통레이어+함수공통 코드 공유

3. Step Functions: 워크플로 오케스트레이션

Step Functions는 AWS의 서버리스 워크플로 서비스로, 복잡한 비즈니스 로직을 상태 머신으로 시각적으로 정의한다.

3.1 Standard vs Express Workflow

특성StandardExpress
최대 실행 시간1년5분
실행 보장Exactly-onceAt-least-once
가격상태 전이당 과금실행 시간/메모리 과금
실행 이력90일 보관CloudWatch Logs
최대 처리량초당 2,000 전이초당 100,000+
용도장기 실행 워크플로대량/빠른 처리

3.2 상태 타입

{
  "Comment": "주문 처리 워크플로",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:validate-order",
      "Next": "CheckInventory",
      "Retry": [
        {
          "ErrorEquals": ["ServiceException"],
          "IntervalSeconds": 2,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ],
      "Catch": [
        {
          "ErrorEquals": ["ValidationError"],
          "Next": "OrderFailed"
        }
      ]
    },
    "CheckInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:check-inventory",
      "Next": "ProcessPaymentOrWait"
    },
    "ProcessPaymentOrWait": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.inventoryAvailable",
          "BooleanEquals": true,
          "Next": "ProcessPayment"
        }
      ],
      "Default": "WaitForInventory"
    },
    "WaitForInventory": {
      "Type": "Wait",
      "Seconds": 300,
      "Next": "CheckInventory"
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:process-payment",
      "Next": "ParallelFulfillment"
    },
    "ParallelFulfillment": {
      "Type": "Parallel",
      "Branches": [
        {
          "StartAt": "UpdateDatabase",
          "States": {
            "UpdateDatabase": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:update-db",
              "End": true
            }
          }
        },
        {
          "StartAt": "SendNotification",
          "States": {
            "SendNotification": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:send-notification",
              "End": true
            }
          }
        },
        {
          "StartAt": "InitiateShipping",
          "States": {
            "InitiateShipping": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:initiate-shipping",
              "End": true
            }
          }
        }
      ],
      "Next": "OrderCompleted"
    },
    "OrderCompleted": {
      "Type": "Succeed"
    },
    "OrderFailed": {
      "Type": "Fail",
      "Error": "OrderProcessingFailed",
      "Cause": "Order validation or processing failed"
    }
  }
}

3.3 Step Functions 상태 타입 요약

상태 타입용도설명
Task작업 실행Lambda, DynamoDB, SQS 등 호출
Choice조건 분기if/else 로직
Parallel병렬 실행여러 브랜치 동시 실행
Map반복 처리배열의 각 요소를 처리
Wait대기지정 시간 또는 타임스탬프까지 대기
Pass데이터 변환입력 변환 후 전달
Succeed성공 종료워크플로 성공 완료
Fail실패 종료워크플로 실패

3.4 Callback 패턴 (사람 승인 워크플로)

Step Functions는 외부 시스템의 응답을 기다리는 콜백 패턴을 지원한다.

# callback_handler.py - 사람 승인을 기다리는 Lambda
import json
import boto3

sfn = boto3.client('stepfunctions')
ses = boto3.client('ses')

def request_approval(event, context):
    """Step Functions가 태스크 토큰과 함께 호출"""
    task_token = event['taskToken']
    order = event['order']

    # 승인 링크가 포함된 이메일 발송
    approval_url = f"https://api.example.com/approve?token={task_token}"
    reject_url = f"https://api.example.com/reject?token={task_token}"

    ses.send_email(
        Source='noreply@example.com',
        Destination={'ToAddresses': ['manager@example.com']},
        Message={
            'Subject': {'Data': f"주문 승인 요청: {order['orderId']}"},
            'Body': {
                'Html': {
                    'Data': f"""
                    <h2>주문 승인 요청</h2>
                    <p>주문 ID: {order['orderId']}</p>
                    <p>금액: {order['total']}원</p>
                    <a href="{approval_url}">승인</a> |
                    <a href="{reject_url}">거절</a>
                    """
                }
            }
        }
    )

def handle_approval(event, context):
    """승인/거절 콜백 처리"""
    params = event['queryStringParameters']
    task_token = params['token']

    if 'approve' in event['path']:
        sfn.send_task_success(
            taskToken=task_token,
            output=json.dumps({'approved': True})
        )
    else:
        sfn.send_task_failure(
            taskToken=task_token,
            error='Rejected',
            cause='Manager rejected the order'
        )

    return {
        'statusCode': 200,
        'body': json.dumps({'message': '처리 완료'})
    }

4. 이벤트 기반 아키텍처 패턴

4.1 이벤트 소싱 (Event Sourcing) with Lambda

# event_store.py
import json
import boto3
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
event_store = dynamodb.Table('EventStore')
sns = boto3.client('sns')

def append_event(aggregate_id, event_type, data, version):
    """이벤트를 저장하고 발행"""
    event = {
        'aggregateId': aggregate_id,
        'version': version,
        'eventType': event_type,
        'data': data,
        'timestamp': datetime.utcnow().isoformat(),
        'metadata': {
            'correlationId': data.get('correlationId', ''),
            'causationId': data.get('causationId', '')
        }
    }

    # 낙관적 잠금: version이 이미 존재하면 실패
    event_store.put_item(
        Item=event,
        ConditionExpression='attribute_not_exists(version)'
    )

    # 이벤트 발행
    sns.publish(
        TopicArn='arn:aws:sns:ap-northeast-2:123456789:domain-events',
        Message=json.dumps(event),
        MessageAttributes={
            'eventType': {
                'DataType': 'String',
                'StringValue': event_type
            }
        }
    )

    return event

def replay_events(aggregate_id):
    """특정 Aggregate의 모든 이벤트를 재생"""
    response = event_store.query(
        KeyConditionExpression='aggregateId = :aid',
        ExpressionAttributeValues={':aid': aggregate_id},
        ScanIndexForward=True  # 시간순 정렬
    )
    return response['Items']

4.2 Saga 패턴 with Step Functions

분산 트랜잭션을 관리하는 Saga 패턴을 Step Functions로 구현한다.

{
  "Comment": "주문 Saga - 보상 트랜잭션 포함",
  "StartAt": "ReserveInventory",
  "States": {
    "ReserveInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:reserve-inventory",
      "Next": "ProcessPayment",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "Next": "InventoryReservationFailed"
      }]
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:process-payment",
      "Next": "ConfirmOrder",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "Next": "RollbackInventory"
      }]
    },
    "ConfirmOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:confirm-order",
      "Next": "SagaCompleted",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "Next": "RollbackPayment"
      }]
    },
    "RollbackPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:rollback-payment",
      "Next": "RollbackInventory"
    },
    "RollbackInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-2:123456789:function:rollback-inventory",
      "Next": "SagaFailed"
    },
    "InventoryReservationFailed": {
      "Type": "Fail",
      "Error": "InventoryReservationFailed",
      "Cause": "Could not reserve inventory"
    },
    "SagaCompleted": {
      "Type": "Succeed"
    },
    "SagaFailed": {
      "Type": "Fail",
      "Error": "SagaFailed",
      "Cause": "Order saga failed, all compensations executed"
    }
  }
}

4.3 Choreography vs Orchestration

특성Choreography (이벤트)Orchestration (Step Functions)
결합도느슨중앙 집중
가시성분산 추적 필요상태 머신에서 확인
복잡도이벤트 흐름 파악 어려움워크플로 정의 명확
에러 처리각 서비스가 독립 처리중앙에서 재시도/보상
적합한 경우단순한 이벤트 흐름복잡한 비즈니스 로직

5. Cold Start 심층 분석

Cold Start는 서버리스의 가장 큰 기술적 과제 중 하나다. Lambda 함수가 새 실행 환경에서 시작될 때 발생하는 지연이다.

5.1 Cold Start 발생 원인

요청 도착
  |
  v
[실행 환경 있음?] --No--> [Cold Start 경로]
  |                           |
  Yes                    1. 실행 환경 프로비저닝
  |                    2. 코드 다운로드 (S3)
  v                    3. 런타임 초기화
[Warm Start]           4. 핸들러 외부 코드 실행
  |                    5. 핸들러 실행
  v                         |
[핸들러 실행]               v
  |                    [응답 반환]
  v
[응답 반환]

5.2 런타임별 Cold Start 시간 비교

런타임평균 Cold StartP99 Cold Start패키지 크기 영향
Python 3.12150-300ms500-800ms낮음
Node.js 20150-350ms500-900ms중간
Go (provided.al2023)50-100ms150-300ms매우 낮음
Rust (provided.al2023)30-80ms100-250ms매우 낮음
Java 21800-3000ms3000-8000ms높음
Java 21 (SnapStart)100-200ms300-500ms중간
.NET 8 (AOT)200-400ms600-1000ms중간

5.3 Cold Start 최적화 전략

# 최적화된 Lambda 함수 구조
import json
import os

# 핸들러 외부에서 초기화 (재사용됨)
# 1. 연결은 전역으로 초기화
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])

# 2. 불필요한 import 제거
# BAD: import pandas  (패키지 크기 증가)
# GOOD: 필요한 것만 import

# 3. SDK 설정 최적화
from botocore.config import Config
config = Config(
    connect_timeout=5,
    read_timeout=5,
    retries={'max_attempts': 2}
)
s3 = boto3.client('s3', config=config)

def handler(event, context):
    """핸들러는 가능한 가볍게"""
    order_id = event['pathParameters']['orderId']

    response = table.get_item(Key={'orderId': order_id})
    item = response.get('Item')

    if not item:
        return {'statusCode': 404, 'body': json.dumps({'error': 'Not found'})}

    return {'statusCode': 200, 'body': json.dumps(item)}

5.4 Provisioned Concurrency

# SAM template - Provisioned Concurrency 설정
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.handler
      Runtime: python3.12
      MemorySize: 512
      AutoPublishAlias: live
      ProvisionedConcurrencyConfig:
        ProvisionedConcurrentExecutions: 10

  # 시간대별 자동 스케일링
  ScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MaxCapacity: 100
      MinCapacity: 5
      ResourceId: !Sub function:${MyFunction}:live
      ScalableDimension: lambda:function:ProvisionedConcurrency
      ServiceNamespace: lambda

  ScalingPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: UtilizationScaling
      PolicyType: TargetTrackingScaling
      ScalableTargetId: !Ref ScalingTarget
      TargetTrackingScalingPolicyConfiguration:
        TargetValue: 0.7
        PredefinedMetricSpecification:
          PredefinedMetricType: LambdaProvisionedConcurrencyUtilization

5.5 Java SnapStart

// SnapStart 최적화된 Java Lambda
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import org.crac.Core;
import org.crac.Resource;

public class OrderHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>,
                                     Resource {

    private final DynamoDbClient dynamoDb;
    private final ObjectMapper objectMapper;

    public OrderHandler() {
        // SnapStart: 이 초기화 코드는 스냅샷에 포함됨
        this.dynamoDb = DynamoDbClient.create();
        this.objectMapper = new ObjectMapper();
        Core.getGlobalContext().register(this);
    }

    @Override
    public void beforeCheckpoint(org.crac.Context<? extends Resource> context) {
        // 스냅샷 전: 연결 정리
    }

    @Override
    public void afterRestore(org.crac.Context<? extends Resource> context) {
        // 복원 후: 연결 재설정
        // 고유성 보장 (난수 시드 재설정 등)
    }

    @Override
    public APIGatewayProxyResponseEvent handleRequest(
            APIGatewayProxyRequestEvent event, Context context) {
        // 비즈니스 로직
        return new APIGatewayProxyResponseEvent()
            .withStatusCode(200)
            .withBody("{\"message\": \"OK\"}");
    }
}

6. API 패턴

6.1 REST API with API Gateway

# SAM template - REST API
Resources:
  OrdersApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: prod
      Auth:
        DefaultAuthorizer: CognitoAuthorizer
        Authorizers:
          CognitoAuthorizer:
            UserPoolArn: !GetAtt UserPool.Arn
      # 스로틀링
      MethodSettings:
        - HttpMethod: '*'
          ResourcePath: '/*'
          ThrottlingBurstLimit: 100
          ThrottlingRateLimit: 50
      # CORS
      Cors:
        AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization'"
        AllowOrigin: "'https://example.com'"

  GetOrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/orders/get.handler
      Runtime: nodejs20.x
      Events:
        GetOrder:
          Type: Api
          Properties:
            RestApiId: !Ref OrdersApi
            Path: /orders/{orderId}
            Method: get

  CreateOrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/orders/create.handler
      Runtime: nodejs20.x
      Events:
        CreateOrder:
          Type: Api
          Properties:
            RestApiId: !Ref OrdersApi
            Path: /orders
            Method: post

6.2 GraphQL with AppSync

# AppSync 스키마
type Order {
  orderId: ID!
  userId: String!
  items: [OrderItem!]!
  total: Float!
  status: OrderStatus!
  createdAt: AWSDateTime!
}

type OrderItem {
  productId: String!
  name: String!
  quantity: Int!
  price: Float!
}

enum OrderStatus {
  CREATED
  PAID
  SHIPPED
  DELIVERED
  CANCELLED
}

type Query {
  getOrder(orderId: ID!): Order
  listOrders(userId: String!, limit: Int, nextToken: String): OrderConnection!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  updateOrderStatus(orderId: ID!, status: OrderStatus!): Order!
}

type Subscription {
  onOrderStatusChanged(orderId: ID!): Order
    @aws_subscribe(mutations: ["updateOrderStatus"])
}

6.3 WebSocket with API Gateway

# websocket_handler.py
import json
import boto3
import os

dynamodb = boto3.resource('dynamodb')
connections_table = dynamodb.Table(os.environ['CONNECTIONS_TABLE'])

def connect(event, context):
    """WebSocket 연결"""
    connection_id = event['requestContext']['connectionId']
    user_id = event['requestContext']['authorizer']['userId']

    connections_table.put_item(Item={
        'connectionId': connection_id,
        'userId': user_id
    })

    return {'statusCode': 200}

def disconnect(event, context):
    """WebSocket 연결 해제"""
    connection_id = event['requestContext']['connectionId']
    connections_table.delete_item(Key={'connectionId': connection_id})
    return {'statusCode': 200}

def send_message(event, context):
    """메시지 전송"""
    domain = event['requestContext']['domainName']
    stage = event['requestContext']['stage']
    body = json.loads(event['body'])

    apigw = boto3.client(
        'apigatewaymanagementapi',
        endpoint_url=f'https://{domain}/{stage}'
    )

    # 모든 연결에 브로드캐스트
    connections = connections_table.scan()['Items']
    for conn in connections:
        try:
            apigw.post_to_connection(
                ConnectionId=conn['connectionId'],
                Data=json.dumps(body['message']).encode()
            )
        except apigw.exceptions.GoneException:
            connections_table.delete_item(
                Key={'connectionId': conn['connectionId']}
            )

    return {'statusCode': 200}

7. 데이터 패턴

7.1 DynamoDB 단일 테이블 설계

# DynamoDB Single Table Design
# PK/SK 패턴으로 여러 엔티티를 하나의 테이블에 저장

ENTITY_PATTERNS = {
    'User': {
        'PK': 'USER#user_id',
        'SK': 'PROFILE'
    },
    'Order': {
        'PK': 'USER#user_id',
        'SK': 'ORDER#order_id'
    },
    'OrderItem': {
        'PK': 'ORDER#order_id',
        'SK': 'ITEM#item_id'
    },
    'Product': {
        'PK': 'PRODUCT#product_id',
        'SK': 'DETAIL'
    }
}

# 접근 패턴별 쿼리
def get_user_with_orders(user_id):
    """사용자와 주문 목록을 한 번에 조회"""
    response = table.query(
        KeyConditionExpression='PK = :pk',
        ExpressionAttributeValues={':pk': f'USER#{user_id}'}
    )
    user = None
    orders = []
    for item in response['Items']:
        if item['SK'] == 'PROFILE':
            user = item
        elif item['SK'].startswith('ORDER#'):
            orders.append(item)
    return {'user': user, 'orders': orders}

def get_order_details(order_id):
    """주문 상세와 아이템을 한 번에 조회"""
    response = table.query(
        KeyConditionExpression='PK = :pk',
        ExpressionAttributeValues={':pk': f'ORDER#{order_id}'}
    )
    return response['Items']

7.2 Aurora Serverless v2

# Aurora Serverless v2 + Lambda
Resources:
  AuroraCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      Engine: aurora-postgresql
      EngineVersion: '15.4'
      ServerlessV2ScalingConfiguration:
        MinCapacity: 0.5
        MaxCapacity: 16
      EnableHttpEndpoint: true  # Data API 사용

  AuroraInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBClusterIdentifier: !Ref AuroraCluster
      DBInstanceClass: db.serverless
      Engine: aurora-postgresql

  # RDS Proxy로 연결 관리
  RDSProxy:
    Type: AWS::RDS::DBProxy
    Properties:
      DBProxyName: orders-proxy
      EngineFamily: POSTGRESQL
      Auth:
        - AuthScheme: SECRETS
          SecretArn: !Ref DBSecret
          IAMAuth: REQUIRED
      VpcSubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2

7.3 S3 이벤트 처리 파이프라인

# S3 이벤트 -> Lambda -> DynamoDB 파이프라인
import json
import boto3
import csv
import io

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ProcessedData')

def process_csv_upload(event, context):
    """S3에 업로드된 CSV를 처리"""
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']

    # S3에서 파일 읽기
    response = s3.get_object(Bucket=bucket, Key=key)
    content = response['Body'].read().decode('utf-8')

    # CSV 파싱 및 배치 쓰기
    reader = csv.DictReader(io.StringIO(content))

    with table.batch_writer() as batch:
        for row in reader:
            batch.put_item(Item={
                'id': row['id'],
                'data': row,
                'sourceFile': key,
                'processedAt': context.get_remaining_time_in_millis()
            })

    return {
        'statusCode': 200,
        'processedFile': key
    }

8. 메시징 서비스 선택 가이드

8.1 SQS vs SNS vs EventBridge vs Kinesis

특성SQSSNSEventBridgeKinesis
패턴큐 (1:1)Pub/Sub (1:N)이벤트 버스 (N:N)스트리밍
순서 보장FIFO만FIFO만없음파티션 내
최대 메시지256KB256KB256KB1MB
재처리DLQDLQ아카이브/재생보존 기간
필터링없음메시지 속성이벤트 패턴없음
지연시간msmsmsms
처리량무제한무제한초당 수천샤드당 1MB/s
비용요청당발행당이벤트당샤드 시간당

8.2 의사결정 트리

메시징 선택 흐름:
1. 실시간 스트리밍이 필요한가?
   -> Yes: Kinesis Data Streams
   -> No: 다음으로

2. 여러 소비자에게 동시 전달?
   -> Yes: 다음으로
   -> No: SQS (단순 큐)

3. 복잡한 이벤트 라우팅/필터링?
   -> Yes: EventBridge
   -> No: SNS

4. 이벤트 재생이 필요한가?
   -> Yes: EventBridge (아카이브) 또는 Kinesis (보존)
   -> No: SNS/SQS

8.3 EventBridge 패턴 매칭

{
  "source": ["com.myapp.orders"],
  "detail-type": ["OrderCreated"],
  "detail": {
    "total": [{"numeric": [">=", 10000]}],
    "status": ["CREATED"],
    "items": {
      "category": ["electronics", "premium"]
    }
  }
}

9. Serverless 컨테이너

9.1 Lambda vs Fargate vs Cloud Run

특성LambdaFargateCloud Run
최대 실행 시간15분무제한60분
최대 메모리10GB120GB32GB
vCPU최대 6최대 16최대 8
스케일 투 제로OX (최소 1 태스크)O
콜드 스타트있음없음 (상시 실행)있음
가격 모델실행시간+메모리vCPU+메모리 시간실행시간+메모리
컨테이너 이미지10GB까지무제한32GB까지

9.2 Lambda Container Image

# Dockerfile - Lambda 컨테이너 이미지
FROM public.ecr.aws/lambda/python:3.12

# 의존성 설치
COPY requirements.txt .
RUN pip install -r requirements.txt

# 애플리케이션 코드
COPY app/ ./app/

# Lambda 핸들러 지정
CMD ["app.main.handler"]
# app/main.py
import json
import numpy as np  # 큰 의존성도 OK (컨테이너 이미지)
from sklearn.ensemble import RandomForestClassifier

# 모델 로드 (콜드 스타트 시 1회)
model = RandomForestClassifier()

def handler(event, context):
    """ML 추론 Lambda"""
    features = np.array(event['features']).reshape(1, -1)
    prediction = model.predict(features)

    return {
        'statusCode': 200,
        'body': json.dumps({
            'prediction': prediction.tolist()
        })
    }

10. 비용 최적화

10.1 Lambda 비용 구조

Lambda 비용 = 요청 수 비용 + 실행 시간 비용

요청 수 비용:
  -100만 건 무료
  - 이후 100만 건당 약 0.20 USD

실행 시간 비용 (x86):
  - 128MB: 0.0000000021 USD / ms
  - 512MB: 0.0000000083 USD / ms
  - 1024MB: 0.0000000167 USD / ms
  - 1769MB (1 vCPU): 0.0000000289 USD / ms
  - 10240MB: 0.0000001667 USD / ms

ARM64 (Graviton2) 비용:
  - x86 대비 약 20% 저렴
  - 성능은 동등하거나 우수

Provisioned Concurrency 추가 비용:
  - 프로비저닝: 0.0000041667 USD / GB-  - 실행: 0.0000000150 USD / GB-ms (일반보다 저렴)

10.2 메모리 최적화 (Power Tuning)

# AWS Lambda Power Tuning 사용
# Step Functions 기반으로 최적 메모리를 찾아줌
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:ap-northeast-2:123456789:stateMachine:powerTuning \
  --input '{
    "lambdaARN": "arn:aws:lambda:ap-northeast-2:123456789:function:my-function",
    "powerValues": [128, 256, 512, 1024, 1769, 3008],
    "num": 50,
    "payload": "{\"test\": true}",
    "parallelInvocation": true,
    "strategy": "cost"
  }'
메모리 (MB)평균 실행 시간비용/호출최적 여부
1282500ms0.0053 USD
2561200ms0.0051 USD
512600ms0.0050 USD비용 최적
1024350ms0.0058 USD
1769200ms0.0058 USD성능 최적

10.3 비용 절감 체크리스트

  1. ARM64 (Graviton2) 전환 - 20% 절감, 동등 성능
  2. 메모리 Power Tuning - 과소/과대 프로비저닝 방지
  3. 타임아웃 적절 설정 - 무한 실행 방지
  4. DLQ 설정 - 실패 반복 호출 방지
  5. Reserved Concurrency - 과도한 스케일링 제한
  6. Lambda Layer 활용 - 코드 크기 줄여 콜드 스타트 감소
  7. EventBridge 스케줄 - CloudWatch Events 대체로 비용 최적화
  8. S3 Intelligent-Tiering - 접근 패턴에 따른 자동 최적화
  9. DynamoDB On-Demand - 예측 불가 트래픽에 적합
  10. API Gateway 캐싱 - Lambda 호출 감소

11. Serverless vs Container 의사결정 프레임워크

11.1 비교 매트릭스

기준Serverless (Lambda)Container (ECS/K8s)
실행 시간최대 15분무제한
스케일링 속도초 단위분 단위
최소 비용0 (미사용 시)항상 기본 비용
최대 처리량동시성 제한 있음Pod 수에 따라 무제한
상태 관리StatelessStateful 가능
워밍업콜드 스타트 있음상시 실행
벤더 종속높음중간
운영 부담매우 낮음높음
디버깅어려움쉬움
네트워크제한적완전 제어

11.2 의사결정 플로우

워크로드 유형 판별:
1. 실행 시간 15분 이상? -> Container
2. 상시 트래픽 (초당 수백 건 이상)? -> Container (비용 효율)
3. 간헐적 트래픽? -> Serverless
4. GPU 필요? -> Container
5. 특수 런타임 필요? -> Container
6. 빠른 프로토타이핑? -> Serverless
7. WebSocket 장기 연결? -> Container
8. 배치 처리 (큰 데이터)? -> Step Functions + Lambda or Container

12. 모니터링과 관찰성

12.1 Lambda Powertools

# Lambda Powertools - 구조화된 로깅, 추적, 메트릭
from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.event_handler import APIGatewayRestResolver

logger = Logger()
tracer = Tracer()
metrics = Metrics()
app = APIGatewayRestResolver()

@app.get("/orders/<order_id>")
@tracer.capture_method
def get_order(order_id: str):
    logger.info("Fetching order", extra={"order_id": order_id})

    order = fetch_order(order_id)

    metrics.add_metric(name="OrderFetched", unit=MetricUnit.Count, value=1)
    metrics.add_dimension(name="Environment", value="production")

    return {"order": order}

@logger.inject_lambda_context
@tracer.capture_lambda_handler
@metrics.log_metrics(capture_cold_start_metric=True)
def handler(event, context):
    return app.resolve(event, context)

12.2 X-Ray 분산 추적

# X-Ray SDK로 외부 호출 추적
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all

# 모든 AWS SDK 호출 자동 추적
patch_all()

@xray_recorder.capture('process_order')
def process_order(order):
    # 하위 세그먼트 생성
    subsegment = xray_recorder.begin_subsegment('validate')
    try:
        validate_order(order)
        subsegment.put_annotation('valid', True)
    except Exception as e:
        subsegment.put_annotation('valid', False)
        subsegment.add_exception(e)
        raise
    finally:
        xray_recorder.end_subsegment()

    # DynamoDB 호출 (자동 추적)
    save_order(order)

    # SNS 발행 (자동 추적)
    publish_event(order)

12.3 CloudWatch 알람 설정

Resources:
  # Lambda 에러율 알람
  LambdaErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: lambda-high-error-rate
      MetricName: Errors
      Namespace: AWS/Lambda
      Dimensions:
        - Name: FunctionName
          Value: !Ref MyFunction
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 2
      Threshold: 5
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref AlertTopic

  # Lambda 스로틀 알람
  LambdaThrottleAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: lambda-throttled
      MetricName: Throttles
      Namespace: AWS/Lambda
      Dimensions:
        - Name: FunctionName
          Value: !Ref MyFunction
      Statistic: Sum
      Period: 60
      EvaluationPeriods: 1
      Threshold: 0
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref AlertTopic

  # 동시성 사용률 알람
  ConcurrencyAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: lambda-high-concurrency
      MetricName: ConcurrentExecutions
      Namespace: AWS/Lambda
      Dimensions:
        - Name: FunctionName
          Value: !Ref MyFunction
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 3
      Threshold: 800
      ComparisonOperator: GreaterThanThreshold

13. 테스트 전략

13.1 로컬 테스트 with SAM CLI

# SAM CLI로 로컬 Lambda 실행
sam local invoke MyFunction \
  --event events/api-gateway.json \
  --env-vars env.json

# 로컬 API 서버 실행
sam local start-api --port 3000

# DynamoDB Local과 함께 사용
docker run -p 8000:8000 amazon/dynamodb-local
sam local invoke --docker-network host

13.2 통합 테스트

# test_integration.py
import boto3
import pytest
import json
import time

STACK_NAME = 'my-serverless-app'
API_URL = None

@pytest.fixture(scope='session', autouse=True)
def setup():
    """CloudFormation 스택에서 API URL 가져오기"""
    global API_URL
    cfn = boto3.client('cloudformation')
    response = cfn.describe_stacks(StackName=STACK_NAME)
    outputs = response['Stacks'][0]['Outputs']
    API_URL = next(o['OutputValue'] for o in outputs if o['OutputKey'] == 'ApiUrl')

def test_create_order():
    """주문 생성 통합 테스트"""
    import requests

    response = requests.post(
        f'{API_URL}/orders',
        json={
            'userId': 'test-user',
            'items': [
                {'productId': 'p1', 'name': 'Widget', 'quantity': 2, 'price': 1000}
            ]
        },
        headers={'Authorization': f'Bearer {get_test_token()}'}
    )

    assert response.status_code == 201
    data = response.json()
    assert 'orderId' in data
    assert data['status'] == 'CREATED'
    assert data['total'] == 2000

def test_get_order():
    """주문 조회 통합 테스트"""
    import requests

    # 먼저 주문 생성
    create_response = requests.post(
        f'{API_URL}/orders',
        json={
            'userId': 'test-user',
            'items': [{'productId': 'p1', 'name': 'Widget', 'quantity': 1, 'price': 500}]
        },
        headers={'Authorization': f'Bearer {get_test_token()}'}
    )
    order_id = create_response.json()['orderId']

    # 조회
    response = requests.get(
        f'{API_URL}/orders/{order_id}',
        headers={'Authorization': f'Bearer {get_test_token()}'}
    )

    assert response.status_code == 200
    assert response.json()['orderId'] == order_id

13.3 단위 테스트 (모킹)

# test_unit.py
import json
import pytest
from unittest.mock import patch, MagicMock
from moto import mock_dynamodb, mock_sns

@mock_dynamodb
@mock_sns
def test_create_order_handler():
    """Lambda 핸들러 단위 테스트"""
    import boto3

    # DynamoDB 테이블 생성
    dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-2')
    table = dynamodb.create_table(
        TableName='orders',
        KeySchema=[{'AttributeName': 'orderId', 'KeyType': 'HASH'}],
        AttributeDefinitions=[{'AttributeName': 'orderId', 'AttributeType': 'S'}],
        BillingMode='PAY_PER_REQUEST'
    )

    # SNS 토픽 생성
    sns = boto3.client('sns', region_name='ap-northeast-2')
    topic = sns.create_topic(Name='order-events')

    import os
    os.environ['ORDERS_TABLE'] = 'orders'
    os.environ['ORDER_TOPIC'] = topic['TopicArn']

    from src.orders.create import handler

    event = {
        'body': json.dumps({
            'userId': 'user123',
            'items': [{'productId': 'p1', 'name': 'Test', 'quantity': 1, 'price': 1000}]
        })
    }

    context = MagicMock()
    context.aws_request_id = 'test-request-id'

    response = handler(event, context)

    assert response['statusCode'] == 201
    body = json.loads(response['body'])
    assert body['userId'] == 'user123'
    assert body['total'] == 1000

14. 실전 아키텍처 예시

14.1 이커머스 주문 시스템

클라이언트
  |
  v
[API Gateway] --> [Lambda: 주문 생성]
                     |
                     v
                  [DynamoDB: 주문 저장]
                     |
                     v
                  [EventBridge: OrderCreated 발행]
                     |
          +----------+----------+
          |          |          |
          v          v          v
     [Lambda:    [Lambda:    [Lambda:
      재고 확인]   결제 처리]   알림 발송]
          |          |
          v          v
     [DynamoDB]  [Stripe API]
          |
          v
     [Step Functions: 배송 워크플로]
          |
          v
     [Lambda: 배송 추적 업데이트]
          |
          v
     [WebSocket -> 클라이언트 실시간 알림]

14.2 미디어 처리 파이프라인

[S3: 원본 업로드]
  |
  v
[EventBridge: S3 이벤트]
  |
  v
[Step Functions: 미디어 파이프라인]
  |
  +-> [Lambda: 메타데이터 추출]
  |
  +-> [Lambda: 썸네일 생성]
  |
  +-> [Lambda: 비디오 트랜스코딩 시작]
  |      |
  |      v
  |   [MediaConvert]
  |      |
  |      v
  |   [Lambda: 트랜스코딩 완료 처리]
  |
  +-> [Lambda: AI 태깅 (Rekognition)]
  |
  v
[DynamoDB: 메타데이터 저장]
  |
  v
[CloudFront: CDN 배포]

15. 퀴즈

Q1. Lambda의 콜드 스타트가 가장 긴 런타임은?

정답: Java (SnapStart 미적용 시)

Java는 JVM 초기화, 클래스 로딩, JIT 컴파일 등으로 인해 콜드 스타트가 800ms에서 8초까지 발생할 수 있다. SnapStart를 사용하면 100-200ms로 크게 줄일 수 있다. Rust와 Go는 네이티브 바이너리로 컴파일되어 30-100ms 수준이다.

Q2. Step Functions Standard와 Express의 주요 차이점은?

정답:

  • Standard: 최대 1년 실행, Exactly-once, 상태 전이당 과금, 실행 이력 90일 보관
  • Express: 최대 5분 실행, At-least-once, 실행 시간/메모리 과금, 초당 100,000건 이상 처리 가능

Standard는 장기 실행 비즈니스 워크플로에, Express는 대량/빠른 데이터 처리에 적합하다.

Q3. Provisioned Concurrency와 Reserved Concurrency의 차이는?

정답:

  • Provisioned Concurrency: Lambda 인스턴스를 미리 초기화하여 콜드 스타트를 제거. 추가 비용 발생
  • Reserved Concurrency: 특정 함수의 최대 동시 실행 수를 제한. 비용 없음. 다른 함수의 동시성 확보가 목적

Provisioned는 성능 보장, Reserved는 리소스 격리 목적이다.

Q4. DynamoDB 단일 테이블 설계의 장단점은?

정답:

장점:

  • 하나의 쿼리로 여러 엔티티를 조회 가능 (낮은 지연시간)
  • 테이블 관리가 단순
  • 트랜잭션 비용 절감

단점:

  • 접근 패턴을 미리 알아야 함
  • 스키마 변경이 어려움
  • 학습 곡선이 높음
  • 데이터 마이그레이션이 복잡
Q5. Serverless를 선택하지 말아야 하는 상황은?

정답:

  • 실행 시간이 15분을 초과하는 장기 실행 작업
  • GPU가 필요한 ML 학습
  • 상시 높은 트래픽으로 인해 컨테이너가 더 비용 효율적인 경우
  • WebSocket 등 장기 연결이 필요한 경우
  • 매우 낮은 지연시간이 필수인 경우 (콜드 스타트 허용 불가)
  • 복잡한 네트워크 구성이 필요한 경우

16. 참고 자료

  1. AWS Lambda 공식 문서 - https://docs.aws.amazon.com/lambda/
  2. AWS Step Functions 개발자 가이드 - https://docs.aws.amazon.com/step-functions/
  3. Serverless Application Model (SAM) - https://docs.aws.amazon.com/serverless-application-model/
  4. Lambda Powertools for Python - https://docs.powertools.aws.dev/lambda/python/
  5. DynamoDB 단일 테이블 설계 - https://www.alexdebrie.com/posts/dynamodb-single-table/
  6. AWS Well-Architected Serverless Lens - https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/
  7. Lambda Power Tuning - https://github.com/alexcasalboni/aws-lambda-power-tuning
  8. Serverless Land - https://serverlessland.com/
  9. EventBridge 패턴 - https://docs.aws.amazon.com/eventbridge/latest/userguide/
  10. Aurora Serverless v2 - https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.html
  11. API Gateway REST API - https://docs.aws.amazon.com/apigateway/latest/developerguide/
  12. X-Ray 분산 추적 - https://docs.aws.amazon.com/xray/latest/devguide/
  13. Serverless Framework - https://www.serverless.com/framework/docs/