Use Cases (업데이트: 2026. 6. 3.)

Claude Code와 AWS DynamoDB 실전 가이드: 키 설계부터 안전한 쓰기까지

Claude Code로 DynamoDB를 설계하고 구현하는 실전 가이드. 키, TTL, IAM, 비용, 핫 파티션을 다룹니다.

Claude Code와 AWS DynamoDB 실전 가이드: 키 설계부터 안전한 쓰기까지

Claude Code에 “DynamoDB 붙여줘”라고만 말하면, 처음에는 빠르게 보이는 코드가 나오지만 운영 단계에서 문제가 드러나기 쉽습니다. DynamoDB는 스키마가 자유로운 데이터베이스처럼 보이지만, 실제 스키마는 접근 패턴입니다. 어떤 화면이 어떤 키로 읽는지, 어떤 쓰기가 덮어쓰면 안 되는지, 트래픽이 파티션 키에 고르게 퍼지는지가 설계의 핵심입니다.

이 글은 Claude Code를 단순 코드 생성기가 아니라 DynamoDB 설계 리뷰 도구로 쓰는 방법을 다룹니다. 테이블 설계, 파티션 키, 단순 설계와 싱글 테이블 설계, 조건부 쓰기, TTL, 로컬 테스트, IAM, 비용, 핫 파티션 실수를 하나의 흐름으로 정리합니다. 함께 읽을 글로는 AWS Lambda 가이드, AWS IAM 가이드, AWS CloudWatch 활용이 좋습니다.

구현 중에는 AWS 공식 문서를 기준으로 확인하세요: DynamoDB data modeling foundations, partition key best practices, Query key condition expressions, condition expressions, TTL, DynamoDB local, throughput capacity, IAM fine-grained access control.

먼저 접근 패턴을 정한다

접근 패턴은 애플리케이션이 실제로 수행하는 읽기와 쓰기입니다. 프로젝트의 작업 목록 보기, 작업 하나 완료 처리, 사용자 세션을 7일 뒤 만료시키기, 같은 webhook 이벤트를 한 번만 처리하기처럼 화면과 API에서 보이는 동작을 말합니다.

코드를 요청하기 전에 Claude Code에 이렇게 요청하세요.

코드를 쓰기 전에 이 DynamoDB 설계를 리뷰해 주세요.
요구사항:
- 프로젝트별 작업 목록을 조회한다
- taskId로 작업 하나를 업데이트한다
- 사용자 세션은 7일 뒤 만료된다
- webhook eventId는 한 번만 처리한다

출력:
1. 접근 패턴 표
2. PK/SK 제안
3. Query로 가능한 작업과 불가능한 작업
4. 필요한 조건부 쓰기
5. 핫 파티션과 비용 리스크

DynamoDB의 Query는 파티션 키의 동등 조건을 필요로 하고, 정렬 키 조건으로 범위를 좁힙니다. FilterExpression은 SQL의 WHERE처럼 비용을 줄여 주지 않습니다. 읽은 뒤에 버리는 필터이므로 이미 용량을 소비합니다. Claude Code가 목록 화면에 Scan을 넣는다면, 먼저 설계를 다시 보세요.

단순 설계와 싱글 테이블 설계

싱글 테이블 설계는 여러 엔터티를 하나의 테이블에 넣고 PK, SK 접두사로 종류를 구분합니다. 관련 데이터를 한 번의 Query로 가져올 수 있지만, 키 명명 규칙, IAM, Streams, 백업 경계가 함께 복잡해집니다.

단순 설계는 엔터티별로 테이블을 나누거나, 하나의 테이블이라도 적은 수의 접근 패턴만 담습니다. MVP나 사내 도구에서는 이 방식이 더 리뷰하기 쉽습니다.

기준단순 설계싱글 테이블 설계
초기 개발설명하기 쉽다리뷰가 더 필요하다
여러 엔터티 화면여러 번 읽을 수 있다한 번의 Query가 가능하다
IAM테이블 단위 분리가 쉽다LeadingKeys 조건이 중요하다
변경테이블 추가가 쉽다키 이름의 일관성이 중요하다
적합한 경우MVP, 학습, 작은 도구접근 패턴이 안정된 업무 시스템

여기서는 하나의 테이블을 쓰되, 프로젝트, 작업, 세션, webhook 중복 방지로 범위를 제한합니다.

ClaudeCodeLabDemo

PK                 SK                   entityType
PROJECT#alpha      META                 Project
PROJECT#alpha      TASK#task-001        Task
USER#u-001         SESSION#s-001        Session
WEBHOOK#stripe     EVENT#evt_001        WebhookEvent

Query:
- 프로젝트 작업 목록: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- 사용자 세션: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Webhook 중복 방지: 같은 PK/SK에 조건부 PutItem

실제 사용 사례

첫 번째는 프로젝트 작업 보드입니다. 작업을 PK = PROJECT#projectId 아래에 넣고 SK = TASK#taskId로 저장하면 목록은 Query로 처리할 수 있습니다. 상태별 조회가 필요하다면 GSI가 필요한지, 아니면 프로젝트 단위 조회 후 애플리케이션에서 나누면 충분한지 먼저 판단해야 합니다.

