서버리스로 바꿨더니 비용이 3배?
서버리스(Serverless)로 전환하면 비용이 줄어든다고 들었습니다. 하지만 저희 팀은 전환 첫 달에 청구서가 3배가 되는 충격을 겪었습니다. 원인을 분석하고 6개월간 최적화한 끝에 비용 70% 절감과 응답 시간 60% 개선을 달성했습니다. 그 과정을 공유합니다.
왜 비용이 폭발했나?
첫 달 청구서를 분석해보니:
| 항목 | 예상 | 실제 | 원인 |
|---|---|---|---|
| Lambda 실행 | $200 | $800 | 메모리 과다 설정, 긴 실행 시간 |
| API Gateway | $50 | $150 | 캐싱 미설정, 불필요한 호출 |
| CloudWatch | $30 | $200 | 로그 과다 출력 |
| 데이터 전송 | $20 | $100 | 응답 크기 최적화 안 함 |
문제의 핵심: Lambda 비용 구조를 이해하지 못한 채 마이그레이션했습니다.
Lambda 비용 구조: 이것만 이해하면 됩니다
Lambda 비용은 크게 4가지로 나뉩니다:
1. 요청 수 (Request)
- 100만 건당 $0.20
- 상대적으로 저렴. 최적화 우선순위 낮음
2. 실행 시간 × 메모리 (Duration × Memory)
- GB-초당 $0.0000166667
- 가장 중요! 이게 비용의 80% 이상
- 예: 1GB 메모리, 1초 실행, 100만 회 = $16.67
3. 프로비저닝 동시성 (Provisioned Concurrency)
- GB-시간당 $0.0000041667
- Cold Start 방지용. 항상 켜두는 인스턴스
- 필요한 만큼만 설정
4. 데이터 전송 (Data Transfer)
- 아웃바운드 GB당 $0.09
- 큰 응답을 자주 보내면 비용 증가
비용 계산 예시
// Lambda 비용 계산기
// 실제로 사용해보세요!
function calculateLambdaCost({
memoryMB, // 메모리 (MB)
avgDurationMs, // 평균 실행 시간 (ms)
requestsPerMonth, // 월 요청 수
provisionedCount = 0 // 프로비저닝 동시성 수
}) {
// 1. 요청 비용
// 첫 100만 건 무료, 이후 $0.20/백만
const requestCost = Math.max(0, (requestsPerMonth - 1_000_000)) * 0.0000002;
// 2. 실행 비용
// GB-초 = (메모리MB / 1024) × (시간ms / 1000)
const gbSeconds = (memoryMB / 1024) * (avgDurationMs / 1000) * requestsPerMonth;
// 첫 40만 GB-초 무료
const billableGbSeconds = Math.max(0, gbSeconds - 400_000);
const durationCost = billableGbSeconds * 0.0000166667;
// 3. 프로비저닝 비용
// GB-시간 = (메모리MB / 1024) × 24시간 × 30일
const provisionedGbHours = (memoryMB / 1024) * 24 * 30 * provisionedCount;
const provisionedCost = provisionedGbHours * 0.0000041667;
return {
requestCost: requestCost.toFixed(2),
durationCost: durationCost.toFixed(2),
provisionedCost: provisionedCost.toFixed(2),
totalCost: (requestCost + durationCost + provisionedCost).toFixed(2)
};
}
// 예시: 최적화 전
console.log(calculateLambdaCost({
memoryMB: 1024, // 1GB (과다!)
avgDurationMs: 3000, // 3초 (느림!)
requestsPerMonth: 10_000_000
}));
// → $467.00/월
// 예시: 최적화 후
console.log(calculateLambdaCost({
memoryMB: 512, // 512MB (적정)
avgDurationMs: 500, // 0.5초 (빠름!)
requestsPerMonth: 10_000_000
}));
// → $38.89/월 (88% 절감!)최적화 1: 메모리 튜닝 (가장 중요!)
Lambda에서 메모리를 늘리면 CPU도 비례해서 증가합니다. 직관과 다르게, 메모리를 늘리면 비용이 오히려 줄어들 수 있습니다.
왜 그런가?
| 메모리 | CPU 비율 | 실행 시간 | GB-초 | 비용/실행 |
|---|---|---|---|---|
| 128MB | ~7% | 3,200ms | 0.4 | $0.0000067 |
| 512MB | ~28% | 850ms | 0.43 | $0.0000071 |
| 1024MB | ~55% | 420ms | 0.43 | $0.0000070 |
| 2048MB | ~100% | 380ms | 0.76 | $0.0000127 |
1024MB가 최적점입니다. 128MB 대비 7배 빠르면서 비용은 거의 같습니다!
AWS Lambda Power Tuning으로 최적점 찾기
AWS에서 공식 제공하는 Power Tuning 도구를 사용하면 자동으로 최적 메모리를 찾아줍니다.
# Step 1: Power Tuning 배포
# SAM(Serverless Application Model) 사용
git clone <div class="link-preview" contenteditable="false" data-url="https://github.com/alexcasalboni/aws-lambda-power-tuning">
<div class="link-preview-content">
<div class="preview-image">
<img src="https://opengraph.githubassets.com/c8dd2f29aeb536f1d01de63b58d8eadd82f16cd78da35c633ab2dba1e9f86738/alexcasalboni/aws-lambda-power-tuning" onerror="this.style.display='none'">
</div>
<div class="preview-content">
<div class="preview-title">GitHub - alexcasalboni/aws-lambda-power-tuning: AWS Lambda Power Tuning is an open-source tool that can help you visualize and fine-tune the memory/power configuration of Lambda functions. It runs in your own AWS account - powered by AWS Step Functions - and it supports three optimization strategies: cost, speed, and balanced.</div>
<div class="preview-description">AWS Lambda Power Tuning is an open-source tool that can help you visualize and fine-tune the memory/power configuration of Lambda functions. It runs in your own AWS account - powered by AWS Step Fu...</div>
</div>
</div>
</div><p>
</p>
cd aws-lambda-power-tuning
sam deploy --guided
# 배포 후 Step Functions 상태 머신이 생성됨# Step 2: 테스트 실행
# 128MB ~ 3008MB까지 다양한 메모리로 함수 실행
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:ap-northeast-2:123456789:stateMachine:powerTuningStateMachine \
--input '{
"lambdaARN": "arn:aws:lambda:ap-northeast-2:123456789:function:my-api",
"powerValues": [128, 256, 512, 1024, 1536, 2048, 3008],
"num": 50,
"payload": "{\"httpMethod\": \"GET\", \"path\": \"/users\"}"
}'
# 실행 결과:
# {
# "power": 1024, // 최적 메모리
# "cost": 0.0000070, // 실행당 비용
# "duration": 420, // 평균 실행 시간(ms)
# "stateMachine": {...} // 상세 결과 링크
# }팁: 코드 변경 후에는 다시 튜닝하세요. 최적점이 바뀔 수 있습니다.
최적화 2: Cold Start 줄이기
Lambda는 요청이 없으면 인스턴스가 내려갑니다. 다음 요청이 오면 새 인스턴스를 띄우는데, 이게 Cold Start입니다. 수백 ms ~ 수 초가 걸릴 수 있습니다.
Cold Start 원인 분석
| 요인 | 영향 | 해결책 |
|---|---|---|
| 런타임 | Java > Python > Node.js > Rust | 가벼운 런타임 선택 |
| 패키지 크기 | 크면 클수록 느림 | 번들 최적화 |
| VPC 연결 | ENI 생성 시간 | VPC Lambda Hyperplane |
| 초기화 코드 | import 많으면 느림 | 지연 로딩 |
번들 크기 최적화 (Node.js)
// webpack.config.js
// Lambda용 번들 최적화 설정
const TerserPlugin = require('terser-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'production',
target: 'node', // 브라우저가 아닌 Node.js용
entry: './src/handler.ts',
output: {
filename: 'handler.js',
libraryTarget: 'commonjs2' // Lambda가 require()로 로드
},
// AWS SDK는 Lambda 런타임에 이미 포함되어 있음
// 번들에 포함하면 용량만 커짐
externals: [
'aws-sdk', // v2
'@aws-sdk/*' // v3
],
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
keep_fnames: false, // 함수명 난독화
mangle: true // 변수명 축소
}
})]
},
plugins: [
// 번들 분석 리포트 생성 (어떤 라이브러리가 큰지 확인)
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html'
})
]
};번들 최적화 결과:
| 항목 | Before | After | 개선 |
|---|---|---|---|
| 번들 크기 | 45MB | 2.8MB | -94% |
| Cold Start | 2.8초 | 0.4초 | -86% |
Provisioned Concurrency (비용 주의!)
Cold Start를 완전히 없애려면 Provisioned Concurrency를 사용합니다. 항상 켜져 있는 인스턴스를 유지하는 것입니다.
# serverless.yml
functions:
api:
handler: src/handler.main
memorySize: 1024
timeout: 30
# 항상 5개 인스턴스 유지
provisionedConcurrency: 5
# 하지만 24시간 유지하면 비용 폭발!
# 트래픽 패턴에 맞춰 스케줄링 권장비용 계산 예시:
- 1GB × 5개 × 24시간 × 30일 = 3,600 GB-시간
- 3,600 × $0.0000041667 = $15/월
- 트래픽 적으면 On-Demand보다 비쌀 수 있음!
권장 방식: 피크 시간에만 Provisioned Concurrency 활성화
# CloudWatch Events로 스케줄링
# 오전 9시에 5개로 늘리고, 밤 10시에 0개로
resources:
Resources:
ScaleUpRule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: cron(0 9 * * ? *) # 매일 9시
Targets:
- Id: scale-up
Arn: !GetAtt ScaleLambda.Arn
Input: '{"action": "scale-up", "count": 5}'
ScaleDownRule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: cron(0 22 * * ? *) # 매일 22시
Targets:
- Id: scale-down
Arn: !GetAtt ScaleLambda.Arn
Input: '{"action": "scale-down", "count": 0}'최적화 3: 불필요한 호출 제거
API Gateway 캐싱
같은 요청에 대해 매번 Lambda를 호출하면 낭비입니다. API Gateway 레벨에서 캐싱하면 Lambda 호출 자체를 줄일 수 있습니다.
# serverless.yml
functions:
getProducts:
handler: src/products.list
events:
- http:
path: /products
method: GET
# API Gateway 캐싱 활성화
caching:
enabled: true
ttlInSeconds: 300 # 5분간 캐시
# 캐시 키: 이 파라미터가 다르면 다른 캐시
cacheKeyParameters:
- name: request.querystring.category
- name: request.querystring.page효과:
- 캐시 히트 시 Lambda 호출 안 함
- 응답 시간: 500ms → 20ms
- Lambda 비용 50-80% 절감 가능
중복 호출 방지 (멱등성)
네트워크 문제로 같은 요청이 여러 번 올 수 있습니다. 중복 처리를 방지해야 합니다.
// 멱등성 키를 사용한 중복 방지
const { DynamoDB } = require('@aws-sdk/client-dynamodb');
const crypto = require('crypto');
const dynamodb = new DynamoDB();
async function processWithIdempotency(event, processor) {
// 1. 요청 내용을 해시하여 고유 키 생성
const idempotencyKey = crypto
.createHash('sha256')
.update(JSON.stringify(event.body))
.digest('hex');
// 2. DynamoDB에 키 저장 시도
try {
await dynamodb.putItem({
TableName: 'IdempotencyTable',
Item: {
pk: { S: idempotencyKey },
ttl: { N: String(Math.floor(Date.now() / 1000) + 3600) } // 1시간 후 만료
},
// 이미 키가 있으면 실패
ConditionExpression: 'attribute_not_exists(pk)'
});
} catch (e) {
if (e.name === 'ConditionalCheckFailedException') {
// 이미 처리된 요청
console.log('Duplicate request, skipping:', idempotencyKey);
return { statusCode: 200, body: 'Already processed' };
}
throw e;
}
// 3. 중복이 아니면 실제 처리
return await processor(event);
}최종 비용 절감 결과
| 항목 | Before | After | 절감 |
|---|---|---|---|
| 월 Lambda 비용 | $2,400 | $720 | -70% |
| 평균 응답 시간 | 850ms | 340ms | -60% |
| Cold Start 비율 | 15% | 2% | -87% |
| 월 요청 수 | 5천만 | 3천만 | -40% (캐싱) |
최적화 체크리스트
- 메모리 튜닝: Power Tuning으로 최적점 찾기
- 번들 최적화: 외부 의존성 최소화, Tree Shaking
- 캐싱 적용: API Gateway, CloudFront 레벨
- Provisioned Concurrency: 피크 시간만 적용
- 모니터링: CloudWatch로 이상 징후 감지
결론: 서버리스가 항상 저렴하진 않다
서버리스는 트래픽 패턴에 따라 비용 효율이 달라집니다:
- 유리한 경우: 트래픽 변동이 큼, 야간/주말 트래픽 적음
- 불리한 경우: 24시간 일정한 트래픽, 장시간 실행 작업
무작정 "서버리스 = 저렴"이라고 생각하지 마세요. 비용 구조를 이해하고, 최적화를 적용해야 진정한 비용 절감을 달성할 수 있습니다.