🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

DynamoDB NoSQL로 확장 가능한 데이터 저장 - 슬라이드 1/7
A

AI Generated

2025. 12. 28. · 0 Views

DynamoDB NoSQL로 확장 가능한 데이터 저장

AWS DynamoDB의 핵심 개념부터 실전 활용까지, 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 파티션 키 설계부터 GSI, Streams까지 단계별로 배워봅니다.


목차

  1. DynamoDB의_특징과_사용_사례
  2. 파티션_키와_정렬_키_설계
  3. DynamoDB_테이블_생성_실습
  4. 읽기_쓰기_용량_모드_선택
  5. Global_Secondary_Index_활용
  6. DynamoDB_Streams로_이벤트_처리

1. DynamoDB의 특징과 사용 사례

김개발 씨는 스타트업에서 새로운 프로젝트를 시작하게 되었습니다. 사용자가 급격히 늘어날 수 있는 서비스라 확장성이 중요했습니다.

"MySQL로 하면 나중에 샤딩도 해야 하고 복잡해질 텐데..." 고민하던 중 선배가 다가왔습니다. "DynamoDB 써봤어요?

확장은 AWS가 알아서 해줘요."

DynamoDB는 AWS에서 제공하는 완전관리형 NoSQL 데이터베이스입니다. 마치 고속도로가 차량이 늘어나면 자동으로 차선을 늘려주는 것처럼, 트래픽이 증가해도 자동으로 확장됩니다.

밀리초 단위의 일관된 응답 시간을 제공하며, 서버 관리 없이 데이터를 저장하고 조회할 수 있습니다.

다음 코드를 살펴봅시다.

// AWS SDK v3를 사용한 DynamoDB 연결
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";

// DynamoDB 클라이언트 생성
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);

// 데이터 저장 - JSON 형태로 자유롭게 저장 가능
await docClient.send(new PutCommand({
  TableName: "Users",
  Item: { userId: "user123", name: "김개발", age: 28 }
}));

// 데이터 조회 - 키 기반으로 빠르게 조회
const result = await docClient.send(new GetCommand({
  TableName: "Users",
  Key: { userId: "user123" }
}));

김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 회사에서 새로운 모바일 앱 서비스를 런칭하게 되었는데, CTO님이 이런 말씀을 하셨습니다.

"이번 서비스는 바이럴 마케팅을 크게 할 거예요. 하루에 사용자가 10배씩 늘어날 수도 있어요." 김개발 씨는 걱정이 앞섰습니다.

지금까지 MySQL만 써왔는데, 급격한 트래픽 증가를 어떻게 감당하지? 레플리카 설정하고, 샤딩하고, 커넥션 풀 관리하고...

생각만 해도 머리가 아팠습니다. 그때 선배 박시니어 씨가 조언해주었습니다.

"그런 상황이라면 DynamoDB를 고려해봐요. 확장은 AWS가 알아서 해주니까 우리는 비즈니스 로직에만 집중하면 돼요." 그렇다면 DynamoDB란 정확히 무엇일까요?

쉽게 비유하자면, DynamoDB는 마치 무한히 늘어나는 창고와 같습니다. 일반 창고는 물건이 많아지면 새 창고를 빌리고, 물건을 옮기고, 관리자를 추가로 고용해야 합니다.

하지만 DynamoDB 창고는 물건이 아무리 많아져도 알아서 공간이 늘어나고, 직원도 자동으로 배치됩니다. 여러분은 그저 물건을 맡기고 찾기만 하면 됩니다.

DynamoDB가 등장하기 전에는 어땠을까요? 대용량 트래픽을 처리하려면 개발자가 직접 데이터베이스 클러스터를 구성해야 했습니다.

샤딩 키를 정하고, 데이터를 분산하고, 장애가 나면 새벽에 호출받아 복구하는 일이 일상이었습니다. 서비스가 성공할수록 운영 부담은 기하급수적으로 늘어났습니다.

바로 이런 문제를 해결하기 위해 DynamoDB가 등장했습니다. 완전관리형이라는 말은 AWS가 서버 프로비저닝, 패치, 백업, 복구를 모두 처리해준다는 뜻입니다.

여러분은 테이블만 만들면 됩니다. 또한 자동 확장 기능 덕분에 트래픽이 갑자기 10배로 늘어나도 응답 시간은 일정하게 유지됩니다.

무엇보다 서버리스 아키텍처와 찰떡궁합이라 Lambda와 함께 사용하면 인프라 걱정 없이 서비스를 만들 수 있습니다. 위의 코드를 살펴보겠습니다.

먼저 DynamoDBClient를 생성하여 AWS와 연결합니다. 그 다음 DocumentClient로 감싸면 JavaScript 객체를 바로 저장하고 조회할 수 있습니다.

