🗄️ 데이터베이스

DynamoDB

Amazon DynamoDB

Amazon DynamoDB는 AWS의 완전 관리형 서버리스 NoSQL 데이터베이스입니다. 키-값 및 문서 데이터 모델을 지원하며, 자동으로 확장되어 트래픽 변화에 대응합니다. 밀리초 단위의 일관된 응답 시간을 보장합니다.

📖 상세 설명

완전 관리형 서버리스: DynamoDB는 서버 프로비저닝, 패치, 백업, 복제 등을 AWS가 자동으로 처리합니다. 사용량에 따라 자동 확장(Auto Scaling)되며, 온디맨드 모드를 사용하면 용량 계획 없이 요청당 과금됩니다. 인프라 관리 부담 없이 애플리케이션 개발에 집중할 수 있습니다.

데이터 모델: 테이블은 파티션 키(PK)와 선택적 정렬 키(SK)로 구성됩니다. 파티션 키는 데이터 분산의 기준이며, 정렬 키로 파티션 내 정렬과 범위 쿼리가 가능합니다. 속성(Attribute)은 스키마리스로 아이템마다 다를 수 있으며, 중첩된 JSON 문서도 저장할 수 있습니다.

일관성 모델: 기본적으로 최종 일관성(Eventually Consistent) 읽기를 제공하여 처리량을 극대화합니다. 강력한 일관성(Strongly Consistent) 읽기도 옵션으로 선택 가능합니다. 트랜잭션 API를 통해 여러 아이템에 대한 ACID 트랜잭션도 지원합니다.

보조 인덱스: GSI(Global Secondary Index)는 다른 파티션 키로 쿼리할 수 있게 해주며, 테이블과 별도의 처리량을 가집니다. LSI(Local Secondary Index)는 같은 파티션 키에 다른 정렬 키로 쿼리합니다. 인덱스를 통해 다양한 액세스 패턴을 지원할 수 있습니다.

주요 사용 사례: 세션 저장소, 사용자 프로필, 장바구니, 게임 리더보드, IoT 데이터, Lambda와 연동한 서버리스 백엔드 등에 적합합니다. Amazon, Lyft, Airbnb, Samsung 등이 대규모로 사용합니다.

💻 코드 예제

# DynamoDB Python 예제 - boto3
import boto3
from boto3.dynamodb.conditions import Key, Attr
from decimal import Decimal
import json

