Amazon API Gateway - 서버리스 API의 관문
API Gateway로 확장 가능하고 안전한 REST API와 WebSocket API 구축하기
Amazon API Gateway란?
Amazon API Gateway는 어떤 규모에서든 개발자가 API를 손쉽게 생성, 게시, 유지 관리, 모니터링 및 보안 유지할 수 있도록 하는 완전 관리형 서비스임.
RESTful API, HTTP API, WebSocket API를 생성할 수 있으며, AWS Lambda, EC2, 기타 AWS 서비스나 퍼블릭 웹 서비스와 통합할 수 있음.
왜 API Gateway를 사용해야 하는가?
기존 방식의 문제점
전통적인 API 서버 구축:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────┐
│ Load Balancer (ALB/NLB) │
│ └─ EC2 Auto Scaling Group │
│ ├─ API Server Instance 1 │
│ ├─ API Server Instance 2 │
│ └─ API Server Instance 3 │
│ │
│ 추가 구성 요소: │
│ - Rate Limiting: NGINX/Custom Code │
│ - Authentication: JWT Middleware │
│ - API Keys: Custom DB │
│ - Monitoring: CloudWatch Agent │
│ - CORS: Manual Configuration │
│ - SSL/TLS: Certificate Manager │
└─────────────────────────────────────────┘
월 비용: ~$100+ (최소 2개 인스턴스 + ALB)
문제점:
- 인프라 관리 부담: 로드 밸런서, Auto Scaling, 헬스 체크 설정 필요
- 보안 구현 복잡: Rate limiting, API key, 인증 직접 구현
- 고정 비용: 트래픽이 없어도 서버 비용 발생
- 확장성 제한: 트래픽 패턴 예측 및 수동 스케일링 필요
- 모니터링 복잡: 별도 APM 도구 필요
API Gateway의 해결 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────┐
│ Amazon API Gateway │
│ ├─ Built-in Rate Limiting │
│ ├─ IAM/Cognito/Lambda Authorizers │
│ ├─ API Key Management │
│ ├─ CloudWatch Integration │
│ ├─ Automatic CORS │
│ ├─ SSL/TLS (Automatic) │
│ └─ Auto Scaling (무한 확장) │
│ │
│ → Lambda, EC2, HTTP Endpoint │
└─────────────────────────────────────────┘
월 비용: $0.90 ~ $3.50 per million requests
(트래픽 없으면 $0)
장점:
- 완전 관리형: 인프라 관리 불필요
- 즉각적인 확장: 트래픽에 따라 자동 확장
- 내장 보안: Rate limiting, API key, 다양한 인증 방식 지원
- 종량제 과금: 실제 요청 수에 대해서만 과금
- 통합 모니터링: CloudWatch 자동 통합
API Gateway 타입 비교
REST API vs HTTP API vs WebSocket API
| 특징 | REST API | HTTP API | WebSocket API |
|---|---|---|---|
| 가격 | $3.50/million | $0.90/million | $1.00/million (msg) |
| 지연 시간 | 보통 | 낮음 (60% 빠름) | 실시간 |
| 인증 | IAM, Cognito, Lambda, API Key | IAM, Cognito, Lambda | IAM, Lambda |
| Rate Limiting | ✓ | ✓ | ✓ |
| API Keys | ✓ | ✗ | ✗ |
| Request Validation | ✓ | ✗ | ✗ |
| Caching | ✓ | ✗ | ✗ |
| Private Endpoints | ✓ | ✓ | ✗ |
| 사용 사례 | 복잡한 API | 심플한 프록시 | 실시간 통신 |
선택 기준:
- HTTP API: 간단한 프록시, 낮은 비용, 낮은 지연시간 중요할 때 (추천)
- REST API: API key, request validation, caching 필요할 때
- WebSocket API: 양방향 실시간 통신 (채팅, 게임, IoT)
실전 프로젝트 사례
1. 주문 시스템 HTTP API (aws-order-system)
아키텍처:
1
2
3
4
5
Client
│
├─ POST /orders → Lambda: order-creator → SQS
├─ GET /orders → Lambda: order-query → DynamoDB
└─ GET /orders/{id} → Lambda: order-query → DynamoDB
Serverless Framework 설정:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# serverless.yml
provider:
name: aws
runtime: nodejs20.x
httpApi:
cors:
allowedOrigins:
- '*'
allowedHeaders:
- Content-Type
- X-Amz-Date
- Authorization
allowedMethods:
- GET
- POST
- OPTIONS
functions:
orderCreator:
handler: functions/order-creator.handler
events:
- httpApi:
path: /orders
method: POST
cors: true
environment:
SQS_QUEUE_URL: !Ref OrderQueue
orderQuery:
handler: functions/order-query.handler
events:
- httpApi:
path: /orders
method: GET
- httpApi:
path: /orders/{orderId}
method: GET
environment:
TABLE_NAME: !Ref OrdersTable
Lambda 핸들러 (order-creator.handler):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
const sqsClient = new SQSClient();
const QUEUE_URL = process.env.SQS_QUEUE_URL;
export const handler = async (event) => {
try {
// API Gateway HTTP API event 구조
const body = JSON.parse(event.body);
// 주문 데이터 검증
if (!body.userId || !body.items) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Missing required fields'
})
};
}
const order = {
orderId: `order-${Date.now()}`,
userId: body.userId,
items: body.items,
totalAmount: body.totalAmount,
status: 'pending',
createdAt: new Date().toISOString()
};
// SQS로 비동기 처리
await sqsClient.send(new SendMessageCommand({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify(order)
}));
return {
statusCode: 202,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
message: 'Order accepted',
orderId: order.orderId
})
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
Lambda 핸들러 (order-query.handler):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient();
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME;
export const handler = async (event) => {
try {
const { orderId } = event.pathParameters || {};
// 특정 주문 조회
if (orderId) {
const result = await docClient.send(new GetCommand({
TableName: TABLE_NAME,
Key: { orderId }
}));
if (!result.Item) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Order not found' })
};
}
return {
statusCode: 200,
body: JSON.stringify(result.Item)
};
}
// 전체 주문 조회 (페이지네이션 적용 권장)
const result = await docClient.send(new ScanCommand({
TableName: TABLE_NAME,
Limit: 50
}));
return {
statusCode: 200,
body: JSON.stringify({
orders: result.Items,
count: result.Count
})
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
테스트:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 주문 생성
curl -X POST https://api-id.execute-api.ap-northeast-2.amazonaws.com/orders \
-H "Content-Type: application/json" \
-d '{
"userId": "user123",
"items": [
{"productId": "prod1", "quantity": 2, "price": 10000}
],
"totalAmount": 20000
}'
# 응답
{
"message": "Order accepted",
"orderId": "order-1699876543210"
}
# 주문 조회
curl https://api-id.execute-api.ap-northeast-2.amazonaws.com/orders/order-1699876543210
2. 이미지 처리 REST API (aws-image-pipeline)
아키텍처:
1
2
3
4
5
Client
│
├─ POST /upload → Lambda → S3 Presigned URL 생성
├─ GET /images → Lambda → DynamoDB 조회
└─ DELETE /images → Lambda → S3 삭제 + DynamoDB 삭제
REST API 특징 활용:
- Request Validation: OpenAPI 스키마로 입력 검증
- API Key: 외부 클라이언트 접근 제어
- Caching: 이미지 목록 조회 결과 캐싱 (5분)
Serverless Framework 설정:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
provider:
apiGateway:
# REST API 설정
apiKeys:
- name: image-api-key
usagePlan:
quota:
limit: 5000
period: MONTH
throttle:
burstLimit: 100
rateLimit: 50
functions:
uploadImage:
handler: functions/upload.handler
events:
- http:
path: /upload
method: POST
private: true # API Key 필요
request:
schemas:
application/json: ${file(schemas/upload-request.json)}
Upload 핸들러:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3Client = new S3Client();
const BUCKET_NAME = process.env.BUCKET_NAME;
export const handler = async (event) => {
try {
const { fileName, contentType } = JSON.parse(event.body);
const key = `uploads/${Date.now()}-${fileName}`;
// Presigned URL 생성 (5분 유효)
const command = new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
ContentType: contentType
});
const presignedUrl = await getSignedUrl(s3Client, command, {
expiresIn: 300
});
return {
statusCode: 200,
body: JSON.stringify({
uploadUrl: presignedUrl,
key: key,
expiresIn: 300
})
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Failed to generate upload URL' })
};
}
};
3. 알림 발송 REST API (notification-fanout)
특징:
- Rate Limiting: 초당 50 요청, 버스트 100 요청
- API Key: 서비스별 키 발급
- Usage Plan: 월 5000건 쿼터 제한
API Gateway 콘솔 설정:
1
2
3
4
5
6
7
8
9
10
11
// Rate Limiting 설정 (Terraform/CloudFormation)
{
"throttle": {
"rateLimit": 50, // 초당 50 요청
"burstLimit": 100 // 버스트 100 요청
},
"quota": {
"limit": 5000, // 월 5000 요청
"period": "MONTH"
}
}
HTTP API vs REST API 실전 비교
내 선택 기준
최근 프로젝트들에서는 HTTP API를 주로 사용했음:
HTTP API를 선택한 이유:
- 70% 저렴: $0.90 vs $3.50 per million
- 60% 빠른 응답: 평균 지연시간 감소
- 충분한 기능: 대부분의 유스케이스에 충분
- 간단한 설정: Serverless Framework 통합 쉬움
REST API가 필요했던 경우:
- 이미지 처리 API: Presigned URL 생성 시 request validation 필요
- 외부 파트너 API: API Key로 접근 제어 필요
- 높은 트래픽 조회 API: CloudFront + API Gateway Caching 활용
API Gateway 핵심 기능
1. CORS 설정
HTTP API (자동 설정):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# serverless.yml
provider:
httpApi:
cors:
allowedOrigins:
- 'https://example.com'
allowedHeaders:
- Content-Type
- Authorization
allowedMethods:
- GET
- POST
allowCredentials: true
maxAge: 300
REST API (수동 설정):
1
2
3
4
5
6
7
8
9
10
11
12
functions:
myFunction:
handler: handler.main
events:
- http:
path: /resource
method: POST
cors:
origin: '*'
headers:
- Content-Type
- X-Api-Key
2. 인증/권한 부여
IAM 인증:
1
2
3
4
5
6
7
8
functions:
privateApi:
handler: handler.main
events:
- http:
path: /private
method: GET
private: true # IAM 인증 필요
Lambda Authorizer (Custom 인증):
1
2
3
4
5
6
7
8
9
10
11
12
13
functions:
authorizer:
handler: authorizer.handler
protectedApi:
handler: handler.main
events:
- http:
path: /protected
method: GET
authorizer:
name: authorizer
resultTtlInSeconds: 300
Authorizer Lambda:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const handler = async (event) => {
const token = event.authorizationToken;
// 토큰 검증 로직
const isValid = await validateToken(token);
return {
principalId: 'user123',
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: isValid ? 'Allow' : 'Deny',
Resource: event.methodArn
}]
},
context: {
userId: 'user123',
email: 'user@example.com'
}
};
};
3. Request/Response 변환
Request Mapping Template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
functions:
transform:
handler: handler.main
events:
- http:
path: /transform
method: POST
request:
template:
application/json: |
{
"body": $input.json('$'),
"timestamp": "$context.requestTime"
}
4. CloudWatch 통합
자동 수집 메트릭:
Count: API 호출 수4XXError: 클라이언트 에러 수5XXError: 서버 에러 수Latency: 응답 시간IntegrationLatency: 백엔드 처리 시간
커스텀 액세스 로그:
1
2
3
4
provider:
logs:
httpApi:
format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}'
API Gateway vs 대안 비교
API Gateway vs Application Load Balancer (ALB)
| 항목 | API Gateway | ALB |
|---|---|---|
| 가격 | $0.90~$3.50/million | $0.0225/hour + $0.008/LCU |
| Lambda 통합 | Native 지원 | HTTP 방식 |
| API 관리 | ✓ (Versioning, Stages) | ✗ |
| Rate Limiting | ✓ (내장) | ✗ (WAF 필요) |
| 인증 | IAM, Cognito, Lambda | ✗ (직접 구현) |
| WebSocket | ✓ | ✗ |
| 사용 사례 | Serverless API | 컨테이너/EC2 API |
ALB가 나은 경우:
- ECS/EKS 컨테이너 기반 API
- 기존 EC2 인프라 활용
- 매우 높은 트래픽 (ALB가 저렴)
API Gateway vs Lambda Function URL
| 항목 | API Gateway | Lambda Function URL |
|---|---|---|
| 가격 | $0.90~$3.50/million | 무료 |
| Rate Limiting | ✓ | ✗ |
| API Key | ✓ | ✗ |
| Custom Domain | ✓ | ✗ |
| Caching | ✓ (REST API) | ✗ |
| 인증 | 다양한 옵션 | IAM만 |
| 사용 사례 | 프로덕션 API | 간단한 Webhook |
Function URL이 나은 경우:
- 간단한 webhook
- 내부 서비스 간 통신
- 비용 최소화 중요
성능 최적화 팁
1. API Gateway Caching (REST API만)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
provider:
apiGateway:
cacheClusterEnabled: true
cacheClusterSize: '0.5' # GB
functions:
cachedApi:
handler: handler.main
events:
- http:
path: /cached
method: GET
caching:
enabled: true
ttlInSeconds: 300
cacheKeyParameters:
- name: request.querystring.id
2. Regional vs Edge-Optimized
1
2
3
4
5
provider:
endpointType: REGIONAL # 또는 EDGE
# REGIONAL: 특정 리전 사용자
# EDGE: 글로벌 사용자 (CloudFront 자동 통합)
선택 기준:
- Regional: 한국 사용자 중심, Lambda와 같은 리전
- Edge: 글로벌 사용자, CDN 필요
3. Lambda 프록시 통합 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// BAD: 큰 응답 데이터
export const handler = async (event) => {
const data = await fetchLargeData(); // 10MB
return {
statusCode: 200,
body: JSON.stringify(data) // API Gateway 6MB 제한!
};
};
// GOOD: S3 Presigned URL 반환
export const handler = async (event) => {
const key = await uploadToS3(data);
const presignedUrl = await getSignedUrl(s3Client,
new GetObjectCommand({ Bucket, Key: key }),
{ expiresIn: 3600 }
);
return {
statusCode: 200,
body: JSON.stringify({ downloadUrl: presignedUrl })
};
};
비용 최적화
실제 프로젝트 비용 분석
주문 시스템 (월 100만 요청):
1
2
3
4
5
6
7
8
9
10
11
HTTP API:
- API 요청: 1,000,000 × $0.90 = $0.90
- Lambda 실행: 1,000,000 × 100ms × $0.0000166667 = $16.67
- 총합: $17.57/월
REST API였다면:
- API 요청: 1,000,000 × $3.50 = $3.50
- Lambda 실행: $16.67
- 총합: $20.17/월
차이: $2.60/월 (13% 절감)
대규모 API (월 5000만 요청):
1
2
3
4
5
6
7
8
9
10
11
HTTP API:
- API 요청: 50,000,000 × $0.90 = $45
- Lambda 실행: $833.35
- 총합: $878.35/월
REST API였다면:
- API 요청: 50,000,000 × $3.50 = $175
- Lambda 실행: $833.35
- 총합: $1,008.35/월
차이: $130/월 (13% 절감)
비용 절감 팁
- HTTP API 우선 고려: 70% 저렴
- Caching 활용: 백엔드 호출 감소
- CloudFront 앞단 배치: Static 응답 캐싱
- Lambda 최적화: Cold start 감소, 실행 시간 단축
- API Gateway → Lambda → API Gateway 체인 피하기
실전 경험에서 배운 것
1. HTTP API로 충분하다
처음에는 REST API의 많은 기능이 필요할 것 같았지만, 실제로는 대부분의 경우 HTTP API로 충분했음.
HTTP API로 충분한 케이스:
- 간단한 CRUD API
- Lambda 프록시 통합
- IAM/Cognito 인증
- CORS 설정만 필요
2. Lambda Authorizer는 캐싱하자
1
2
authorizer:
resultTtlInSeconds: 300 # 5분 캐싱
인증 결과를 캐싱하면 매 요청마다 authorizer를 호출하지 않아 비용과 지연시간 감소.
3. API Gateway 제한 사항 인지
- 페이로드 크기: 최대 10MB (HTTP API), 6MB (REST API)
- 타임아웃: 최대 30초 (Lambda), 29초 (HTTP 백엔드)
- Rate Limit: 기본 10,000 req/s (증가 요청 가능)
큰 파일 업로드는 S3 Presigned URL 사용!
4. 단계별 배포 활용
1
2
3
4
5
6
provider:
stage: ${opt:stage, 'dev'}
# 배포
sls deploy --stage dev
sls deploy --stage prod
dev, staging, prod 환경 분리로 안전한 배포 가능.
마무리
API Gateway는 서버리스 아키텍처의 핵심 진입점임. Lambda와의 네이티브 통합, 자동 확장, 종량제 과금으로 서버 관리 없이 확장 가능한 API를 구축할 수 있음.
선택 가이드:
- 간단한 API → HTTP API (추천)
- 복잡한 API 관리 → REST API
- 실시간 양방향 통신 → WebSocket API
- 극단적 저비용 → Lambda Function URL
최근 프로젝트들에서 HTTP API + Lambda 조합으로 빠르고 저렴하게 API를 구축했고, 트래픽 증가에도 자동으로 대응할 수 있어 만족스러웠음.