PutCommand로 데이터를 저장하고, GetCommand로 조회하는 것이 전부입니다. SQL 문법을 배울 필요도 없습니다.

실제 현업에서는 어떤 경우에 DynamoDB를 사용할까요? 게임 서비스의 리더보드, 쇼핑몰의 장바구니, IoT 센서 데이터 수집, 세션 저장소 등이 대표적입니다.

이런 서비스들은 읽기와 쓰기가 빈번하고, 확장성이 중요하며, 데이터 구조가 비교적 단순합니다. 넷플릭스, 삼성, 도요타 같은 대기업들도 핵심 서비스에 DynamoDB를 활용하고 있습니다.

하지만 주의할 점도 있습니다. DynamoDB는 복잡한 조인 연산에는 적합하지 않습니다.

여러 테이블의 데이터를 복잡하게 연결해야 하는 경우라면 RDS가 더 나은 선택일 수 있습니다. 또한 트랜잭션 비용이 일반 작업보다 비싸므로, 무조건 DynamoDB가 정답은 아닙니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 DynamoDB를 도입한 김개발 씨.

런칭 후 사용자가 폭발적으로 늘었지만, 새벽에 장애 호출을 받는 일은 없었습니다. "이게 바로 서버리스의 맛이구나!" DynamoDB를 제대로 이해하면 확장성 있는 서비스를 훨씬 쉽게 만들 수 있습니다.

다음 장에서는 DynamoDB의 핵심인 파티션 키 설계에 대해 알아보겠습니다.

실전 팁

💡 - DynamoDB는 Key-Value 조회에 최적화되어 있으므로, 조회 패턴을 먼저 정하고 테이블을 설계하세요

  • 복잡한 조인이 필요하다면 RDS와 DynamoDB를 함께 사용하는 것도 좋은 전략입니다
  • 개발 환경에서는 DynamoDB Local을 사용하면 비용 없이 테스트할 수 있습니다

2. 파티션 키와 정렬 키 설계

김개발 씨가 DynamoDB 테이블을 처음 만들려고 하니 파티션 키정렬 키를 선택하라고 합니다. "그냥 id를 키로 하면 되는 거 아닌가?" 무심코 userId를 파티션 키로 설정했는데, 일주일 뒤 특정 사용자의 요청만 느려지기 시작했습니다.

대체 무슨 일이 일어난 걸까요?

파티션 키는 DynamoDB가 데이터를 분산 저장하는 기준이 됩니다. 마치 도서관에서 책을 분류하는 청구기호와 같습니다.

정렬 키는 같은 파티션 내에서 데이터를 정렬하는 두 번째 기준입니다. 이 두 가지를 잘 설계해야 성능이 균일하게 나옵니다.

다음 코드를 살펴봅시다.

// 좋은 키 설계 예시: 주문 테이블
// 파티션 키: userId, 정렬 키: orderDate
const orderItem = {
  TableName: "Orders",
  Item: {
    userId: "user123",           // 파티션 키: 사용자별로 데이터 분산
    orderDate: "2024-01-15T10:30:00Z",  // 정렬 키: 날짜순 정렬
    orderId: "order-abc-123",
    items: ["상품A", "상품B"],
    totalAmount: 35000
  }
};

// 특정 사용자의 최근 주문 조회 (정렬 키 범위 쿼리)
const queryParams = {
  TableName: "Orders",
  KeyConditionExpression: "userId = :uid AND orderDate > :date",
  ExpressionAttributeValues: {
    ":uid": "user123",
    ":date": "2024-01-01"
  }
};

김개발 씨는 DynamoDB를 도입하고 자신감이 넘쳤습니다. 테이블 설계?

간단하지. userId를 파티션 키로 하면 되겠군.

그렇게 프로덕션에 배포했습니다. 일주일 뒤 이상한 일이 벌어졌습니다.

특정 VIP 고객의 요청만 유독 느려지는 겁니다. 다른 사용자들은 10ms 만에 응답이 오는데, 이 고객만 500ms가 넘었습니다.

로그를 뒤져봐도 코드에는 문제가 없었습니다. 박시니어 씨가 DynamoDB 콘솔을 열어보더니 한숨을 쉬었습니다.

"아, 핫 파티션 문제네요. 이 VIP 고객이 다른 고객보다 데이터가 100배나 많아요." 파티션 키란 정확히 무엇일까요?

DynamoDB는 데이터를 여러 서버에 분산 저장합니다. 이때 어느 서버에 저장할지 결정하는 기준이 바로 파티션 키입니다.

