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 활용으로 비용을 최적화하면서도 높은 성능과 가용성을 유지할 수 있습니다.
도움이 되셨길 바랍니다! 😀