두 번째는 세션과 초대 링크입니다. DynamoDB TTL은 Number 타입의 Unix epoch 초를 만료 시각으로 사용합니다. 세션, 비밀번호 재설정 링크, 임시 캐시를 정리하는 데 좋지만 정밀한 스케줄러는 아닙니다. 만료된 item은 백그라운드 삭제 전까지 읽힐 수 있으므로 보안과 관련된 데이터는 애플리케이션에서도 expiresAt을 확인해야 합니다.

세 번째는 webhook 멱등성입니다. 멱등성이란 같은 이벤트가 여러 번 와도 부작용이 한 번만 일어나는 성질입니다. WEBHOOK#providerEVENT#eventId를 키로 두고 attribute_not_exists(PK) AND attribute_not_exists(SK) 조건을 넣으면 첫 번째 처리만 성공합니다.

네 번째는 간단한 rate limit입니다. PK = RATE#userId, SK = WINDOW#2026-06-03T10:00 같은 시간 창을 만들 수 있습니다. 다만 인기 사용자나 테넌트에 트래픽이 몰리면 핫 파티션이 되기 쉬우므로 고트래픽 API에서는 별도 보호 장치가 필요합니다.

로컬 실행 준비

DynamoDB Local을 먼저 띄웁니다. 아래 Compose 파일은 AWS 문서의 Docker 예시와 같은 형태입니다.

services:
  dynamodb-local:
    image: "amazon/dynamodb-local:latest"
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
docker compose up -d
export AWS_ACCESS_KEY_ID=fakeMyKeyId
export AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
export AWS_REGION=us-west-2

로컬 테이블을 만듭니다. 실제 AWS에 생성하지 않도록 --endpoint-url을 확인하세요.

aws dynamodb create-table \
  --table-name ClaudeCodeLabDemo \
  --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \
  --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --endpoint-url http://localhost:8000 \
  --region us-west-2

TTL 속성을 켭니다. 로컬에서는 속성 형태와 애플리케이션 판단을 확인하는 용도로 보고, 실제 삭제 시점 검증은 프로덕션과 분리해서 생각합니다.

aws dynamodb update-time-to-live \
  --table-name ClaudeCodeLabDemo \
  --time-to-live-specification "Enabled=true,AttributeName=expiresAt" \
  --endpoint-url http://localhost:8000 \
  --region us-west-2

의존성을 설치합니다.

npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

복사해서 실행하는 구현

다음 내용을 app.mjs로 저장하세요. 작업 생성, 프로젝트 작업 조회, 조건부 완료 처리, TTL 세션 생성을 포함합니다.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  QueryCommand,
  UpdateCommand,
} from "@aws-sdk/lib-dynamodb";

const TABLE_NAME = process.env.TABLE_NAME ?? "ClaudeCodeLabDemo";
const isLocal = process.env.DDB_LOCAL !== "0";

const client = new DynamoDBClient({
  region: process.env.AWS_REGION ?? "us-west-2",
  ...(isLocal
    ? {
        endpoint: "http://localhost:8000",
        credentials: {
          accessKeyId: "fakeMyKeyId",
          secretAccessKey: "fakeSecretAccessKey",
        },
      }
    : {}),
});

const ddb = DynamoDBDocumentClient.from(client, {
  marshallOptions: { removeUndefinedValues: true },
});

const nowIso = () => new Date().toISOString();
const ttlAfterDays = (days) => Math.floor(Date.now() / 1000) + days * 86400;
const taskKey = (projectId, taskId) => ({
  PK: `PROJECT#${projectId}`,
  SK: `TASK#${taskId}`,
});

async function createTask({ projectId, taskId, title, ownerId }) {
  const item = {
    ...taskKey(projectId, taskId),
    entityType: "Task",
    title,
    ownerId,
    status: "OPEN",
    createdAt: nowIso(),
    updatedAt: nowIso(),
  };

  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: item,
      ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
    }),
  );

  return item;
}

async function listProjectTasks(projectId) {
  const result = await ddb.send(
    new QueryCommand({
      TableName: TABLE_NAME,
      KeyConditionExpression: "PK = :pk AND begins_with(SK, :taskPrefix)",
      ExpressionAttributeValues: {
        ":pk": `PROJECT#${projectId}`,
        ":taskPrefix": "TASK#",
      },
      ReturnConsumedCapacity: "TOTAL",
    }),
  );

  console.log("consumed capacity:", result.ConsumedCapacity);
  return result.Items ?? [];
}

async function completeTask({ projectId, taskId, expectedOwnerId }) {
  const result = await ddb.send(
    new UpdateCommand({
      TableName: TABLE_NAME,
      Key: taskKey(projectId, taskId),
      UpdateExpression: "SET #status = :done, updatedAt = :now",
      ConditionExpression: "ownerId = :ownerId AND #status <> :done",
      ExpressionAttributeNames: {
        "#status": "status",
      },
      ExpressionAttributeValues: {
        ":done": "DONE",
        ":ownerId": expectedOwnerId,
        ":now": nowIso(),
      },
      ReturnValues: "ALL_NEW",
    }),
  );

  return result.Attributes;
}