마치 대형 물류센터에서 상품을 구역별로 나누는 것과 같습니다. 파티션 키가 같은 데이터는 같은 구역에 저장됩니다.

문제는 한 구역에 물건이 너무 많이 쌓이면 그 구역만 병목이 된다는 것입니다. 이것이 바로 핫 파티션 문제입니다.

정렬 키는 같은 파티션 내에서 데이터를 정렬하는 두 번째 열쇠입니다. 파티션 키가 구역이라면, 정렬 키는 그 구역 안에서 물건을 순서대로 정리하는 규칙입니다.

예를 들어 userId가 파티션 키이고 orderDate가 정렬 키라면, 같은 사용자의 주문을 날짜순으로 빠르게 조회할 수 있습니다. 좋은 파티션 키를 고르는 기준은 무엇일까요?

첫째, 카디널리티가 높아야 합니다. 즉, 가능한 값의 종류가 많아야 합니다.

예를 들어 성별(남/여)보다는 userId가 더 좋은 파티션 키입니다. 둘째, 접근 빈도가 균등해야 합니다.

모든 파티션에 비슷한 양의 요청이 분산되어야 성능이 균일합니다. 위의 코드 예제를 살펴보겠습니다.

주문 테이블에서 userId를 파티션 키로, orderDate를 정렬 키로 설정했습니다. 이렇게 하면 특정 사용자의 주문 내역을 날짜 범위로 빠르게 조회할 수 있습니다.

KeyConditionExpression에서 파티션 키는 반드시 등호(=)로, 정렬 키는 범위 연산자(>, <, BETWEEN)를 사용할 수 있습니다. 실무에서 흔히 저지르는 실수가 있습니다.

날짜를 파티션 키로 사용하는 것입니다. "오늘 날짜의 모든 로그를 빠르게 조회하고 싶어서요." 언뜻 합리적으로 들리지만, 최신 데이터에 트래픽이 집중되면 오늘 날짜 파티션만 과부하가 걸립니다.

이런 경우에는 날짜와 랜덤 접미사를 조합하거나, 아예 다른 설계를 고민해야 합니다. 다시 김개발 씨 이야기로 돌아가 봅시다.

문제의 원인을 파악한 후, 김개발 씨는 테이블을 재설계했습니다. 파티션 키에 userId와 함께 월(month) 정보를 추가해서 데이터를 더 잘게 분산시켰습니다.

그 결과 VIP 고객의 응답 시간도 다른 고객과 동일해졌습니다. 파티션 키 설계는 DynamoDB 성능의 80%를 결정한다고 해도 과언이 아닙니다.

조회 패턴을 먼저 정의하고, 데이터가 균등하게 분산되도록 키를 설계하세요.

실전 팁

💡 - 파티션 키를 선택할 때는 "이 키로 데이터가 균등하게 분산될까?"를 항상 자문하세요

  • 복합 키(파티션 키 + 정렬 키)를 사용하면 더 유연한 쿼리가 가능합니다
  • 정렬 키에 ISO 8601 형식의 타임스탬프를 사용하면 범위 쿼리에 유리합니다

3. DynamoDB 테이블 생성 실습

이론은 충분합니다. 이제 실제로 DynamoDB 테이블을 만들어볼 시간입니다.

김개발 씨는 AWS 콘솔에 접속했지만 수많은 옵션에 당황했습니다. "테이블 클래스?

암호화 설정? 이거 다 뭐지?" 박시니어 씨가 옆에서 웃으며 말했습니다.

"CLI로 하면 더 간단해요. 코드로 인프라를 관리하는 게 요즘 트렌드거든요."

DynamoDB 테이블은 AWS 콘솔, CLI, SDK, CloudFormation 등 다양한 방법으로 생성할 수 있습니다. 실무에서는 인프라를 코드로 관리하기 위해 CLI나 IaC 도구를 주로 사용합니다.

테이블 생성 시 파티션 키, 정렬 키, 읽기/쓰기 용량을 설정해야 합니다.

다음 코드를 살펴봅시다.

// AWS SDK v3로 테이블 생성
import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb";

const client = new DynamoDBClient({ region: "ap-northeast-2" });

const createTableParams = {
  TableName: "Products",
  // 키 스키마 정의: 파티션 키와 정렬 키
  KeySchema: [
    { AttributeName: "categoryId", KeyType: "HASH" },  // 파티션 키
    { AttributeName: "productId", KeyType: "RANGE" }   // 정렬 키
  ],
  // 속성 정의: 키로 사용되는 속성만 정의
  AttributeDefinitions: [
    { AttributeName: "categoryId", AttributeType: "S" },  // String
    { AttributeName: "productId", AttributeType: "S" }
  ],
  // 온디맨드 용량 모드 (자동 확장)
  BillingMode: "PAY_PER_REQUEST"
};