class DynamoDBService:
    """DynamoDB 데이터베이스 서비스"""

    def __init__(self, table_name: str, region: str = 'ap-northeast-2'):
        self.dynamodb = boto3.resource('dynamodb', region_name=region)
        self.table = self.dynamodb.Table(table_name)
        self.client = boto3.client('dynamodb', region_name=region)

    def create_table(self):
        """테이블 생성 (파티션 키 + 정렬 키)"""
        table = self.dynamodb.create_table(
            TableName='UserOrders',
            KeySchema=[
                {'AttributeName': 'user_id', 'KeyType': 'HASH'},   # 파티션 키
                {'AttributeName': 'order_id', 'KeyType': 'RANGE'}  # 정렬 키
            ],
            AttributeDefinitions=[
                {'AttributeName': 'user_id', 'AttributeType': 'S'},
                {'AttributeName': 'order_id', 'AttributeType': 'S'},
                {'AttributeName': 'status', 'AttributeType': 'S'},
            ],
            GlobalSecondaryIndexes=[
                {
                    'IndexName': 'StatusIndex',
                    'KeySchema': [
                        {'AttributeName': 'status', 'KeyType': 'HASH'},
                        {'AttributeName': 'order_id', 'KeyType': 'RANGE'}
                    ],
                    'Projection': {'ProjectionType': 'ALL'}
                }
            ],
            BillingMode='PAY_PER_REQUEST'  # 온디맨드 모드
        )
        table.wait_until_exists()
        print(f"테이블 생성 완료: {table.table_name}")

    def put_item(self, user_id: str, order_id: str, data: dict):
        """아이템 저장"""
        item = {
            'user_id': user_id,
            'order_id': order_id,
            **data
        }
        # Decimal 변환 (float 미지원)
        item = json.loads(json.dumps(item), parse_float=Decimal)

        response = self.table.put_item(
            Item=item,
            ConditionExpression='attribute_not_exists(user_id)'  # 중복 방지
        )
        print(f"저장 완료: {user_id}/{order_id}")
        return response

    def get_item(self, user_id: str, order_id: str):
        """단일 아이템 조회"""
        response = self.table.get_item(
            Key={
                'user_id': user_id,
                'order_id': order_id
            },
            ConsistentRead=True  # 강력한 일관성 읽기
        )
        return response.get('Item')

    def query_user_orders(self, user_id: str, limit: int = 10):
        """사용자의 주문 목록 조회 (정렬 키 기반)"""
        response = self.table.query(
            KeyConditionExpression=Key('user_id').eq(user_id),
            ScanIndexForward=False,  # 최신순 정렬
            Limit=limit
        )
        return response['Items']

    def query_by_status(self, status: str):
        """상태별 주문 조회 (GSI 사용)"""
        response = self.table.query(
            IndexName='StatusIndex',
            KeyConditionExpression=Key('status').eq(status)
        )
        return response['Items']

    def update_item(self, user_id: str, order_id: str, status: str):
        """아이템 업데이트 (조건부)"""
        response = self.table.update_item(
            Key={
                'user_id': user_id,
                'order_id': order_id
            },
            UpdateExpression='SET #status = :status, updated_at = :now',
            ExpressionAttributeNames={'#status': 'status'},
            ExpressionAttributeValues={
                ':status': status,
                ':now': '2024-01-15T10:00:00Z',
                ':old_status': 'pending'
            },
            ConditionExpression='#status = :old_status',  # 낙관적 락
            ReturnValues='ALL_NEW'
        )
        return response['Attributes']

    def batch_write(self, items: list):
        """배치 쓰기 (최대 25개)"""
        with self.table.batch_writer() as batch:
            for item in items:
                batch.put_item(Item=item)
        print(f"{len(items)}개 아이템 배치 저장 완료")

    def transact_write(self, user_id: str, order_id: str, amount: Decimal):
        """트랜잭션 쓰기 (ACID 보장)"""
        self.client.transact_write_items(
            TransactItems=[
                {
                    'Update': {
                        'TableName': 'UserOrders',
                        'Key': {
                            'user_id': {'S': user_id},
                            'order_id': {'S': order_id}
                        },
                        'UpdateExpression': 'SET #status = :status',
                        'ExpressionAttributeNames': {'#status': 'status'},
                        'ExpressionAttributeValues': {':status': {'S': 'completed'}}
                    }
                },
                {
                    'Update': {
                        'TableName': 'UserBalance',
                        'Key': {'user_id': {'S': user_id}},
                        'UpdateExpression': 'SET balance = balance - :amount',
                        'ExpressionAttributeValues': {':amount': {'N': str(amount)}}
                    }
                }
            ]
        )
        print("트랜잭션 완료")


# 사용 예시
if __name__ == "__main__":
    db = DynamoDBService('UserOrders')

    # 주문 저장
    db.put_item('user123', 'order456', {
        'product': 'MacBook Pro',
        'price': Decimal('2500000'),
        'status': 'pending'
    })

    # 조회
    order = db.get_item('user123', 'order456')
    print(f"주문: {order}")

    # 사용자 주문 목록
    orders = db.query_user_orders('user123')
    for o in orders:
        print(f"주문 ID: {o['order_id']}, 상품: {o['product']}")
// DynamoDB Node.js 예제 - AWS SDK v3
const {
    DynamoDBClient,
    CreateTableCommand,
    PutItemCommand,
    GetItemCommand,
    QueryCommand,
    UpdateItemCommand,
    TransactWriteItemsCommand
} = require('@aws-sdk/client-dynamodb');

const {
    DynamoDBDocumentClient,
    PutCommand,
    GetCommand,
    QueryCommand: DocQueryCommand,
    UpdateCommand,
    BatchWriteCommand
} = require('@aws-sdk/lib-dynamodb');