async function createSession({ userId, sessionId }) {
  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: {
        PK: `USER#${userId}`,
        SK: `SESSION#${sessionId}`,
        entityType: "Session",
        createdAt: nowIso(),
        expiresAt: ttlAfterDays(7),
      },
      ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
    }),
  );
}

async function main() {
  const projectId = "alpha";
  const taskId = `task-${Date.now()}`;

  await createTask({
    projectId,
    taskId,
    title: "Review DynamoDB key design",
    ownerId: "masa",
  });

  await createSession({
    userId: "masa",
    sessionId: `session-${Date.now()}`,
  });

  console.log(await listProjectTasks(projectId));
  console.log(
    await completeTask({
      projectId,
      taskId,
      expectedOwnerId: "masa",
    }),
  );
}

main().catch((error) => {
  if (error.name === "ConditionalCheckFailedException") {
    console.error("Condition failed:", error.message);
    process.exit(2);
  }

  console.error(error);
  process.exit(1);
});
DDB_LOCAL=1 node app.mjs

Claude Code에 Lambda로 바꾸라고 요청할 때는 핵심 제약을 같이 줍니다.

app.mjs를 Lambda handler로 분리해 주세요.
ConditionExpression을 제거하지 마세요.
Query가 불가능한 이유를 설명하지 않는 한 Scan을 추가하지 마세요.
expiresAt은 Unix epoch 초 Number로 유지하세요.
개발 환경에서는 ReturnConsumedCapacity를 유지하세요.

IAM과 안전 경계

싱글 테이블에서는 테이블 전체에 대한 권한이 너무 넓을 수 있습니다. DynamoDB IAM 조건 키인 dynamodb:LeadingKeys를 사용하면 파티션 키 기준으로 접근을 제한할 수 있습니다. 아래 예시는 projectId principal tag가 있다고 가정합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProjectScopedDynamoDBAccess",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd",
        "arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd/index/*"
      ],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": [
            "PROJECT#${aws:PrincipalTag/projectId}"
          ]
        }
      }
    }
  ]
}

생성된 정책을 볼 때는 Scan이 필요한 이유, index ARN 포함 여부, 개발과 운영 테이블 분리, ReturnValues나 projection 설정으로 민감 속성이 노출되지 않는지를 확인하세요.

비용과 핫 파티션

온디맨드 모드는 새 워크로드를 시작하기 쉽습니다. 요청 단위 과금이라 초기 용량 예측이 필요 없습니다. 반대로 트래픽이 안정적이고 예측 가능하다면 프로비저닝 모드가 비용 통제에 더 적합할 수 있습니다. Claude Code에 “싸게 해줘”라고만 말하지 말고 item 크기, 초당 읽기와 쓰기, 피크 패턴을 같이 주세요.

자주 나오는 실수는 다음과 같습니다.

  • 목록 화면을 Scan으로 만든다
  • FilterExpression을 SQL WHERE처럼 생각한다
  • GLOBAL, TODAY, DEFAULT 같은 고정 키에 트래픽을 몰아넣는다
  • TTL이 만료 시각에 즉시 삭제한다고 믿는다
  • 접근 패턴 없이 GSI를 추가한다
  • 주문, webhook, 상태 변경에 조건부 쓰기를 넣지 않는다
  • 로컬 endpoint를 운영 설정에 남긴다

리뷰 요청은 이렇게 쓰면 좋습니다.

이 DynamoDB 구현에서 Scan 의존, 핫 파티션, TTL 오해, 조건부 쓰기 누락, 과도한 IAM 권한, on-demand 비용 급증 위험을 찾아 주세요. 문제를 먼저 쓰고 그다음 수정안을 제안해 주세요.

프롬프트와 리뷰 체크리스트를 반복해서 쓰고 싶다면 ClaudeCodeLab 제품을 확인하세요. 팀에서 AWS, IAM, CI 리뷰, 운영 검증을 함께 정리해야 한다면 Claude Code 교육 및 컨설팅이 다음 단계입니다.

마무리

DynamoDB는 테이블 수보다 접근 패턴이 중요합니다. 어떤 키로 읽고, 어떤 쓰기를 막고, 어떤 데이터가 만료되며, 어떤 권한이 필요한지 먼저 결정해야 합니다. Claude Code는 이 제약을 코드로 바꾸는 데 강하지만, 제약 자체를 자동으로 올바르게 만들어 주지는 않습니다.

실습 메모(実際に試した結果): 가장 안정적인 순서는 Claude Code에 접근 패턴 표를 먼저 만들게 하고, PK/SK와 실패 조건을 검토한 뒤, DynamoDB Local에서 조건부 쓰기를 실행하는 것이었습니다. 특히 attribute_not_exists를 이용한 webhook 중복 방지는 실제 코드 리뷰에서 바로 쓸 수 있는 안전 장치입니다.

#claude-code #aws #dynamodb #nosql #typescript #database
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.