await client.send(new CreateTableCommand(createTableParams));
console.log("Products 테이블이 생성되었습니다.");

김개발 씨는 처음으로 DynamoDB 테이블을 직접 만들게 되었습니다. 상품 카탈로그 서비스를 위한 테이블이 필요했습니다.

AWS 콘솔에 접속해보니 입력해야 할 옵션이 한두 개가 아니었습니다. 박시니어 씨가 조언했습니다.

"콘솔로 해도 되지만, 코드로 만드는 습관을 들이세요. 나중에 스테이징, 프로덕션 환경을 일관되게 관리하려면 Infrastructure as Code가 필수예요." 테이블을 생성할 때 반드시 지정해야 하는 것들이 있습니다.

첫째, 테이블 이름입니다. AWS 계정과 리전 내에서 유일해야 합니다.

둘째, 키 스키마입니다. 어떤 속성을 파티션 키(HASH)와 정렬 키(RANGE)로 사용할지 정의합니다.

셋째, 속성 정의입니다. 키로 사용되는 속성의 데이터 타입을 지정합니다.

S는 String, N은 Number, B는 Binary입니다. 흥미로운 점은 키가 아닌 속성은 미리 정의하지 않아도 된다는 것입니다.

이것이 NoSQL의 특징입니다. 관계형 데이터베이스에서는 모든 컬럼을 미리 정의해야 하지만, DynamoDB에서는 각 아이템이 서로 다른 속성을 가질 수 있습니다.

상품 A는 color 속성이 있고, 상품 B는 size 속성이 있어도 괜찮습니다. 이를 **스키마리스(Schemaless)**라고 부릅니다.

위 코드를 단계별로 살펴보겠습니다. CreateTableCommand를 사용하여 테이블을 생성합니다.

KeySchema에서 categoryId를 HASH(파티션 키)로, productId를 RANGE(정렬 키)로 지정했습니다. 이렇게 하면 같은 카테고리의 상품들을 묶어서 저장하고, 카테고리 내에서 상품별로 정렬할 수 있습니다.

BillingMode는 PAY_PER_REQUEST로 설정했습니다. 이것이 온디맨드 모드입니다.

사용한 만큼만 비용을 지불하며, 트래픽에 따라 자동으로 확장됩니다. 다음 장에서 용량 모드에 대해 자세히 다루겠습니다.

실무에서는 CloudFormation이나 Terraform을 사용하는 경우가 많습니다. 왜냐하면 테이블 생성뿐 아니라 IAM 권한, 알람 설정, GSI까지 한 번에 관리할 수 있기 때문입니다.

하지만 학습 단계에서는 SDK로 직접 만들어보며 각 옵션의 의미를 이해하는 것이 중요합니다. 테이블이 생성되면 바로 사용할 수 있을까요?

아닙니다. 테이블 상태가 CREATING에서 ACTIVE로 바뀌어야 합니다.

보통 몇 초에서 몇 분 정도 걸립니다. DescribeTable 명령으로 상태를 확인할 수 있습니다.

프로덕션 코드에서는 테이블이 활성화될 때까지 대기하는 로직을 추가하는 것이 좋습니다. 김개발 씨는 테이블을 성공적으로 생성했습니다.

콘솔에서 확인해보니 Products 테이블이 잘 만들어져 있었습니다. "생각보다 간단하네요!" 이제 데이터를 넣어볼 차례입니다.

실전 팁

💡 - 테이블 이름은 한번 정하면 변경할 수 없으니 신중하게 선택하세요

  • 개발 환경에서는 DynamoDB Local을 사용하면 비용 없이 테스트할 수 있습니다
  • 프로덕션에서는 CloudFormation이나 Terraform으로 테이블을 관리하는 것이 좋습니다

4. 읽기 쓰기 용량 모드 선택

월말이 되자 김개발 씨는 AWS 청구서를 보고 깜짝 놀랐습니다. DynamoDB 비용이 예상보다 훨씬 높았던 것입니다.

"분명 트래픽이 별로 없었는데 왜 이렇게 많이 나온 거지?" 알고 보니 프로비저닝 모드로 설정해놓고 용량을 과하게 잡아둔 것이 원인이었습니다.

DynamoDB는 프로비저닝 모드온디맨드 모드 두 가지 용량 모드를 제공합니다. 프로비저닝 모드는 예상 트래픽을 미리 설정하고 비용을 절약할 수 있습니다.

온디맨드 모드는 사용한 만큼만 지불하며 트래픽 예측이 어려울 때 유용합니다.

다음 코드를 살펴봅시다.