class DynamoDBService {
    constructor(tableName, region = 'ap-northeast-2') {
        const client = new DynamoDBClient({ region });
        this.docClient = DynamoDBDocumentClient.from(client);
        this.tableName = tableName;
    }

    async createTable() {
        const client = new DynamoDBClient({ region: 'ap-northeast-2' });
        const command = new CreateTableCommand({
            TableName: this.tableName,
            KeySchema: [
                { AttributeName: 'pk', KeyType: 'HASH' },
                { AttributeName: 'sk', KeyType: 'RANGE' }
            ],
            AttributeDefinitions: [
                { AttributeName: 'pk', AttributeType: 'S' },
                { AttributeName: 'sk', AttributeType: 'S' },
                { AttributeName: 'gsi1pk', AttributeType: 'S' },
                { AttributeName: 'gsi1sk', AttributeType: 'S' }
            ],
            GlobalSecondaryIndexes: [{
                IndexName: 'GSI1',
                KeySchema: [
                    { AttributeName: 'gsi1pk', KeyType: 'HASH' },
                    { AttributeName: 'gsi1sk', KeyType: 'RANGE' }
                ],
                Projection: { ProjectionType: 'ALL' }
            }],
            BillingMode: 'PAY_PER_REQUEST'
        });

        await client.send(command);
        console.log(`테이블 생성: ${this.tableName}`);
    }

    async putItem(pk, sk, data) {
        const command = new PutCommand({
            TableName: this.tableName,
            Item: {
                pk,
                sk,
                ...data,
                createdAt: new Date().toISOString()
            },
            ConditionExpression: 'attribute_not_exists(pk)'
        });

        await this.docClient.send(command);
        console.log(`저장 완료: ${pk}/${sk}`);
    }

    async getItem(pk, sk) {
        const command = new GetCommand({
            TableName: this.tableName,
            Key: { pk, sk },
            ConsistentRead: true
        });

        const response = await this.docClient.send(command);
        return response.Item;
    }

    async queryByPartitionKey(pk, options = {}) {
        const command = new DocQueryCommand({
            TableName: this.tableName,
            KeyConditionExpression: 'pk = :pk',
            ExpressionAttributeValues: {
                ':pk': pk
            },
            ScanIndexForward: options.ascending ?? false,
            Limit: options.limit ?? 20
        });

        const response = await this.docClient.send(command);
        return response.Items;
    }

    async queryByGSI(gsi1pk, gsi1sk) {
        const command = new DocQueryCommand({
            TableName: this.tableName,
            IndexName: 'GSI1',
            KeyConditionExpression: 'gsi1pk = :pk AND begins_with(gsi1sk, :sk)',
            ExpressionAttributeValues: {
                ':pk': gsi1pk,
                ':sk': gsi1sk
            }
        });

        const response = await this.docClient.send(command);
        return response.Items;
    }

    async updateItem(pk, sk, updates) {
        const updateExpressions = [];
        const attributeNames = {};
        const attributeValues = {};

        Object.entries(updates).forEach(([key, value], index) => {
            const nameKey = `#attr${index}`;
            const valueKey = `:val${index}`;
            updateExpressions.push(`${nameKey} = ${valueKey}`);
            attributeNames[nameKey] = key;
            attributeValues[valueKey] = value;
        });

        const command = new UpdateCommand({
            TableName: this.tableName,
            Key: { pk, sk },
            UpdateExpression: `SET ${updateExpressions.join(', ')}`,
            ExpressionAttributeNames: attributeNames,
            ExpressionAttributeValues: attributeValues,
            ReturnValues: 'ALL_NEW'
        });

        const response = await this.docClient.send(command);
        return response.Attributes;
    }

    async batchWrite(items) {
        // 25개씩 분할
        const chunks = [];
        for (let i = 0; i < items.length; i += 25) {
            chunks.push(items.slice(i, i + 25));
        }

        for (const chunk of chunks) {
            const command = new BatchWriteCommand({
                RequestItems: {
                    [this.tableName]: chunk.map(item => ({
                        PutRequest: { Item: item }
                    }))
                }
            });

            await this.docClient.send(command);
        }

        console.log(`${items.length}개 배치 저장 완료`);
    }

