서버리스 플랫폼 비교 분석 2025
AWS Lambda vs Vercel vs Cloudflare Workers - 서버리스 플랫폼 선택 가이드
⚡ 서버리스 플랫폼 비교 분석 2025
서버리스 아키텍처를 위한 최적의 플랫폼 선택 가이드
📊 개요
비교 대상
- AWS Lambda: 서버리스 컴퓨팅의 선구자
- Vercel: 프론트엔드 중심 서버리스
- Cloudflare Workers: 엣지 컴퓨팅 플랫폼
- Netlify Functions: JAMstack 친화적
- Google Cloud Functions: GCP 통합
- Azure Functions: 엔터프라이즈 서버리스
평가 기준
- 성능 (콜드 스타트, 응답 시간)
- 가격
- 개발 경험
- 생태계
- 제한 사항
- 확장성
📈 상세 비교표
핵심 특성 비교
| 특성 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | | ------------------ | ----------- | ------------------- | ------------------ | --------- | | 콜드 스타트 | 100-300ms | 50-150ms | <10ms | 100-200ms | | 최대 실행 시간 | 15분 | 10초(무료)/300초 | 30초 | 10초 | | 메모리 | 128MB-10GB | 1GB-3GB | 128MB | 1GB | | 언어 지원 | 다양함 | Node/Python/Go/Ruby | JS/Wasm | JS/Go | | 엣지 배포 | CloudFront | 글로벌 | 280+ 도시 | CDN 통합 | | 가격 | 사용량 기반 | 무료 시작 | 저렴함 | 무료 시작 |
기술적 제한사항
| 항목 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | | ----------------- | ---------- | --------- | ------------------ | --------- | | 패키지 크기 | 250MB | 50MB | 1MB | 50MB | | 환경 변수 | 4KB | 64KB | 1MB | 무제한 | | 동시 실행 | 1000(기본) | 무제한 | 무제한 | 무제한 | | HTTP 페이로드 | 6MB | 4.5MB | 100MB | 6MB | | 파일 시스템 | /tmp 512MB | 읽기 전용 | 없음 | 읽기 전용 |
가격 비교 (월 기준)
| 사용량 | AWS Lambda | Vercel | Cloudflare Workers | Netlify | | --------------- | ---------------- | ------ | ------------------ | ------- | | 100만 요청 | $0.20 | $0 | $5 | $0 | | 1000만 요청 | $2 | $20 | $50 | $19 | | 1억 요청 | $20 | $200 | $500 | $190 | | 컴퓨팅 시간 | $0.0000166/GB-초 | 포함 | 포함 | 포함 |
💼 사용 사례별 구현
🌐 글로벌 API 서비스
추천: Cloudflare Workers
// Cloudflare Workers - 엣지에서 실행되는 API
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// 지역별 라우팅
const country = request.cf?.country || "US";
const colo = request.cf?.colo || "Unknown";
// KV 스토어에서 캐시 확인
const cache = caches.default;
const cacheKey = new Request(request.url.toString(), request);
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
// API 라우팅
const url = new URL(request.url);
switch (url.pathname) {
case "/api/users":
return handleUsers(request, country);
case "/api/products":
return handleProducts(request);
case "/api/geo":
return handleGeoData(request, country, colo);
default:
return new Response("Not Found", { status: 404 });
}
}
async function handleUsers(request, country) {
// Durable Objects를 사용한 상태 관리
const id = USERS_NAMESPACE.idFromName(country);
const stub = USERS_NAMESPACE.get(id);
return stub.fetch(request);
}
// Durable Object 클래스
export class UserCounter {
constructor(state, env) {
this.state = state;
this.env = env;
}
async fetch(request) {
const url = new URL(request.url);
if (request.method === "GET") {
const value = (await this.state.storage.get("count")) || 0;
return new Response(JSON.stringify({ count: value }));
}
if (request.method === "POST") {
let value = (await this.state.storage.get("count")) || 0;
value++;
await this.state.storage.put("count", value);
return new Response(JSON.stringify({ count: value }));
}
}
}
// WebSocket 지원
async function handleWebSocket(request) {
const upgradeHeader = request.headers.get("Upgrade");
if (upgradeHeader !== "websocket") {
return new Response("Expected websocket", { status: 400 });
}
const [client, server] = Object.values(new WebSocketPair());
server.accept();
server.addEventListener("message", (event) => {
server.send(
JSON.stringify({
type: "echo",
data: event.data,
timestamp: Date.now(),
}),
);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
// R2 스토리지 활용
async function handleFileUpload(request) {
const formData = await request.formData();
const file = formData.get("file");
if (!file) {
return new Response("No file uploaded", { status: 400 });
}
const key = `uploads/${Date.now()}-${file.name}`;
await R2_BUCKET.put(key, file.stream(), {
httpMetadata: {
contentType: file.type,
},
});
return new Response(
JSON.stringify({
key,
size: file.size,
type: file.type,
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
🚀 Next.js 풀스택 앱
추천: Vercel
// Vercel Functions - Next.js API Routes
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
// Edge Runtime for better performance
export const runtime = 'edge'
const productSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
description: z.string(),
category: z.string()
})
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')
const limit = Number(searchParams.get('limit')) || 10
// Vercel KV for caching
const cacheKey = `products:${category || 'all'}:${limit}`
const cached = await kv.get(cacheKey)
if (cached) {
return NextResponse.json(cached)
}
const products = await prisma.product.findMany({
where: category ? { category } : undefined,
take: limit,
orderBy: { createdAt: 'desc' }
})
// Cache for 5 minutes
await kv.setex(cacheKey, 300, products)
return NextResponse.json(products)
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = productSchema.parse(body)
const product = await prisma.product.create({
data: validated
})
// Revalidate cache
revalidatePath('/products')
// Send to analytics
await fetch('https://analytics.vercel.com/api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.VERCEL_ANALYTICS_TOKEN}`
},
body: JSON.stringify({
event: 'product_created',
properties: {
product_id: product.id,
category: product.category,
price: product.price
}
})
})
return NextResponse.json(product, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
}
// Vercel Cron Jobs
// app/api/cron/cleanup/route.ts
export async function GET(request: NextRequest) {
// Verify cron secret
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 })
}
// Cleanup old data
const deleted = await prisma.product.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days
}
}
})
return NextResponse.json({
deleted: deleted.count,
timestamp: new Date().toISOString()
})
}
// vercel.json
{
"crons": [{
"path": "/api/cron/cleanup",
"schedule": "0 2 * * *"
}]
}
💼 엔터프라이즈 워크플로우
추천: AWS Lambda
# AWS Lambda - 복잡한 비즈니스 로직
import json
import boto3
from aws_lambda_powertools import Logger, Tracer, Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
tracer = Tracer()
metrics = Metrics()
app = APIGatewayRestResolver()
dynamodb = boto3.resource('dynamodb')
s3 = boto3.client('s3')
sns = boto3.client('sns')
sqs = boto3.client('sqs')
# Step Functions 워크플로우의 일부
@tracer.capture_lambda_handler
@logger.inject_lambda_context
@metrics.log_metrics
def process_order_handler(event: dict, context: LambdaContext) -> dict:
"""주문 처리 Lambda"""
order_id = event['orderId']
logger.info(f"Processing order: {order_id}")
try:
# 1. DynamoDB에서 주문 조회
table = dynamodb.Table('orders')
response = table.get_item(Key={'orderId': order_id})
order = response.get('Item')
if not order:
raise ValueError(f"Order {order_id} not found")
# 2. 재고 확인 (다른 Lambda 호출)
lambda_client = boto3.client('lambda')
inventory_response = lambda_client.invoke(
FunctionName='check-inventory',
InvocationType='RequestResponse',
Payload=json.dumps({
'items': order['items']
})
)
inventory_result = json.loads(
inventory_response['Payload'].read()
)
if not inventory_result['available']:
raise ValueError("Insufficient inventory")
# 3. 결제 처리
payment_result = process_payment(order)
# 4. S3에 영수증 저장
receipt = generate_receipt(order, payment_result)
s3.put_object(
Bucket='receipts-bucket',
Key=f'receipts/{order_id}.pdf',
Body=receipt,
ContentType='application/pdf'
)
# 5. 주문 상태 업데이트
table.update_item(
Key={'orderId': order_id},
UpdateExpression='SET #status = :status, paymentId = :paymentId',
ExpressionAttributeNames={'#status': 'status'},
ExpressionAttributeValues={
':status': 'PAID',
':paymentId': payment_result['transactionId']
}
)
# 6. SNS 알림 발송
sns.publish(
TopicArn='arn:aws:sns:region:account:order-notifications',
Message=json.dumps({
'orderId': order_id,
'status': 'PAID',
'customerId': order['customerId']
}),
MessageAttributes={
'eventType': {
'DataType': 'String',
'StringValue': 'ORDER_PAID'
}
}
)
# 7. 메트릭 기록
metrics.add_metric(
name="OrderProcessed",
unit=MetricUnit.Count,
value=1
)
metrics.add_metric(
name="OrderAmount",
unit=MetricUnit.Count,
value=order['totalAmount']
)
return {
'statusCode': 200,
'body': json.dumps({
'orderId': order_id,
'status': 'PAID',
'paymentId': payment_result['transactionId']
})
}
except Exception as e:
logger.exception(f"Error processing order {order_id}")
# Dead Letter Queue로 전송
sqs.send_message(
QueueUrl='https://sqs.region.amazonaws.com/account/dlq',
MessageBody=json.dumps({
'orderId': order_id,
'error': str(e),
'timestamp': context.aws_request_id
})
)
raise
# EventBridge 이벤트 처리
@tracer.capture_lambda_handler
def event_processor_handler(event: dict, context: LambdaContext) -> dict:
"""EventBridge 이벤트 처리"""
for record in event.get('Records', []):
event_detail = json.loads(record['body'])
event_type = event_detail.get('detail-type')
if event_type == 'ORDER_CREATED':
handle_order_created(event_detail['detail'])
elif event_type == 'PAYMENT_COMPLETED':
handle_payment_completed(event_detail['detail'])
elif event_type == 'SHIPMENT_DISPATCHED':
handle_shipment_dispatched(event_detail['detail'])
return {'statusCode': 200}
# Lambda Extensions로 설정 관리
import requests
def get_config():
"""Lambda Extension에서 설정 가져오기"""
response = requests.get(
'http://localhost:2773/systemsmanager/parameters/get',
params={'name': '/myapp/config'},
headers={'X-Aws-Parameters-Secrets-Token': os.environ.get('AWS_SESSION_TOKEN')}
)
return response.json()
🎮 실시간 게임 백엔드
추천: Cloudflare Workers + Durable Objects
// Cloudflare Durable Objects - 게임 룸 상태 관리
export class GameRoom {
state: DurableObjectState;
sessions: Map<WebSocket, PlayerInfo>;
gameState: GameState;
constructor(state: DurableObjectState) {
this.state = state;
this.sessions = new Map();
this.gameState = {
players: new Map(),
board: createEmptyBoard(),
turn: 0,
status: "waiting",
};
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
case "/websocket":
return this.handleWebSocket(request);
case "/state":
return this.getGameState();
case "/action":
return this.handleAction(request);
default:
return new Response("Not found", { status: 404 });
}
}
async handleWebSocket(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.state.acceptWebSocket(server);
const playerId = new URL(request.url).searchParams.get("playerId");
this.sessions.set(server, { playerId, ws: server });
server.addEventListener("message", async (event) => {
await this.handleMessage(server, event.data);
});
server.addEventListener("close", () => {
this.handleDisconnect(server);
});
// 현재 게임 상태 전송
server.send(
JSON.stringify({
type: "gameState",
data: this.serializeGameState(),
}),
);
return new Response(null, { status: 101, webSocket: client });
}
async handleMessage(ws: WebSocket, message: string) {
const data = JSON.parse(message);
const player = this.sessions.get(ws);
switch (data.type) {
case "move":
await this.processMove(player.playerId, data.move);
break;
case "chat":
this.broadcastChat(player.playerId, data.message);
break;
}
}
async processMove(playerId: string, move: Move) {
// 게임 로직 검증
if (!this.isValidMove(playerId, move)) {
return;
}
// 상태 업데이트
this.gameState.board[move.x][move.y] = playerId;
this.gameState.turn++;
// 상태 저장
await this.state.storage.put("gameState", this.gameState);
// 모든 플레이어에게 브로드캐스트
this.broadcast({
type: "move",
playerId,
move,
nextTurn: this.getNextPlayer(),
});
// 승리 조건 확인
if (this.checkWinCondition(playerId)) {
this.endGame(playerId);
}
}
broadcast(message: any) {
const data = JSON.stringify(message);
this.sessions.forEach(({ ws }) => {
ws.send(data);
});
}
}
// Workers KV를 사용한 리더보드
export async function updateLeaderboard(
playerId: string,
score: number,
env: Env,
) {
const key = `leaderboard:${new Date().toISOString().slice(0, 10)}`;
const current = (await env.LEADERBOARD.get(key, "json")) || [];
current.push({ playerId, score, timestamp: Date.now() });
// 상위 100명만 유지
const sorted = current.sort((a, b) => b.score - a.score).slice(0, 100);
await env.LEADERBOARD.put(key, JSON.stringify(sorted), {
expirationTtl: 86400 * 30, // 30일 보관
});
return sorted;
}
🏢 실제 기업 사례
AWS Lambda 사용
- Netflix: 비디오 인코딩
- iRobot: IoT 데이터 처리
- 카카오: 이미지 처리
- 토스: 일부 마이크로서비스
Vercel 사용
- TikTok: 웹 프론트엔드
- McDonald's: 주문 시스템
- Hulu: 마케팅 사이트
- 당근마켓: 웹 서비스
Cloudflare Workers 사용
- Discord: API 라우팅
- Shopify: 엣지 로직
- DoorDash: 지역별 처리
🔧 개발 도구 및 프레임워크
프레임워크 지원
| 플랫폼 | 지원 프레임워크 | | -------------- | ------------------------------ | | AWS Lambda | Serverless Framework, SAM, CDK | | Vercel | Next.js, SvelteKit, Nuxt | | Cloudflare | Wrangler, Pages, Remix | | Netlify | Next.js, Gatsby, Nuxt |
로컬 개발
# AWS SAM
sam local start-api
sam local invoke
# Vercel CLI
vercel dev
# Wrangler (Cloudflare)
wrangler dev
# Netlify CLI
netlify dev
💰 비용 최적화 전략
비용 절감 팁
- 적절한 메모리 할당: 과도한 메모리는 비용 증가
- 콜드 스타트 최소화: 워밍 전략 사용
- 캐싱 활용: CDN, 메모리 캐시
- 배치 처리: 개별 호출보다 효율적
예상 비용 계산
// 월간 비용 예상
const calculateCost = (platform, requests, avgDuration) => {
switch (platform) {
case "lambda":
const gbSeconds = requests * (avgDuration / 1000) * 0.5; // 512MB
return requests * 0.0000002 + gbSeconds * 0.0000166667;
case "vercel":
return requests > 100000 ? (requests - 100000) * 0.000002 : 0;
case "cloudflare":
return requests > 100000 ? (requests - 100000) * 0.0000005 : 0;
}
};
🎯 선택 가이드
AWS Lambda 선택 시
✅ AWS 서비스 통합 필요
✅ 긴 실행 시간 (최대 15분)
✅ 복잡한 워크플로우
✅ 엔터프라이즈 요구사항
❌ 낮은 레이턴시 필요
❌ 간단한 웹 API
Vercel 선택 시
✅ Next.js 프로젝트
✅ 프론트엔드 중심
✅ 빠른 배포
✅ 프리뷰 배포
❌ 긴 실행 시간
❌ 복잡한 백엔드
Cloudflare Workers 선택 시
✅ 글로벌 저지연
✅ 엣지 컴퓨팅
✅ 높은 동시성
✅ WebSocket 지원
❌ 큰 패키지
❌ 파일 시스템
Netlify Functions 선택 시
✅ JAMstack 프로젝트
✅ 정적 사이트 + API
✅ 간단한 백엔드
✅ Git 기반 배포
❌ 복잡한 로직
❌ 대규모 트래픽
📚 추가 리소스
공식 문서
모범 사례
- 콜드 스타트 최적화
- 환경 변수 관리
- 에러 처리 및 재시도
- 모니터링 및 로깅
커뮤니티
- Serverless Stack Discord
- Vercel Community
- Cloudflare Developers Discord
- AWS Serverless Forum
💡 핵심 조언: 서버리스는 "서버가 없다"가 아니라 "서버를 관리하지 않는다"입니다. 간단한 API는 Cloudflare Workers로, Next.js는 Vercel로, 복잡한 워크플로우는 AWS Lambda로 시작하세요. 중요한 것은 벤더 락인을 피하고 필요에 따라 이동할 수 있는 아키텍처를 설계하는 것입니다.