들어가며: '서버리스'의 달콤한 유혹과 냉혹한 현실
서버리스 아키텍처는 처음에는 매우 단순해 보입니다. Lambda 함수 몇 개로 시작해서, SQS로 비동기 처리를 연결하고, DynamoDB로 데이터를 저장하면 끝이죠. 하지만 이 구조가 수백, 수천 개의 AWS 계정으로 확장되고, Lambda 함수가 100만 개를 넘어가면 이야기가 완전히 달라집니다.
이번 글에서는 ProGlove라는 회사가 실제로 겪은 경험을 바탕으로, 서버리스 시스템이 대규모로 확장될 때 발생하는 예상치 못한 문제들과 그 해결책을 살펴보겠습니다. 단순히 'Lambda 비용을 줄이는 방법'을 넘어서, 아키텍처 패턴 자체를 재고해야 했던 과정을 중점적으로 다룹니다.
참고자료: 이 글은 AWS Architecture Blog의 원문을 기반으로, 한국 개발자 커뮤니티의 맥락에 맞게 재구성했습니다.

1단계: 계정당 하나의 테넌트 — 단순함이 준 선물과 저주
ProGlove는 계정당 하나의 테넌트(1 account per tenant) 모델을 선택했습니다. 보안 격리, 비용 투명성, 서비스 소유권 명확화라는 장점이 있었죠. 하지만 이 선택은 이후 모든 확장 문제의 근원이 됩니다.
CloudFormation StackSets: 처음에는 신이었는데…
처음 50개 계정까지는 CloudFormation StackSets가 훌륭하게 작동했습니다. 중앙 관리 계정에서 한 번의 배포 명령으로 수십 개 계정에 동시에 인프라를 업데이트할 수 있었죠.
# 초기 StackSets 구성 예시 (단순화)
AWSTemplateFormatVersion: '2010-09-09'
Description: '마이크로서비스 기본 스택 - 모든 테넌트 계정에 배포'
Parameters:
TenantId:
Type: String
Description: '테넌트 고유 식별자'
Resources:
TenantLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${TenantId}-processor'
Handler: index.handler
Runtime: python3.12
Code:
ZipFile: |
import json
def handler(event, context):
# ... 실제 비즈니스 로직
return {'statusCode': 200}
Role: !GetAtt LambdaExecutionRole.Arn
하지만 Lambda 함수가 100만 개를 넘어가면서 StackSets는 성능 한계에 부딪혔습니다. 배포 오류가 쌓이고, 롤백이 실패하는 일이 잦아졌죠. 결국 자체 배포 엔진을 만들기 시작했는데, 이 소식을 들은 AWS CloudFormation 팀이 직접 협력을 제안했다는 일화는 꽤 흥미롭습니다.

2단계: '셀프 DDoS'와 관찰 가능성의 역설
동기화된 스케줄이 만든 재앙
모든 Lambda 함수가 동일한 5분 주기로 CloudWatch 알람을 보내도록 설정되어 있었습니다. 계정이 수백 개를 넘어가면서, 이 '동시다발적 요청'이 내부 API를 마치 DDoS 공격처럼 압도해버린 거죠.
해결책은 '지터(Jitter)'의 도입이었습니다.
# 지터 도입 전: 모든 함수가 정각에 실행
import boto3
def scheduled_handler(event, context):
# 모든 계정이 동시에 이 함수를 실행
client = boto3.client('cloudwatch')
# ... 지표 전송
pass
# 지터 도입 후: 무작위 지연 추가
import random
import time
def scheduled_handler(event, context):
# 0~300초 사이의 무작위 지연
delay = random.uniform(0, 300)
time.sleep(delay)
client = boto3.client('cloudwatch')
# ... 지표 전송
pass
핵심 원칙: "절대 모든 곳에서 같은 일을 동시에 하지 마라."
관찰 가능성 비용: 숨은 복병
계정당 월 3달러의 모니터링 비용은 몇 백 개까지는 '작은 비용'이었습니다. 하지만 수천 개 계정으로 확장되자, 모니터링 비용이 컴퓨팅 비용을 넘어서는 상황이 발생했습니다.
ProGlove는 모든 지표를 외부 모니터링 도구로 전달하는 대신, 우선순위에 따라 데이터를 분류하여 전송 비용을 계정당 0.7달러까지 낮췄습니다. 비활성 계정은 기본 지표만 모니터링하여 거의 0에 가깝게 유지했습니다.

3단계: SQS의 함정 — '무료'라고 생각했던 비용
전통적인 서버리스 모범 사례는 SQS를 사용하여 이벤트를 버퍼링하고 재시도 메커니즘을 제공하는 것입니다. 하지만 수천 개의 계정에서 각각의 SQS 큐를 폴링하면, 메시지가 하나도 없는데도 지속적으로 요청이 발생하여 비용이 쌓입니다.
해결책: SQS 제거와 중앙 집중형 DLQ
# 기존: EventBridge -> SQS -> Lambda (계정별 DLQ)
# 변경: EventBridge -> Lambda (중앙 DLQ)
# 중앙 DLQ 구성 예시
Resources:
CentralDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: 'central-dlq'
MessageRetentionPeriod: 1209600 # 14일
TenantProcessorFunction:
Type: AWS::Lambda::EventInvokeConfig
Properties:
FunctionName: !Ref TenantLambdaFunction
MaximumRetryAttempts: 2
DestinationConfig:
OnFailure:
Destination: !GetAtt CentralDeadLetterQueue.Arn
Type: SQS
핵심 트레이드오프: 중앙 DLQ는 데이터 격리(isolation) 원칙을 깨뜨립니다. 여러 테넌트의 실패 이벤트가 한 곳에 모이기 때문에, AWS 계정 ID를 테넌트 ID로 사용하는 '브리지드 모델'로 전환해야 했습니다.
결론: 효율성이 성장보다 빨라야 한다
ProGlove의 사례는 서버리스가 '자동 확장'만으로 해결되지 않는 문제들을 명확히 보여줍니다. 100만 개의 Lambda 함수를 운영하는 데 필요한 것은 용량이 아니라 효율성입니다.
한국 개발 생태계에서의 적용 맥락
국내 SI/스타트업 환경에서는 '계정당 테넌트' 모델이 비용 측면에서 부담스러울 수 있습니다. 하지만 핵심 교훈은 계정 분리가 아닌, '규모에 따른 아키텍처 패턴의 변화' 에 있습니다.
- 초기 스타트업: SQS 기반의 표준 패턴을 따르되, 미리 지터를 도입하세요.
- 성장 단계(50~200 계정): 모니터링 비용을 컴퓨팅 비용과 동등하게 관리하세요.
- 대규모(200+ 계정): SQS 제거, 중앙 DLQ 도입, 자체 배포 엔진 고려를 검토하세요.
이 기술의 한계와 주의사항
- 데이터 격리 vs 비용: 중앙 DLQ는 격리를 약화시킵니다. 규제가 엄격한 산업(금융, 의료)에서는 적용이 어려울 수 있습니다.
- StackSets 의존성: AWS 서비스 팀과의 긴밀한 협력이 가능한 환경에서만 추천합니다.
다음 단계 학습 방향
- Serverless Land에서 다양한 이벤트 기반 패턴을 탐색하세요.
- AWS Lambda의 'Provisioned Concurrency' 와 'Scale-to-Zero'의 균형을 실험해보세요.
- 엔터프라이즈 AI 에이전트의 새로운 시대에서 다루는 자동화 패턴도 서버리스와 결합하면 좋습니다.
함께 보면 좋은 글