    async transactWrite(operations) {
        const client = new DynamoDBClient({ region: 'ap-northeast-2' });
        const command = new TransactWriteItemsCommand({
            TransactItems: operations
        });

        await client.send(command);
        console.log('트랜잭션 완료');
    }
}

// Single Table Design 패턴 예시
async function main() {
    const db = new DynamoDBService('MyApp');

    // 사용자 저장
    await db.putItem('USER#user123', 'PROFILE', {
        name: '홍길동',
        email: 'hong@example.com',
        gsi1pk: 'USER',
        gsi1sk: 'EMAIL#hong@example.com'
    });

    // 주문 저장 (같은 파티션에 다른 정렬 키)
    await db.putItem('USER#user123', 'ORDER#2024-001', {
        product: 'MacBook Pro',
        price: 2500000,
        status: 'pending',
        gsi1pk: 'ORDER#pending',
        gsi1sk: '2024-01-15'
    });

    // 사용자의 모든 데이터 조회
    const userData = await db.queryByPartitionKey('USER#user123');
    console.log('사용자 데이터:', userData);

    // 대기중인 모든 주문 조회 (GSI)
    const pendingOrders = await db.queryByGSI('ORDER#pending', '2024');
    console.log('대기 주문:', pendingOrders);
}

main().catch(console.error);
# DynamoDB AWS CLI 예제

# ============================================
# 1. 테이블 생성
# ============================================
aws dynamodb create-table \
    --table-name UserOrders \
    --key-schema \
        AttributeName=user_id,KeyType=HASH \
        AttributeName=order_id,KeyType=RANGE \
    --attribute-definitions \
        AttributeName=user_id,AttributeType=S \
        AttributeName=order_id,AttributeType=S \
    --billing-mode PAY_PER_REQUEST \
    --region ap-northeast-2

# 테이블 상태 확인
aws dynamodb describe-table --table-name UserOrders


# ============================================
# 2. 아이템 저장
# ============================================
aws dynamodb put-item \
    --table-name UserOrders \
    --item '{
        "user_id": {"S": "user123"},
        "order_id": {"S": "order-2024-001"},
        "product": {"S": "MacBook Pro"},
        "price": {"N": "2500000"},
        "status": {"S": "pending"},
        "created_at": {"S": "2024-01-15T10:00:00Z"}
    }'


# ============================================
# 3. 아이템 조회
# ============================================
# 단일 아이템 조회
aws dynamodb get-item \
    --table-name UserOrders \
    --key '{
        "user_id": {"S": "user123"},
        "order_id": {"S": "order-2024-001"}
    }' \
    --consistent-read

# 쿼리 (파티션 키로 조회)
aws dynamodb query \
    --table-name UserOrders \
    --key-condition-expression "user_id = :uid" \
    --expression-attribute-values '{
        ":uid": {"S": "user123"}
    }' \
    --scan-index-forward false \
    --limit 10

# 범위 쿼리 (정렬 키 조건 추가)
aws dynamodb query \
    --table-name UserOrders \
    --key-condition-expression "user_id = :uid AND begins_with(order_id, :prefix)" \
    --expression-attribute-values '{
        ":uid": {"S": "user123"},
        ":prefix": {"S": "order-2024"}
    }'


# ============================================
# 4. 아이템 업데이트
# ============================================
aws dynamodb update-item \
    --table-name UserOrders \
    --key '{
        "user_id": {"S": "user123"},
        "order_id": {"S": "order-2024-001"}
    }' \
    --update-expression "SET #status = :status, updated_at = :now" \
    --expression-attribute-names '{"#status": "status"}' \
    --expression-attribute-values '{
        ":status": {"S": "completed"},
        ":now": {"S": "2024-01-15T12:00:00Z"}
    }' \
    --condition-expression "#status = :old_status" \
    --expression-attribute-values '{
        ":status": {"S": "completed"},
        ":now": {"S": "2024-01-15T12:00:00Z"},
        ":old_status": {"S": "pending"}
    }' \
    --return-values ALL_NEW


