Amazon DynamoDB - 완전 관리형 NoSQL 데이터베이스
DynamoDB의 핵심 개념부터 실전 활용까지, 프로젝트 경험 기반 완벽 가이드
Amazon DynamoDB란?
Amazon DynamoDB는 AWS에서 제공하는 완전 관리형 NoSQL 데이터베이스 서비스임.
서버리스 아키텍처로 설계되어 프로비저닝, 패치, 백업을 자동으로 처리하며, 밀리초 단위의 일관된 성능과 사실상 무제한 확장성을 제공함.
왜 DynamoDB를 사용해야 하는가?
기존 관계형 데이터베이스의 한계
전통적인 RDS(MySQL, PostgreSQL) 방식:
1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────┐
│ RDS 인스턴스 (db.t3.medium) │
│ - 월 비용: ~$100 │
│ - 스토리지: 100GB ($10/월 추가) │
│ - IOPS: 3000 ($90/월 추가) │
│ - 확장: 수동 스케일업 필요 │
│ - 다운타임: 버전 업그레이드 시 발생 │
└─────────────────────────────────────┘
총 비용: ~$200/월
문제점:
- 고정 용량: 미리 인스턴스 크기와 스토리지 결정 필요
- 스케일링 복잡도: 읽기 복제본, 샤딩 직접 구현
- 관리 부담: OS 패치, 백업, 장애 조치 설정
- 성능 예측 어려움: 트래픽 급증 시 응답 시간 증가
- 비용 최적화 어려움: 사용하지 않을 때도 인스턴스 실행
DynamoDB의 해결 방법
1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────┐
│ DynamoDB (On-Demand) │
│ - 월 비용: ~$25 (동일 부하 가정) │
│ - 스토리지: 사용한 만큼 ($0.25/GB) │
│ - 처리량: 자동 스케일링 │
│ - 확장: 완전 자동 │
│ - 다운타임: 없음 │
└─────────────────────────────────────┘
총 비용: ~$25/월 (87% 절감)
장점:
- 서버리스: 인프라 관리 완전 자동화
- 자동 스케일링: 트래픽에 따라 자동 확장/축소
- 일관된 성능: 어떤 규모에서도 밀리초 단위 응답
- 고가용성: 3개 가용 영역에 자동 복제
- 종량제 과금: 사용한 만큼만 지불
DynamoDB 핵심 개념
1. 테이블, 아이템, 속성
계층 구조:
1
2
3
4
5
6
7
테이블 (Table)
├─ 아이템 (Item) = 행(Row)
│ ├─ 파티션 키 (Partition Key) = 필수
│ ├─ 정렬 키 (Sort Key) = 선택
│ └─ 속성 (Attributes) = 자유롭게 추가 가능
└─ 아이템 (Item)
└─ ...
RDS vs DynamoDB 비교:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RDS (관계형):
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
age INT
);
# 모든 컬럼을 미리 정의해야 함
DynamoDB (NoSQL):
{
"userId": "user-123", // 파티션 키만 필수
"name": "김민국", // 나머지는 자유
"email": "kim@example.com",
"preferences": { // 중첩 객체도 가능
"theme": "dark",
"language": "ko"
}
}
# 스키마 유연함, 각 아이템마다 다른 속성 가능
2. 파티션 키 (Partition Key)
DynamoDB의 가장 중요한 개념. 데이터가 저장될 파티션을 결정하는 키.
동작 원리:
1
2
3
파티션 키 "user-123" → 해시 함수 → 파티션 7번에 저장
파티션 키 "user-456" → 해시 함수 → 파티션 2번에 저장
파티션 키 "user-789" → 해시 함수 → 파티션 5번에 저장
파티션 키 선택 기준:
- 고유성: 값이 고르게 분산되어야 함
- 쿼리 패턴: 자주 조회하는 속성
- 핫 파티션 회피: 특정 키에 트래픽 집중 방지
좋은 예시:
1
2
3
4
// ✅ 좋은 파티션 키 - 고르게 분산됨
{ userId: "user-123" } // 사용자별 고유
{ orderId: "order-20250109-456" } // 주문별 고유
{ deviceId: "sensor-001" } // 디바이스별 고유
나쁜 예시:
1
2
3
4
// ❌ 나쁜 파티션 키 - 핫 파티션 발생
{ status: "active" } // 대부분 active → 한 파티션에 몰림
{ date: "2025-01-09" } // 오늘 데이터가 한 파티션에 집중
{ country: "KR" } // 한국 사용자가 대부분 → 불균형
3. 정렬 키 (Sort Key)
파티션 키와 함께 사용하여 복합 기본 키를 구성. 같은 파티션 내에서 데이터를 정렬함.
사용 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 파티션 키: userId, 정렬 키: timestamp
테이블: UserActivityLogs
{
userId: "user-123", // 파티션 키
timestamp: 1704772800000, // 정렬 키
action: "login",
ipAddress: "203.0.113.1"
}
{
userId: "user-123", // 같은 파티션
timestamp: 1704773400000, // 정렬 키로 정렬됨
action: "purchase",
amount: 50000
}
// 쿼리: user-123의 최근 활동 10개
Query({
KeyConditionExpression: 'userId = :userId',
ScanIndexForward: false, // 내림차순 (최신순)
Limit: 10
})
정렬 키의 강력함:
1
2
3
4
5
// 범위 쿼리 가능
KeyConditionExpression: 'userId = :userId AND timestamp BETWEEN :start AND :end'
// 시작 문자 검색
KeyConditionExpression: 'userId = :userId AND begins_with(productName, :prefix)'
4. 글로벌 보조 인덱스 (GSI)
테이블과 다른 파티션 키/정렬 키로 쿼리할 수 있게 해주는 인덱스.
실전 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 기본 테이블: Orders
파티션 키: orderId
정렬 키: (없음)
{
orderId: "order-001",
customerId: "user-123",
status: "completed",
createdAt: "2025-01-09T10:00:00Z",
totalAmount: 50000
}
// GSI: StatusIndex
파티션 키: status
정렬 키: createdAt
// 이제 status로 쿼리 가능!
Query({
IndexName: 'StatusIndex',
KeyConditionExpression: 'status = :status AND createdAt > :date'
})
# 특정 상태의 주문을 날짜별로 조회
GSI vs 로컬 보조 인덱스 (LSI):
| 특성 | GSI | LSI |
|---|---|---|
| 파티션 키 | 다른 키 사용 가능 | 테이블과 동일해야 함 |
| 정렬 키 | 자유롭게 선택 | 테이블과 달라야 함 |
| 생성 시점 | 언제든지 추가/삭제 | 테이블 생성 시에만 |
| 프로비전 | 별도 RCU/WCU | 테이블과 공유 |
| 최대 개수 | 20개 | 5개 |
실전 프로젝트 활용 사례
사례 1: 주문 관리 시스템
테이블 설계:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Orders 테이블
{
orderId: "order-20250109-001", // 파티션 키
customerId: "user-123",
items: [
{ productId: "prod-1", quantity: 2, price: 10000 },
{ productId: "prod-2", quantity: 1, price: 30000 }
],
totalAmount: 50000,
status: "pending",
createdAt: "2025-01-09T10:00:00Z",
processedAt: null
}
// GSI: StatusIndex (status + createdAt)
// 용도: 특정 상태의 주문을 시간순으로 조회
Document Client 사용 (권장):
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
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
// 일반 클라이언트를 Document Client로 래핑
const dynamoClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(dynamoClient);
// 주문 저장
async function createOrder(order) {
await docClient.send(new PutCommand({
TableName: 'Orders',
Item: {
orderId: order.orderId,
customerId: order.customerId,
items: order.items,
totalAmount: order.totalAmount,
status: 'pending',
createdAt: new Date().toISOString()
}
}));
}
// 주문 조회
async function getOrder(orderId) {
const response = await docClient.send(new GetCommand({
TableName: 'Orders',
Key: { orderId }
}));
return response.Item;
}
// 상태별 주문 조회
async function getOrdersByStatus(status, limit = 50) {
const response = await docClient.send(new QueryCommand({
TableName: 'Orders',
IndexName: 'StatusIndex',
KeyConditionExpression: '#status = :status',
ExpressionAttributeNames: {
'#status': 'status' // status는 예약어라 # 필요
},
ExpressionAttributeValues: {
':status': status
},
ScanIndexForward: false, // 최신순
Limit: limit
}));
return response.Items;
}
주문 상태 업데이트 (조건부 쓰기):
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
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
async function processOrder(orderId, paymentInfo) {
try {
await docClient.send(new UpdateCommand({
TableName: 'Orders',
Key: { orderId },
UpdateExpression: 'SET #status = :newStatus, processedAt = :now, paymentInfo = :payment',
ConditionExpression: '#status = :pending', // pending 상태일 때만 업데이트
ExpressionAttributeNames: {
'#status': 'status'
},
ExpressionAttributeValues: {
':newStatus': 'completed',
':pending': 'pending',
':now': new Date().toISOString(),
':payment': paymentInfo
}
}));
console.log('Order processed successfully');
} catch (error) {
if (error.name === 'ConditionalCheckFailedException') {
console.error('Order is not in pending status');
}
throw error;
}
}
사례 2: IoT 센서 데이터 저장
시계열 데이터 최적화:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SensorData 테이블
{
deviceId: "sensor-001", // 파티션 키
timestamp: 1704772800000, // 정렬 키 (Unix timestamp)
temperature: 24.5,
humidity: 62.3,
metadata: {
battery: 87,
signalStrength: -45
},
ttl: 1707364800 // 30일 후 자동 삭제 (TTL)
}
// Aggregation 테이블 (1분 단위 집계)
{
deviceId: "sensor-001", // 파티션 키
aggregationPeriod: "2025-01-09T10:15:00Z", // 정렬 키 (1분 단위)
count: 12, // 1분간 측정 횟수
sumTemperature: 294.6,
minTemperature: 24.1,
maxTemperature: 24.9,
avgTemperature: 24.55
}
원자적 집계 업데이트:
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
async function updateAggregation(deviceId, timestamp, temperature) {
// 타임스탬프를 1분 단위로 정규화
const period = new Date(Math.floor(timestamp / 60000) * 60000).toISOString();
await docClient.send(new UpdateCommand({
TableName: 'SensorAggregations',
Key: { deviceId, aggregationPeriod: period },
UpdateExpression: `
SET #count = if_not_exists(#count, :zero) + :one,
sumTemperature = if_not_exists(sumTemperature, :zero) + :temp,
minTemperature = if_not_exists(minTemperature, :temp),
maxTemperature = if_not_exists(maxTemperature, :temp),
lastUpdated = :now
`,
ExpressionAttributeNames: {
'#count': 'count'
},
ExpressionAttributeValues: {
':zero': 0,
':one': 1,
':temp': temperature,
':now': new Date().toISOString()
}
}));
}
// 여러 Lambda가 동시에 호출해도 안전하게 집계됨
TTL을 통한 자동 삭제:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Serverless Framework 설정
resources:
Resources:
SensorDataTable:
Type: AWS::DynamoDB::Table
Properties:
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
// 데이터 저장 시 TTL 설정
const ttl = Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60); // 30일 후
await docClient.send(new PutCommand({
TableName: 'SensorData',
Item: {
deviceId,
timestamp,
temperature,
humidity,
ttl // DynamoDB가 자동으로 삭제
}
}));
사례 3: 알림 이력 관리
복합 키 활용:
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
// NotificationHistory 테이블
{
notificationId: "notif-123", // 파티션 키
timestamp: 1704772800000, // 정렬 키
title: "주문 완료",
message: "주문이 성공적으로 처리되었습니다",
channels: ["email", "slack"],
status: "PUBLISHED",
emailStatus: "SENT",
emailSentAt: "2025-01-09T10:05:00Z",
slackStatus: "SENT",
slackSentAt: "2025-01-09T10:05:01Z"
}
// 각 채널별 Lambda가 상태 업데이트
async function updateChannelStatus(notificationId, timestamp, channel, status, sentAt) {
const updateExpression = `SET ${channel}Status = :status, ${channel}SentAt = :sentAt`;
await docClient.send(new UpdateCommand({
TableName: 'NotificationHistory',
Key: { notificationId, timestamp },
UpdateExpression: updateExpression,
ExpressionAttributeValues: {
':status': status,
':sentAt': sentAt
}
}));
}
DynamoDB Streams를 통한 CDC (Change Data Capture)
사용 사례: 주문 상태 변경 시 알림 전송
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Serverless Framework
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES # 변경 전후 데이터 모두 포함
functions:
streamProcessor:
handler: handler.processStream
events:
- stream:
type: dynamodb
arn: !GetAtt OrdersTable.StreamArn
batchSize: 100
startingPosition: LATEST
Stream 이벤트 처리:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const handler = async (event) => {
for (const record of event.Records) {
if (record.eventName === 'MODIFY') {
const oldImage = record.dynamodb.OldImage;
const newImage = record.dynamodb.NewImage;
// 상태가 pending → completed로 변경된 경우
if (oldImage.status.S === 'pending' && newImage.status.S === 'completed') {
const orderId = newImage.orderId.S;
const customerId = newImage.customerId.S;
// SNS로 알림 전송
await sendNotification(customerId, `주문 ${orderId}이 완료되었습니다`);
}
}
}
};
StreamViewType 옵션:
KEYS_ONLY: 키만 포함NEW_IMAGE: 변경 후 데이터OLD_IMAGE: 변경 전 데이터NEW_AND_OLD_IMAGES: 둘 다 포함 (권장)
배치 작업 및 트랜잭션
1. BatchWrite (최대 25개)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { BatchWriteCommand } from '@aws-sdk/lib-dynamodb';
async function batchWriteOrders(orders) {
// 25개씩 나누기
const chunks = [];
for (let i = 0; i < orders.length; i += 25) {
chunks.push(orders.slice(i, i + 25));
}
for (const chunk of chunks) {
const requests = chunk.map(order => ({
PutRequest: { Item: order }
}));
await docClient.send(new BatchWriteCommand({
RequestItems: {
'Orders': requests
}
}));
}
}
2. TransactWrite (ACID 트랜잭션)
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
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
async function transferBalance(fromUserId, toUserId, amount) {
await docClient.send(new TransactWriteCommand({
TransactItems: [
{
// 출금 계좌 잔액 감소
Update: {
TableName: 'Accounts',
Key: { userId: fromUserId },
UpdateExpression: 'SET balance = balance - :amount',
ConditionExpression: 'balance >= :amount', // 잔액 충분한지 확인
ExpressionAttributeValues: { ':amount': amount }
}
},
{
// 입금 계좌 잔액 증가
Update: {
TableName: 'Accounts',
Key: { userId: toUserId },
UpdateExpression: 'SET balance = balance + :amount',
ExpressionAttributeValues: { ':amount': amount }
}
}
]
}));
}
// 둘 중 하나라도 실패하면 전체 롤백 (All-or-Nothing)
용량 모드: Provisioned vs On-Demand
Provisioned 모드
고정 처리량을 미리 할당:
1
2
3
4
5
6
7
8
9
resources:
Resources:
MyTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PROVISIONED
ProvisionedThroughput:
ReadCapacityUnits: 5 # 최대 5 read/초
WriteCapacityUnits: 5 # 최대 5 write/초
비용:
- Read: $0.00013 per RCU-hour
- Write: $0.00065 per WCU-hour
- 예측 가능한 비용
사용 사례:
- 트래픽이 일정한 애플리케이션
- 비용 예측이 중요한 경우
On-Demand 모드
사용한 만큼 자동 청구:
1
2
3
4
5
6
resources:
Resources:
MyTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
비용:
- Read: $0.25 per million requests
- Write: $1.25 per million requests
- 예측 불가능하지만 자동 스케일
사용 사례:
- 트래픽 변동이 큰 경우
- 신규 애플리케이션 (트래픽 예측 어려움)
- 서버리스 애플리케이션
비용 비교 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
시나리오: 월 100만 read, 50만 write
Provisioned (평균 분산):
- Read: 5 RCU × 720시간 × $0.00013 = $0.47
- Write: 5 WCU × 720시간 × $0.00065 = $2.34
총: $2.81
On-Demand:
- Read: 1,000,000 × $0.25/million = $0.25
- Write: 500,000 × $1.25/million = $0.625
총: $0.875 (69% 절감)
하지만 트래픽이 집중되면 Provisioned가 더 저렴할 수 있음
DynamoDB 쿼리 패턴
1. GetItem (단일 항목 조회)
1
2
3
4
5
6
const response = await docClient.send(new GetCommand({
TableName: 'Users',
Key: { userId: 'user-123' }
}));
const user = response.Item;
속성 선택 (Projection Expression):
1
2
3
4
5
6
7
8
const response = await docClient.send(new GetCommand({
TableName: 'Users',
Key: { userId: 'user-123' },
ProjectionExpression: 'userId, #name, email', // 필요한 속성만
ExpressionAttributeNames: {
'#name': 'name' // name은 예약어
}
}));
2. Query (파티션 키로 쿼리)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 특정 사용자의 최근 활동 로그
const response = await docClient.send(new QueryCommand({
TableName: 'ActivityLogs',
KeyConditionExpression: 'userId = :userId AND #timestamp > :startDate',
ExpressionAttributeNames: {
'#timestamp': 'timestamp'
},
ExpressionAttributeValues: {
':userId': 'user-123',
':startDate': Date.now() - (7 * 24 * 60 * 60 * 1000) // 7일 전
},
ScanIndexForward: false, // 내림차순
Limit: 20
}));
const logs = response.Items;
3. Scan (전체 테이블 스캔 - 비권장)
1
2
3
4
5
6
7
8
9
10
// ⚠️ 전체 테이블 스캔 - 느리고 비용 높음
const response = await docClient.send(new ScanCommand({
TableName: 'Users',
FilterExpression: 'age > :minAge',
ExpressionAttributeValues: {
':minAge': 18
}
}));
// 대신 GSI 사용 권장
4. Pagination (페이지네이션)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function getAllItems(tableName, partitionKey) {
const items = [];
let lastEvaluatedKey = null;
do {
const params = {
TableName: tableName,
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': partitionKey },
Limit: 100
};
if (lastEvaluatedKey) {
params.ExclusiveStartKey = lastEvaluatedKey;
}
const response = await docClient.send(new QueryCommand(params));
items.push(...response.Items);
lastEvaluatedKey = response.LastEvaluatedKey;
} while (lastEvaluatedKey);
return items;
}
DynamoDB Best Practices
1. 단일 테이블 설계 (Single Table Design)
안티패턴 (관계형 사고):
1
2
3
4
5
Users 테이블
Orders 테이블
Products 테이블
OrderItems 테이블
# 4개 테이블, 조인 불가능
DynamoDB 패턴:
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
// 단일 테이블에 모든 엔티티 저장
{
PK: "USER#user-123",
SK: "PROFILE",
name: "김민국",
email: "kim@example.com"
}
{
PK: "USER#user-123",
SK: "ORDER#order-001",
orderId: "order-001",
totalAmount: 50000,
status: "completed"
}
{
PK: "ORDER#order-001",
SK: "ITEM#1",
productId: "prod-1",
quantity: 2,
price: 10000
}
// 한 번의 Query로 사용자 + 주문 조회 가능
Query({
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': 'USER#user-123' }
})
2. 핫 파티션 회피
문제 상황:
1
2
3
4
5
6
7
// ❌ 나쁜 설계 - 모든 로그가 하나의 파티션에
{
logType: "error", // 파티션 키 - 대부분 error
timestamp: 1704772800000,
message: "..."
}
# error 파티션에만 쓰기가 집중 → 처리량 제한 발생
해결책:
1
2
3
4
5
6
7
8
// ✅ 좋은 설계 - 샤딩 추가
{
shardId: `error#${timestamp % 10}`, // 0-9로 분산
timestamp: 1704772800000,
logType: "error",
message: "..."
}
# 10개 파티션으로 분산 → 10배 처리량
3. GSI 오버로딩
하나의 GSI로 여러 쿼리 패턴 지원:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GSI: GSI1PK + GSI1SK
{
PK: "USER#user-123",
SK: "PROFILE",
GSI1PK: "STATUS#active", // 활성 사용자 조회
GSI1SK: "USER#user-123"
}
{
PK: "ORDER#order-001",
SK: "METADATA",
GSI1PK: "CUSTOMER#user-123", // 고객별 주문 조회
GSI1SK: "ORDER#2025-01-09"
}
// GSI 하나로 두 가지 쿼리 패턴 지원
4. 낙관적 잠금 (Optimistic Locking)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 버전 번호를 사용한 동시성 제어
{
userId: "user-123",
balance: 10000,
version: 5
}
// 업데이트 시 버전 확인
await docClient.send(new UpdateCommand({
TableName: 'Accounts',
Key: { userId: 'user-123' },
UpdateExpression: 'SET balance = :newBalance, version = version + :inc',
ConditionExpression: 'version = :currentVersion',
ExpressionAttributeValues: {
':newBalance': 9000,
':currentVersion': 5,
':inc': 1
}
}));
// 다른 트랜잭션이 먼저 업데이트했다면 실패 → 재시도
5. 조건부 쓰기로 중복 방지
1
2
3
4
5
6
7
8
// 아이템이 없을 때만 생성
await docClient.send(new PutCommand({
TableName: 'Users',
Item: { userId: 'user-123', name: '김민국' },
ConditionExpression: 'attribute_not_exists(userId)'
}));
// 이미 존재하면 ConditionalCheckFailedException 발생
DynamoDB 비용 최적화
1. Reserved Capacity (예약 용량)
Provisioned 모드에서 1년 또는 3년 약정 시 최대 76% 할인.
2. TTL 활용
만료된 데이터 자동 삭제로 스토리지 비용 절감.
3. Compression (압축)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import zlib from 'zlib';
// 큰 데이터는 압축해서 저장
const compressed = zlib.gzipSync(JSON.stringify(largeData));
await docClient.send(new PutCommand({
TableName: 'Data',
Item: {
id: 'data-123',
data: compressed.toString('base64'),
isCompressed: true
}
}));
// 읽을 때 압축 해제
const item = response.Item;
if (item.isCompressed) {
const decompressed = zlib.gunzipSync(Buffer.from(item.data, 'base64'));
item.data = JSON.parse(decompressed.toString());
}
4. S3 오프로딩
400KB 이상 큰 속성은 S3에 저장하고 DynamoDB에는 참조만.
1
2
3
4
5
6
7
8
9
10
11
// ❌ 나쁜 예 - 큰 데이터를 DynamoDB에
{
userId: "user-123",
profileImage: "base64 encoded image (1MB)" // 비쌈
}
// ✅ 좋은 예 - S3 참조만 저장
{
userId: "user-123",
profileImageUrl: "s3://bucket/user-123/profile.jpg" // 참조만
}
DynamoDB vs 다른 데이터베이스
DynamoDB vs RDS (관계형)
| 특성 | DynamoDB | RDS |
|---|---|---|
| 데이터 모델 | Key-Value, Document | 관계형 (SQL) |
| 확장성 | 자동 무제한 확장 | 수직/수평 확장 (복잡) |
| 조인 | 불가능 | 가능 |
| 트랜잭션 | 제한적 (최대 100개 항목) | 완전 지원 |
| 일관성 | Eventually Consistent (기본) | Strong Consistent |
| 비용 (100GB) | ~$25/월 | ~$100/월 (인스턴스 포함) |
| 관리 | 완전 관리형 | 백업, 패치 필요 |
DynamoDB 선택 시:
- 트래픽 변동 큼
- 확장성이 중요
- 서버리스 아키텍처
- Key-Value 액세스 패턴
RDS 선택 시:
- 복잡한 쿼리 (JOIN, GROUP BY 등)
- 기존 SQL 애플리케이션
- 트랜잭션이 중요
- 데이터 일관성 최우선
DynamoDB vs MongoDB
| 특성 | DynamoDB | MongoDB |
|---|---|---|
| 호스팅 | AWS 완전 관리 | 자체 호스팅 또는 Atlas |
| 쿼리 | 제한적 | 풍부한 쿼리 언어 |
| 스키마 | 유연 | 유연 |
| 확장 | 자동 | 수동 샤딩 |
| 비용 | 사용량 기반 | 인스턴스 기반 |
마치며
Amazon DynamoDB는 서버리스 애플리케이션의 완벽한 데이터베이스임.
DynamoDB를 선택해야 할 때:
- 서버리스 아키텍처 (Lambda 기반)
- 예측 불가능한 트래픽
- 밀리초 단위 응답 시간 필요
- 관리 부담 최소화
- Key-Value 또는 Document 모델
핵심 패턴:
- 파티션 키 신중히 선택: 고르게 분산되는 키 사용
- GSI 적극 활용: 다양한 쿼리 패턴 지원
- 단일 테이블 설계: 조인 없이 한 번에 조회
- 조건부 쓰기: 동시성 제어
- TTL로 자동 정리: 비용 절감
실전 프로젝트에서는 DynamoDB를 Lambda(처리), S3(대용량 데이터), SQS(비동기 처리)와 조합하여 완전한 서버리스 시스템을 구축할 수 있음.
Document Client 사용, 적절한 용량 모드 선택, TTL 활용으로 비용을 최적화하면서도 높은 성능과 가용성을 유지할 수 있음.