서버리스로 바꿨더니 비용이 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
  • 큰 응답을 자주 보내면 비용 증가

비용 계산 예시

JS
// 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,200ms0.4$0.0000067
512MB~28%850ms0.43$0.0000071
1024MB~55%420ms0.43$0.0000070
2048MB~100%380ms0.76$0.0000127

1024MB가 최적점입니다. 128MB 대비 7배 빠르면서 비용은 거의 같습니다!

AWS Lambda Power Tuning으로 최적점 찾기

AWS에서 공식 제공하는 Power Tuning 도구를 사용하면 자동으로 최적 메모리를 찾아줍니다.

BASH
# 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 상태 머신이 생성됨
BASH
# 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)

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'
    })
  ]
};

번들 최적화 결과:

항목BeforeAfter개선
번들 크기45MB2.8MB-94%
Cold Start2.8초0.4초-86%

Provisioned Concurrency (비용 주의!)

Cold Start를 완전히 없애려면 Provisioned Concurrency를 사용합니다. 항상 켜져 있는 인스턴스를 유지하는 것입니다.

YAML
# 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 활성화

YAML
# 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 호출 자체를 줄일 수 있습니다.

YAML
# 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% 절감 가능

중복 호출 방지 (멱등성)

네트워크 문제로 같은 요청이 여러 번 올 수 있습니다. 중복 처리를 방지해야 합니다.

JS
// 멱등성 키를 사용한 중복 방지
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);
}

최종 비용 절감 결과

항목BeforeAfter절감
월 Lambda 비용$2,400$720-70%
평균 응답 시간850ms340ms-60%
Cold Start 비율15%2%-87%
월 요청 수5천만3천만-40% (캐싱)

최적화 체크리스트

  1. 메모리 튜닝: Power Tuning으로 최적점 찾기
  2. 번들 최적화: 외부 의존성 최소화, Tree Shaking
  3. 캐싱 적용: API Gateway, CloudFront 레벨
  4. Provisioned Concurrency: 피크 시간만 적용
  5. 모니터링: CloudWatch로 이상 징후 감지

결론: 서버리스가 항상 저렴하진 않다

서버리스는 트래픽 패턴에 따라 비용 효율이 달라집니다:

  • 유리한 경우: 트래픽 변동이 큼, 야간/주말 트래픽 적음
  • 불리한 경우: 24시간 일정한 트래픽, 장시간 실행 작업

무작정 "서버리스 = 저렴"이라고 생각하지 마세요. 비용 구조를 이해하고, 최적화를 적용해야 진정한 비용 절감을 달성할 수 있습니다.



 

aws lambda