# ============================================
# 5. GSI 생성
# ============================================
aws dynamodb update-table \
    --table-name UserOrders \
    --attribute-definitions AttributeName=status,AttributeType=S \
    --global-secondary-index-updates '[{
        "Create": {
            "IndexName": "StatusIndex",
            "KeySchema": [
                {"AttributeName": "status", "KeyType": "HASH"},
                {"AttributeName": "order_id", "KeyType": "RANGE"}
            ],
            "Projection": {"ProjectionType": "ALL"}
        }
    }]'


# ============================================
# 6. 배치 쓰기
# ============================================
aws dynamodb batch-write-item \
    --request-items '{
        "UserOrders": [
            {
                "PutRequest": {
                    "Item": {
                        "user_id": {"S": "user456"},
                        "order_id": {"S": "order-001"},
                        "product": {"S": "iPhone"}
                    }
                }
            },
            {
                "PutRequest": {
                    "Item": {
                        "user_id": {"S": "user456"},
                        "order_id": {"S": "order-002"},
                        "product": {"S": "iPad"}
                    }
                }
            }
        ]
    }'


# ============================================
# 7. TTL 설정 (자동 삭제)
# ============================================
aws dynamodb update-time-to-live \
    --table-name UserOrders \
    --time-to-live-specification Enabled=true,AttributeName=expires_at


# ============================================
# 8. 백업 및 복원
# ============================================
# 온디맨드 백업
aws dynamodb create-backup \
    --table-name UserOrders \
    --backup-name UserOrders-backup-20240115

# Point-in-Time Recovery 활성화
aws dynamodb update-continuous-backups \
    --table-name UserOrders \
    --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true


# ============================================
# 9. 로컬 개발 (DynamoDB Local)
# ============================================
# Docker로 실행
docker run -p 8000:8000 amazon/dynamodb-local

# 로컬에 연결
aws dynamodb list-tables --endpoint-url http://localhost:8000

🗣️ 실무에서 이렇게 말하세요

💬 서버리스 아키텍처 설계에서
"Lambda와 DynamoDB 조합이면 서버리스 백엔드가 완성됩니다. 온디맨드 모드로 설정하면 트래픽에 따라 자동 확장되고, 사용한 만큼만 비용이 청구돼요. 세션 저장소나 사용자 프로필 같은 간단한 키-값 데이터에 완벽합니다."
💬 데이터 모델링 리뷰에서
"DynamoDB는 쿼리 패턴을 먼저 정의하고 테이블을 설계해야 합니다. Single Table Design을 적용해서 pk는 'USER#user123', sk는 'ORDER#2024-001' 형태로 하면 한 번의 쿼리로 사용자와 주문을 모두 가져올 수 있어요."
💬 비용 최적화 회의에서
"스캔 연산이 너무 많이 발생하고 있네요. DynamoDB에서 Scan은 전체 테이블을 읽어서 비용이 급증합니다. GSI를 추가해서 Query로 바꾸거나, 필터링을 애플리케이션 레벨로 옮기는 게 좋겠습니다."

⚠️ 주의사항 & 베스트 프랙티스

Scan 연산 지양

Scan은 전체 테이블을 읽어 비용과 지연이 급증합니다. 반드시 파티션 키를 포함한 Query를 사용하고, 필요하면 GSI를 추가하세요.

핫 파티션 방지

특정 파티션 키에 트래픽이 몰리면 쓰로틀링이 발생합니다. 고르게 분산되는 파티션 키를 선택하거나, 쓰기 샤딩을 적용하세요.

아이템 크기 제한 (400KB)

단일 아이템은 400KB를 초과할 수 없습니다. 대용량 데이터는 S3에 저장하고 참조만 DynamoDB에 저장하세요.

베스트 프랙티스

Single Table Design 적용, 액세스 패턴 기반 설계, TTL로 자동 정리, DAX 캐시로 읽기 성능 향상, Point-in-Time Recovery 활성화.

🔗 관련 용어

📚 더 배우기