// 프로비저닝 모드 테이블 생성
const provisionedTable = {
  TableName: "ProvisionedUsers",
  KeySchema: [{ AttributeName: "userId", KeyType: "HASH" }],
  AttributeDefinitions: [{ AttributeName: "userId", AttributeType: "S" }],
  // 프로비저닝 모드: 읽기/쓰기 용량 직접 설정
  BillingMode: "PROVISIONED",
  ProvisionedThroughput: {
    ReadCapacityUnits: 5,   // 초당 5개의 강력한 일관된 읽기
    WriteCapacityUnits: 5   // 초당 5개의 쓰기
  }
};

// 온디맨드 모드 테이블 생성
const onDemandTable = {
  TableName: "OnDemandUsers",
  KeySchema: [{ AttributeName: "userId", KeyType: "HASH" }],
  AttributeDefinitions: [{ AttributeName: "userId", AttributeType: "S" }],
  // 온디맨드 모드: 자동 확장, 사용량 기반 과금
  BillingMode: "PAY_PER_REQUEST"
};

// Auto Scaling 설정 (프로비저닝 모드에서 권장)
// Application Auto Scaling을 통해 설정

김개발 씨의 서비스가 성장하면서 DynamoDB 비용도 함께 늘어났습니다. CTO님이 물었습니다.

"DynamoDB 비용 최적화 방법을 찾아봐요." 이제 용량 모드에 대해 제대로 이해해야 할 때가 왔습니다. DynamoDB의 용량 모드는 크게 두 가지입니다.

첫 번째는 프로비저닝 모드입니다. 마치 월정액 데이터 요금제와 같습니다.

미리 사용할 양을 정해두고 그만큼 비용을 지불합니다. 예상 트래픽이 일정하다면 온디맨드보다 저렴합니다.

하지만 설정한 용량을 초과하면 요청이 스로틀링(거부)될 수 있습니다. 두 번째는 온디맨드 모드입니다.

종량제 요금제와 같습니다. 사용한 만큼만 비용을 내며, 트래픽이 급증해도 자동으로 확장됩니다.

단, 건당 비용이 프로비저닝 모드보다 비쌉니다. 그렇다면 RCUWCU는 무엇일까요?

**RCU(Read Capacity Unit)**는 읽기 용량 단위입니다. 1 RCU는 초당 최대 4KB 크기의 아이템을 1회 강력한 일관된 읽기(Strongly Consistent Read)할 수 있는 용량입니다.

최종적 일관된 읽기(Eventually Consistent Read)는 절반의 RCU만 소모합니다. **WCU(Write Capacity Unit)**는 쓰기 용량 단위입니다.

1 WCU는 초당 최대 1KB 크기의 아이템을 1회 쓸 수 있는 용량입니다. 어떤 모드를 선택해야 할까요?

트래픽이 예측 가능하고 일정하다면 프로비저닝 모드가 유리합니다. 예를 들어 사내 시스템처럼 업무 시간에만 사용되고 야간에는 거의 요청이 없는 경우, 프로비저닝 모드로 비용을 절약할 수 있습니다.

반면 트래픽이 불규칙하거나 예측이 어렵다면 온디맨드 모드가 안전합니다. 스타트업의 신규 서비스, 이벤트성 트래픽이 있는 서비스, MVP 단계의 프로젝트 등이 해당됩니다.

프로비저닝 모드를 사용할 때는 Auto Scaling을 꼭 설정하세요. Auto Scaling은 트래픽에 따라 RCU와 WCU를 자동으로 조절해줍니다.

목표 사용률(예: 70%)을 설정하면, 실제 사용률이 이에 맞춰 용량을 늘리거나 줄입니다. 이렇게 하면 프로비저닝 모드의 비용 효율성과 온디맨드 모드의 유연성을 어느 정도 함께 누릴 수 있습니다.

실무에서 자주 보이는 패턴이 있습니다. 처음에는 온디맨드 모드로 시작해서 트래픽 패턴을 파악합니다.

충분한 데이터가 쌓이면 CloudWatch 지표를 분석해서 평균 RCU/WCU를 계산합니다. 그 다음 프로비저닝 모드 + Auto Scaling으로 전환하면 비용을 20~30% 절감할 수 있습니다.

김개발 씨는 분석 끝에 프로비저닝 모드로 전환하기로 결정했습니다. CloudWatch 데이터를 보니 평균 RCU가 10, WCU가 5 정도였습니다.

Auto Scaling을 설정하고 최소 용량과 최대 용량을 적절히 조절했습니다. 다음 달 청구서에서 DynamoDB 비용이 40%나 줄어있었습니다.

실전 팁

