1. 프롤로그 - "서버 관리 없이 코드만?"
"서버 관리 없이 코드만 올리면 된다?" Serverless라는 개념을 처음 들었을 때 반신반의했다.
EC2를 써본 사람이라면 안다. 인스턴스 타입 선택, CPU/메모리 설정, OS 패치, 오토 스케일링 설정... 코드보다 인프라 관리에 더 시간이 쏟아진다. 서버가 죽으면 직접 재부팅하고, 로그 뒤지고, 원인 찾아야 한다. 그게 싫어서 Serverless를 파보게 됐다.
2. 처음엔 이해가 안 갔던 "서버리스"
이름에 속았습니다
"Serverless"라는 단어를 처음 들었을 때, 저는 완전히 잘못 이해했습니다. "서버가 없다고? 그럼 코드가 어디서 돌아가는데?" 라고 혼란스러웠죠. 나중에 알고 보니 "서버가 없다"가 아니라 "내가 서버를 관리하지 않는다"는 뜻이었습니다. 서버는 분명히 존재합니다. 다만 그 서버의 존재를 개발자가 의식하지 않아도 된다는 게 핵심이었습니다.
이 개념을 받아들이기까지 꽤 시간이 걸렸습니다. EC2를 써본 사람이라면 알겠지만, 우리는 항상 "인스턴스 타입", "CPU 코어 수", "메모리 용량", "OS 패치" 같은 것들을 신경 써야 했습니다. 근데 서버리스는 그런 걸 전부 AWS한테 맡깁니다. 개발자는 오직 "내 코드가 뭘 하는가"만 고민하면 됩니다.
비유 - 자가용(EC2) vs 택시(Lambda)
이 개념이 확 와닿았던 건, 누군가가 해준 이 비유 덕분이었습니다.
- EC2 (IaaS): 자가용을 샀습니다. 안 타도 보험료, 세금 나갑니다. 엔진 오일 갈아야(패치 관리) 합니다. 주차 공간도 필요하고(고정 IP), 차가 고장 나면(서버 다운) 직접 정비소에 가야 합니다.
- Lambda (FaaS): 택시(우버)입니다. 탈 때만 돈 냅니다. 정비는 기사님(AWS)이 알아서 합니다. 1,000명이 동시에 타면? 택시 1,000대가 옵니다. 안 타면 0원입니다.
이 비유를 듣자마자 무릎을 쳤습니다. "아, 그래서 트래픽이 들쭉날쭉한 서비스에 유리하구나." 매일 아침 9시에만 사용자가 몰리는 서비스라면, EC2는 24시간 내내 비싼 인스턴스를 켜놓고 있어야 하지만, Lambda는 9시에만 함수가 실행되고 나머지 시간엔 0원입니다.
3. 핵심 아키텍처 - FaaS와 이벤트 기반 설계
서버리스의 핵심은 FaaS (Function as a Service)입니다. 거대한 모놀리식 서버를 띄워놓는 게 아니라, 작은 함수(Function) 하나하나를 클라우드에 올립니다.
동작 원리 (이벤트 기반)
함수는 가만히 있다가, 이벤트(Event)가 발생하면 깨어납니다. 마치 소방관처럼 평소엔 대기하다가 화재 신고가 들어오면 출동하는 겁니다.
- HTTP 요청: 사용자가 API 호출 → API Gateway가 함수 깨움.
- DB 변경: DynamoDB에 데이터 저장됨 → Stream이 함수 깨움.
- 파일 업로드: S3에 사진 올라옴 → 함수 깨움.
- 시간: 매일 밤 12시(Cron) → 함수 깨움.
"항상 켜져 있는 서버는 없다. 사건이 터지면 그제야 실행되고, 할 일 끝나면 즉시 사라진다."
이 철학을 이해했을 때, 제가 왜 서버 모니터링에 시달렸는지 알게 됐습니다. EC2는 항상 떠 있기 때문에 "죽었는지 살았는지" 감시해야 했지만, Lambda는 필요할 때만 태어나고 즉시 사라지니까 모니터링할 게 없습니다.
실제 서비스 구성 예시: API Gateway + Lambda + DynamoDB
예를 들어 회원 가입 API를 만든다고 해보자. 처음엔 Express.js 서버를 EC2에 올리는 게 자연스러운 선택이다. 근데 가입 API는 하루에 몇 건 안 들어오는데, 24시간 서버를 켜놓으면 너무 비효율적이다. 이걸 Lambda로 바꾸면 이런 구조가 된다.
// Lambda Function: 회원 가입 처리
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
const body = JSON.parse(event.body);
const { email, password, name } = body;
// 1. 이메일 중복 체크
const existingUser = await dynamodb.get({
TableName: 'Users',
Key: { email }
}).promise();
if (existingUser.Item) {
return {
statusCode: 400,
body: JSON.stringify({ error: '이미 존재하는 이메일입니다.' })
};
}
// 2. 비밀번호 해싱 (bcrypt)
const hashedPassword = await bcrypt.hash(password, 10);
// 3. DynamoDB에 저장
await dynamodb.put({
TableName: 'Users',
Item: {
email,
password: hashedPassword,
name,
createdAt: new Date().toISOString()
}
}).promise();
return {
statusCode: 201,
body: JSON.stringify({ message: '회원가입 성공' })
};
};
API Gateway 연결:
POST /signup → Lambda (signup 함수) → DynamoDB
트래픽이 적은 API에서는 이런 전환으로 비용이 크게 줄어든다고 한다. 월 $50 → $2 수준의 절감도 가능하다는 사례가 있다. 트래픽이 불규칙하고 적은 API에는 서버리스가 유리하다.
4. 실제 Lab - 썸네일 자동 생성기
가장 클래식한 서버리스 예제입니다. 이걸 직접 만들어보면서 "아, 이게 서버리스구나" 하고 체감했습니다.
시나리오
사용자가 프로필 사진을 올리면 자동으로 작은 썸네일을 만들어야 합니다.
전통적인 방식 (EC2)
- 24시간 이미지 처리 서버를 켜둬야 함.
- 사용자가 없을 때도 돈 나감.
- 갑자기 1만 명이 동시 업로드하면 서버 터짐.
서버리스 방식 (Lambda + S3)
Code (Node.js):
const sharp = require('sharp');
const aws = require('aws-sdk');
const s3 = new aws.S3();
exports.handler = async (event) => {
// 1. 이벤트에서 파일 정보 추출
const bucket = event.Records[0].s3.bucket.name;
const key = event.Records[0].s3.object.key;
// 2. S3에서 원본 다운로드
const image = await s3.getObject({ Bucket: bucket, Key: key }).promise();
// 3. 리사이징 (메모리상에서 처리)
const resized = await sharp(image.Body).resize(200, 200).toBuffer();
// 4. 다시 S3에 저장
await s3.putObject({
Bucket: bucket,
Key: `thumbnails/${key}`,
Body: resized
}).promise();
};
결과:
- 비용: 이미지 1장당 약 0.0002원. (업로드 안 하면 0원).
- 확장성: 사용자 100만 명이 동시에 올려도 AWS가 알아서 Lambda 100만 개 띄워서 처리. (병렬 처리).
이 코드를 처음 배포했을 때, 신기했던 게 서버를 띄운 적이 없다는 겁니다. 그냥 코드만 올렸는데, S3에 파일이 올라오면 자동으로 실행됐습니다. "아, 이게 이벤트 기반이구나" 하고 받아들였습니다.
5. 배포 자동화: SAM vs Serverless Framework
처음엔 AWS Console에서 손으로 Lambda 함수를 만들었습니다. 근데 함수가 10개, 20개로 늘어나니까 지옥이었습니다. 그래서 Infrastructure as Code (IaC)를 도입했습니다.
AWS SAM (Serverless Application Model)
AWS 공식 도구입니다. CloudFormation의 확장판이라고 생각하면 됩니다.
template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
ThumbnailFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
MemorySize: 512
Timeout: 60
Events:
S3Event:
Type: S3
Properties:
Bucket: !Ref ImageBucket
Events: s3:ObjectCreated:*
Filter:
S3Key:
Rules:
- Name: prefix
Value: uploads/
ImageBucket:
Type: AWS::S3::Bucket
배포:
sam build
sam deploy --guided
SAM을 쓰면서 정리해본 장점은 이겁니다.
- Lambda, API Gateway, DynamoDB를 하나의 YAML 파일로 관리.
- 로컬에서
sam local start-api로 테스트 가능. - CloudFormation 기반이라 AWS와 찰떡궁합.
Serverless Framework (서드파티)
더 간결하고 멀티 클라우드를 지원합니다. (AWS, GCP, Azure 모두 가능).
serverless.yml:
service: thumbnail-service
provider:
name: aws
runtime: nodejs18.x
region: ap-northeast-2
functions:
thumbnail:
handler: handler.thumbnail
events:
- s3:
bucket: my-image-bucket
event: s3:ObjectCreated:*
rules:
- prefix: uploads/
resources:
Resources:
ImageBucket:
Type: AWS::S3::Bucket
배포:
serverless deploy
저는 처음엔 SAM을 썼다가, 멀티 클라우드 지원이 필요해서 Serverless Framework로 갈아탔습니다. 둘 다 좋습니다. AWS만 쓴다면 SAM, 멀티 클라우드 고려하면 Serverless Framework가 와닿았습니다.
6. 치명적 단점: Cold Start
세상에 공짜는 없습니다. 서버리스의 최대 약점은 Cold Start입니다. 이 문제 때문에 한동안 머리를 쥐어뜯었습니다.
Cold Start란?
함수가 오랫동안 실행되지 않으면, AWS는 리소스 절약을 위해 컨테이너를 꺼버립니다(Freeze). 이 상태에서 요청이 오면:
- 새 컨테이너 준비 (부팅)
- 코드 다운로드
- 런타임(Node, Python) 초기화
- 코드 실행
이 과정이 0.5초 ~ 3초 정도 걸립니다. 사용자는 클릭하고 3초간 멍하니 있어야 합니다.
로그인 API를 Lambda로 만들면 새벽에 첫 로그인을 시도하는 사용자가 3초를 기다려야 한다. "서버 고장 났나?" 하고 새로고침을 누르게 되는 상황이다. Cold Start가 UX에 직접 영향을 준다는 걸 이해하는 순간이다.
해결책 1 - Provisioned Concurrency (유료)
"항상 1개는 켜놔"라고 돈 더 내고 설정합니다. EC2보다는 싸지만 0원은 아닙니다.
aws lambda put-provisioned-concurrency-config \
--function-name my-function \
--provisioned-concurrent-executions 1
이렇게 하면 항상 1개의 컨테이너가 warm 상태로 대기합니다. 비용은 시간당 약 $0.015 정도입니다. (월 $10 정도).
해결책 2 - Keep-Alive 핑 (무료)
5분마다 한 번씩 함수를 찔러주는 봇을 돌려서 안 재우기.
// CloudWatch Events (EventBridge)로 5분마다 실행
exports.handler = async () => {
console.log('Keep-alive ping');
return 'OK';
};
이 방법은 무료지만, 완벽하진 않습니다. 컨테이너가 여러 개 떠 있으면 일부는 여전히 cold 상태일 수 있습니다.
해결책 3 - 언어 선택
Java는 JVM 로딩 때문에 매우 느립니다. Node.js나 Go, Python이 Cold Start에 유리합니다.
| 언어 | Cold Start 평균 |
|---|---|
| Node.js | ~200ms |
| Python | ~250ms |
| Go | ~150ms |
| Java | ~2000ms |
저는 이 표를 보고 Java를 포기하고 Node.js로 갈아탔습니다. 결국 언어 선택도 비용이구나 하고 이해했습니다.
7. Stateless (비저장) 원칙
Lambda 함수는 실행이 끝나면 메모리의 모든 것이 사라집니다. 이걸 모르고 처음에 엄청 삽질했습니다.
Bad (제가 실제로 짠 코드):
let count = 0;
exports.handler = async () => {
count++; // 기대: 1, 2, 3... 실제: 매번 1일 수도 있음 (새 컨테이너면 0부터)
return count;
};
이 코드를 배포하고 테스트했을 때, 첫 요청은 1이 나왔고, 두 번째도 1이 나왔습니다. "뭐지?" 하고 당황했는데, 알고 보니 두 번째 요청은 새 컨테이너에서 실행된 거였습니다. Lambda는 매번 같은 컨테이너를 쓰지 않습니다.
Good (DynamoDB에 저장):
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async () => {
// 1. 현재 count 읽기
const result = await dynamodb.get({
TableName: 'Counter',
Key: { id: 'global' }
}).promise();
const currentCount = result.Item ? result.Item.count : 0;
// 2. +1 증가
await dynamodb.put({
TableName: 'Counter',
Item: { id: 'global', count: currentCount + 1 }
}).promise();
return currentCount + 1;
};
이제 매번 올바른 숫자가 나옵니다. 상태는 반드시 외부 저장소(DynamoDB, Redis, S3)에 저장해야 한다는 걸 뼈저리게 받아들였습니다.
8. 복잡한 워크플로우 - Step Functions로 오케스트레이션
함수 하나하나는 간단하지만, 여러 함수를 연결하려면 복잡해집니다. 예를 들어 "주문 처리" 워크플로우를 생각해봤다.
- 결제 검증 (Lambda 1)
- 재고 확인 (Lambda 2)
- 배송 요청 (Lambda 3)
- 이메일 발송 (Lambda 4)
이걸 어떻게 연결할까요? 처음엔 Lambda 1에서 Lambda 2를 호출하고, Lambda 2에서 Lambda 3를 호출하고... 이런 식으로 짰습니다. 근데 이러면 에러 핸들링이 지옥입니다. Lambda 2에서 에러가 나면 어떻게 롤백할까요?
그래서 AWS Step Functions를 도입했습니다.
state machine 정의:
{
"Comment": "주문 처리 워크플로우",
"StartAt": "ValidatePayment",
"States": {
"ValidatePayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:validate-payment",
"Next": "CheckInventory",
"Catch": [{
"ErrorEquals": ["PaymentError"],
"Next": "PaymentFailed"
}]
},
"CheckInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:check-inventory",
"Next": "RequestShipping",
"Catch": [{
"ErrorEquals": ["OutOfStock"],
"Next": "RefundPayment"
}]
},
"RequestShipping": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:request-shipping",
"Next": "SendEmail"
},
"SendEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:send-email",
"End": true
},
"PaymentFailed": {
"Type": "Fail",
"Error": "PaymentError",
"Cause": "결제 검증 실패"
},
"RefundPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-northeast-2:123456789012:function:refund",
"Next": "OrderFailed"
},
"OrderFailed": {
"Type": "Fail",
"Error": "OrderError",
"Cause": "주문 처리 실패"
}
}
}
Step Functions를 쓰면 시각적으로 워크플로우를 볼 수 있고, 에러 발생 시 자동으로 롤백 로직을 태울 수 있습니다. 이걸 쓰면서 "아, 서버리스도 복잡한 로직을 다룰 수 있구나" 하고 정리해본 게 이겁니다.
9. 비용 폭탄의 위험 (DDoS)
서버리스는 "쓴 만큼 낸다"가 장점이지만, "쓰는 대로 다 낸다"가 단점이기도 합니다. 이 문제로 실제로 청구서 폭탄을 맞을 뻔했습니다.
실제 사례 - 봇이 API를 무한 호출
실제로 이런 사례가 알려져 있다. 봇이 API를 초당 1,000번씩 호출하자, Lambda는 충실히 1,000개의 함수를 띄웠고 하루 만에 $500이 청구됐다. (평소 월 $10 정도 나오던 서비스였다고 한다.)
EC2였다면 서버가 뻗고 끝났을 텐데, Lambda는 "요청이 있으니 처리해야지!" 하고 무한정 확장했다. 이게 서버리스의 또 다른 얼굴이다.
방어: API Gateway Throttling
API Gateway에서 속도 제한(Throttling)을 반드시 걸어야 합니다.
# serverless.yml
provider:
apiGateway:
throttle:
rateLimit: 100 # 초당 최대 100 요청
burstLimit: 200 # 버스트 최대 200 요청
이렇게 설정하고 나니 봇이 아무리 호출해도 초당 100개까지만 처리되고 나머지는 429 Too Many Requests를 받았습니다. 청구서도 정상으로 돌아왔습니다.
결국 이거였습니다. 서버리스는 무한 확장이 가능하지만, 그만큼 무한 과금도 가능하다. 반드시 제한을 걸어야 합니다.
10. 비용 비교 - EC2 vs Lambda (실제 케이스)
"이미지 리사이징 API"를 EC2와 Lambda로 각각 운영했을 때 비용을 비교하면 이렇다.
트래픽 패턴
- 평소: 하루 1,000건
- 마케팅 이벤트 시: 하루 100,000건 (한 달에 3일)
EC2 비용 (t3.small, $0.023/시간)
- 기본 비용: $0.023 × 24시간 × 30일 = $16.56/월
- 이벤트 대응: 인스턴스 10개로 스케일업 필요 → 3일간 $11.04 추가
- 총 비용: $27.60/월
Lambda 비용
- 평소: 1,000건 × 27일 = 27,000건
- 이벤트: 100,000건 × 3일 = 300,000건
- 총 요청: 327,000건
- 비용: (327,000 - 무료 100만건) = 0건 (무료 한도 내)
- 실행 시간: 200ms 평균, 512MB 메모리
- 총 비용: $0.80/월
이런 트래픽 패턴이라면 Lambda가 압도적으로 유리하다. 트래픽이 불규칙하고 피크가 몰리는 서비스일수록 서버리스 전환의 효과가 크다.
11. Lambda@Edge: CDN에서 함수 실행
마지막으로 소개하고 싶은 게 Lambda@Edge입니다. 이건 CloudFront(CDN) 엣지 로케이션에서 Lambda를 실행하는 겁니다.
유즈 케이스 - A/B 테스트
사용자가 웹사이트에 접속할 때, 50%는 A 버전, 50%는 B 버전을 보여주고 싶었습니다.
Lambda@Edge 코드:
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// 쿠키에 variant가 없으면 랜덤 할당
if (!headers.cookie || !headers.cookie[0].value.includes('variant=')) {
const variant = Math.random() < 0.5 ? 'A' : 'B';
request.headers['x-variant'] = [{ key: 'X-Variant', value: variant }];
}
return request;
};
이 코드를 CloudFront에 연결하면, 오리진 서버에 도달하기 전에 엣지에서 처리됩니다. 레이턴시가 거의 없습니다.
와닿았던 건, "Lambda가 중앙 리전에만 있는 게 아니라 전 세계에 뿌려질 수 있구나" 하는 겁니다.
12. 마무리 - 서버리스는 은탄환이 아니다
서버리스를 공부하고 직접 코드를 짜보면서 정리해본 결론은 이겁니다.
서버리스가 유리한 경우:
- 트래픽이 불규칙한 서비스 (이벤트 티켓팅, 마케팅 캠페인)
- 백그라운드 작업 (이미지 처리, 데이터 ETL)
- 마이크로서비스 아키텍처
- 빠른 프로토타입 개발
EC2가 유리한 경우:
- 항상 일정한 트래픽 (넷플릭스 스트리밍)
- WebSocket 같은 Long-running connection
- 15분 이상 걸리는 작업
- Cold Start가 치명적인 실시간 서비스 (게임 서버)
하이브리드 접근이 현실적이다. API는 Lambda, WebSocket 채팅은 EC2, 데이터 처리는 Lambda, ML 학습은 EC2. 이렇게 섞어 쓰는 구조가 비용과 성능 양쪽에서 유리하다고 알려져 있다.
결국 이거였습니다. "모든 걸 서버리스로" 할 필요는 없다. 도구는 적재적소에 써야 한다.
13. 용어 사전 (Glossary)
- FaaS (Function as a Service): 함수 단위로 배포하고 실행하는 클라우드 서비스 모델. (AWS Lambda, Google Cloud Functions).
- BaaS (Backend as a Service): 백엔드 기능(DB, Auth 등)을 API로 제공하는 서비스. (Firebase, Supabase).
- Cold Start: 꺼져있던 함수가 처음 실행될 때 초기화 때문에 지연되는 현상.
- Vendor Lock-in: 특정 클라우드(AWS)의 기능에 너무 깊게 의존해서, 다른 클라우드(Azure, GCP)로 이사 가기 힘든 상태.
- Provisioned Concurrency: Cold Start를 막기 위해 미리 컨테이너를 데워놓는 기능 (유료).
- Idempotency (멱등성): 같은 요청을 여러 번 보내도 결과가 같아야 함. (네트워크 오류로 Lambda가 두 번 실행될 수 있으므로 중요).
- Step Functions: 여러 Lambda 함수를 워크플로우로 연결하는 오케스트레이션 서비스.
- Lambda@Edge: CloudFront 엣지 로케이션에서 실행되는 Lambda 함수.
14. FAQ
- Q: 넷플릭스는 서버리스를 쓰나요?
- A: 네, 하지만 "접착제(Glue)" 용도로 많이 씁니다. 동영상 인코딩이나 메인 스트리밍 서버는 EC2를 쓰고, 파일 관리, 로그 처리, 백업 같은 보조 업무에 Lambda를 적극 활용합니다. 모든 걸 서버리스로 할 필요는 없습니다. 하이브리드가 정답입니다.
- Q: 15분 제한은 뭔가요?
- A: AWS Lambda는 1회 실행 시간이 최대 15분입니다. 15분이 넘어가면 강제 종료됩니다. 그래서 긴 동영상 변환이나 딥러닝 학습에는 부적합합니다. (AWS Batch나 EC2 써야 함).
- Q: Lambda에서 Docker 컨테이너를 쓸 수 있나요?
- A: 네, 2020년부터 Container Image Support가 추가됐습니다. 최대 10GB 이미지까지 올릴 수 있습니다. 복잡한 의존성(ML 라이브러리 등)이 있을 때 유용합니다.