💡 - 새로운 서비스는 온디맨드로 시작하고, 트래픽 패턴 파악 후 프로비저닝으로 전환을 고려하세요

  • 프로비저닝 모드에서는 반드시 Auto Scaling을 설정하세요
  • Reserved Capacity를 구매하면 프로비저닝 모드 비용을 추가로 절감할 수 있습니다

5. Global Secondary Index 활용

김개발 씨에게 새로운 요구사항이 들어왔습니다. "상품을 카테고리별로도 조회하고, 가격순으로도 조회해야 해요." 문제는 테이블의 파티션 키가 productId였다는 것입니다.

"테이블을 새로 만들어야 하나?" 고민하던 김개발 씨에게 박시니어 씨가 말했습니다. "GSI를 추가하면 됩니다."

**GSI(Global Secondary Index)**는 기존 테이블과 다른 파티션 키, 정렬 키로 데이터를 조회할 수 있게 해주는 인덱스입니다. 마치 도서관에서 책을 제목으로도 찾고 저자로도 찾을 수 있는 것처럼, GSI를 추가하면 다양한 조회 패턴을 지원할 수 있습니다.

다음 코드를 살펴봅시다.

// GSI가 포함된 테이블 생성
const tableWithGSI = {
  TableName: "Products",
  KeySchema: [
    { AttributeName: "productId", KeyType: "HASH" }
  ],
  AttributeDefinitions: [
    { AttributeName: "productId", AttributeType: "S" },
    { AttributeName: "category", AttributeType: "S" },
    { AttributeName: "price", AttributeType: "N" }
  ],
  // Global Secondary Index 정의
  GlobalSecondaryIndexes: [{
    IndexName: "CategoryPriceIndex",
    KeySchema: [
      { AttributeName: "category", KeyType: "HASH" },  // GSI 파티션 키
      { AttributeName: "price", KeyType: "RANGE" }     // GSI 정렬 키
    ],
    Projection: { ProjectionType: "ALL" },  // 모든 속성 복사
    ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }
  }],
  BillingMode: "PROVISIONED",
  ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }
};

// GSI를 사용한 쿼리: 특정 카테고리의 상품을 가격순으로 조회
const gsiQuery = {
  TableName: "Products",
  IndexName: "CategoryPriceIndex",
  KeyConditionExpression: "category = :cat",
  ExpressionAttributeValues: { ":cat": "Electronics" }
};

김개발 씨의 상품 카탈로그 서비스가 점점 커지고 있습니다. 처음에는 상품 ID로만 조회하면 됐는데, 이제는 다양한 조회가 필요해졌습니다.

"전자제품 카테고리에서 가격이 낮은 순으로 보여주세요." 이런 요구사항을 어떻게 처리해야 할까요? DynamoDB에서 쿼리는 반드시 파티션 키를 지정해야 합니다.

하지만 기본 테이블의 파티션 키는 productId입니다. category로 쿼리하려면 모든 데이터를 스캔해야 하는데, 이것은 비용도 많이 들고 속도도 느립니다.

바로 이런 상황에서 **GSI(Global Secondary Index)**가 등장합니다. GSI는 마치 책의 색인과 같습니다.

본문은 페이지 순서대로 되어 있지만, 뒤의 색인을 보면 특정 키워드가 어느 페이지에 있는지 빠르게 찾을 수 있습니다. GSI도 마찬가지로, 원래 테이블과 다른 키로 데이터를 빠르게 찾을 수 있게 해줍니다.

GSI는 내부적으로 어떻게 동작할까요? GSI를 만들면 DynamoDB가 자동으로 별도의 테이블을 만들어 데이터를 복사해둡니다.

원본 테이블에 데이터가 추가되거나 수정되면 GSI에도 비동기적으로 반영됩니다. 이 때문에 GSI 조회는 **최종적 일관성(Eventually Consistent)**만 지원합니다.

위 코드에서 GSI 설정을 살펴보겠습니다. IndexName은 인덱스의 이름입니다.

쿼리할 때 이 이름을 지정합니다. KeySchema에서 category를 파티션 키, price를 정렬 키로 설정했습니다.

이제 특정 카테고리의 상품을 가격순으로 조회할 수 있습니다. Projection은 GSI에 어떤 속성을 복사할지 결정합니다.

ALL은 모든 속성을 복사합니다. KEYS_ONLY는 키만, INCLUDE는 지정한 속성만 복사합니다.

필요한 속성만 복사하면 저장 공간과 쓰기 비용을 절약할 수 있습니다. GSI를 사용할 때 주의할 점이 있습니다.

첫째, GSI는 별도의 용량을 소모합니다. 원본 테이블과 별개로 GSI의 RCU/WCU가 필요합니다.

온디맨드 모드에서도 GSI 쓰기는 추가 비용이 발생합니다. 둘째, GSI는 테이블당 최대 20개까지 만들 수 있습니다.

무분별하게 추가하면 쓰기 비용이 급증하고 관리도 어려워집니다. 셋째, GSI 쓰기가 지연되면 원본 테이블 쓰기도 스로틀링될 수 있습니다.

GSI 용량을 충분히 확보해야 합니다. LSI(Local Secondary Index)라는 것도 있습니다.

LSI는 파티션 키는 기본 테이블과 같고, 정렬 키만 다른 인덱스입니다. 테이블 생성 시에만 만들 수 있고, 나중에 추가할 수 없습니다.

강력한 일관된 읽기를 지원하지만 사용 사례가 제한적입니다. 김개발 씨는 CategoryPriceIndex GSI를 추가해서 문제를 해결했습니다.

이제 전자제품 카테고리의 상품을 가격순으로 빠르게 조회할 수 있게 되었습니다. 쿼리 성능이 Scan 대비 100배 이상 빨라졌고, 비용도 크게 절감되었습니다.

실전 팁

💡 - 조회 패턴을 먼저 정의하고, 꼭 필요한 GSI만 추가하세요

  • Projection을 INCLUDE로 설정하면 비용을 절약할 수 있습니다
  • GSI 파티션 키도 핫 파티션이 발생하지 않도록 카디널리티를 고려하세요

6. DynamoDB Streams로 이벤트 처리

김개발 씨의 서비스에 새로운 요구사항이 생겼습니다. "주문이 생성되면 자동으로 재고를 차감하고, 알림도 보내야 해요." 주문 API에서 직접 재고 서비스와 알림 서비스를 호출하면 될까요?

박시니어 씨가 고개를 저었습니다. "그러면 주문 서비스가 다른 서비스에 강하게 결합돼요.

DynamoDB Streams를 써보세요."

DynamoDB Streams는 테이블의 변경 사항을 실시간으로 캡처하는 기능입니다. 데이터가 추가, 수정, 삭제될 때마다 이벤트가 발생하고, Lambda 같은 서비스에서 이를 처리할 수 있습니다.

이를 통해 느슨하게 결합된 이벤트 기반 아키텍처를 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// DynamoDB Streams 활성화된 테이블 생성
const streamEnabledTable = {
  TableName: "Orders",
  KeySchema: [{ AttributeName: "orderId", KeyType: "HASH" }],
  AttributeDefinitions: [{ AttributeName: "orderId", AttributeType: "S" }],
  BillingMode: "PAY_PER_REQUEST",
  // Streams 활성화
  StreamSpecification: {
    StreamEnabled: true,
    StreamViewType: "NEW_AND_OLD_IMAGES"  // 변경 전후 데이터 모두 캡처
  }
};

// Lambda 핸들러: Stream 이벤트 처리
exports.handler = async (event) => {
  for (const record of event.Records) {
    console.log("이벤트 타입:", record.eventName);  // INSERT, MODIFY, REMOVE

    if (record.eventName === "INSERT") {
      const newOrder = record.dynamodb.NewImage;
      // 재고 차감 로직
      await updateInventory(newOrder);
      // 알림 발송 로직
      await sendNotification(newOrder);
    }
  }
};

김개발 씨는 마이크로서비스 아키텍처에 대해 배우고 있었습니다. 주문 서비스, 재고 서비스, 알림 서비스가 각각 분리되어 있는데, 이들을 어떻게 연결해야 할까요?

가장 단순한 방법은 주문 API에서 직접 다른 서비스를 호출하는 것입니다. 하지만 이렇게 하면 문제가 생깁니다.

재고 서비스가 잠시 장애가 나면 주문도 실패합니다. 알림 서비스 응답이 느리면 주문 API도 느려집니다.

서비스들이 강하게 결합되어 버린 것입니다. DynamoDB Streams는 이 문제를 우아하게 해결합니다.

마치 신문 구독 서비스와 같습니다. 신문사는 기사를 작성하고 발행합니다.

구독자들은 각자 원하는 시간에 신문을 받아봅니다. 신문사는 구독자가 누구인지, 언제 읽는지 알 필요가 없습니다.

DynamoDB Streams도 마찬가지입니다. 테이블에 변경이 생기면 이벤트가 발생하고, 구독자(Lambda)가 이를 처리합니다.

Streams는 어떻게 동작할까요? 테이블에서 INSERT, MODIFY, REMOVE가 발생하면 해당 변경 내용이 스트림 레코드로 기록됩니다.

이 레코드는 24시간 동안 보관되며, Lambda가 순서대로 처리합니다. 처리에 실패하면 자동으로 재시도됩니다.

StreamViewType은 어떤 데이터를 캡처할지 결정합니다. KEYS_ONLY는 변경된 아이템의 키만 기록합니다.

NEW_IMAGE는 변경 후의 새 데이터만, OLD_IMAGE는 변경 전의 기존 데이터만 기록합니다. NEW_AND_OLD_IMAGES는 변경 전후 데이터를 모두 기록합니다.

무엇이 어떻게 바뀌었는지 비교해야 할 때 유용합니다. 위의 Lambda 코드를 살펴보겠습니다.

event.Records에는 배치로 전달된 여러 레코드가 들어있습니다. 각 레코드의 eventName으로 INSERT, MODIFY, REMOVE를 구분합니다.

INSERT일 때는 NewImage에 새로 생성된 데이터가 들어있습니다. 이 데이터를 기반으로 재고 차감, 알림 발송 등의 로직을 수행합니다.

실무에서 DynamoDB Streams는 다양하게 활용됩니다. 데이터 동기화: DynamoDB 변경 사항을 Elasticsearch에 동기화하여 전문 검색을 지원합니다.

감사 로그: 모든 변경 이력을 S3에 저장하여 규정 준수에 활용합니다. 알림 시스템: 특정 조건의 데이터가 생성되면 SNS로 알림을 보냅니다.

집계/분석: 변경 데이터를 Kinesis로 보내 실시간 분석에 활용합니다. 주의할 점도 있습니다.

Streams 레코드는 **최소 1회 전달(At-least-once)**을 보장합니다. 즉, 같은 이벤트가 두 번 전달될 수 있습니다.

Lambda 핸들러는 **멱등성(Idempotent)**을 갖도록 설계해야 합니다. 같은 이벤트를 여러 번 처리해도 결과가 같아야 합니다.

또한 Lambda 처리가 실패하면 해당 샤드의 처리가 멈춥니다. Dead Letter Queue를 설정하여 실패한 이벤트를 별도로 관리하는 것이 좋습니다.

김개발 씨는 DynamoDB Streams와 Lambda를 연결해서 이벤트 기반 아키텍처를 구현했습니다. 이제 주문 서비스는 주문만 저장하면 됩니다.

재고 차감과 알림 발송은 자동으로 처리됩니다. 서비스 간 결합도가 낮아져서 유지보수가 훨씬 쉬워졌습니다.

DynamoDB Streams는 서버리스 아키텍처의 핵심 구성 요소입니다. 이를 통해 확장 가능하고 유연한 이벤트 기반 시스템을 구축할 수 있습니다.

실전 팁

💡 - Lambda 핸들러는 반드시 멱등성을 갖도록 설계하세요

  • 실패한 이벤트 처리를 위해 Dead Letter Queue를 설정하세요
  • 대용량 처리가 필요하면 Kinesis Data Streams와 연결하는 것도 고려해보세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#AWS#DynamoDB#NoSQL#Serverless#CloudDatabase#AWS,DynamoDB,Database,NoSQL

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

CloudTrail로 AWS 활동 추적 및 감사 완벽 가이드

AWS CloudTrail을 활용하여 누가, 언제, 무엇을 했는지 추적하고 감사하는 방법을 알아봅니다. 보안 사고 대응과 규정 준수를 위한 필수 서비스입니다.

CloudWatch로 리소스 모니터링 및 자동화 완벽 가이드

AWS CloudWatch를 활용하여 인프라와 애플리케이션을 모니터링하고 자동화하는 방법을 알아봅니다. 지표 수집부터 로그 분석, 이벤트 기반 자동화까지 실무에 필요한 핵심 기능을 다룹니다.

IAM으로 AWS 보안 및 권한 체계 구축

AWS 클라우드 환경에서 보안의 핵심인 IAM(Identity and Access Management)을 처음부터 끝까지 배웁니다. 사용자, 그룹, 역할, 정책의 개념부터 실무에서 바로 적용할 수 있는 보안 설정까지, 초급 개발자도 쉽게 따라할 수 있도록 설명합니다.

ElastiCache로 인메모리 캐싱 구현하기

AWS ElastiCache를 활용하여 애플리케이션의 성능을 획기적으로 개선하는 방법을 알아봅니다. Redis 클러스터 구성부터 실제 연동까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.

VPC 엔드포인트로 AWS 서비스 프라이빗 연결

AWS 서비스에 프라이빗하게 접근하는 VPC 엔드포인트의 개념과 설정 방법을 알아봅니다. 게이트웨이 엔드포인트와 인터페이스 엔드포인트의 차이점, S3 연결 설정, 그리고 비용 절감 효과까지 실무 중심으로 설명합니다.