DynamoDB 쿼리 최적화를 위한 디자인 패턴 by ChatGPT

Namu CHO
13 min readJan 4, 2025

--

DynamoDB는 스키마리스 NoSQL 데이터베이스로, 특정 데이터 모델링과 접근 방식이 성능에 큰 영향을 미칩니다. 최적화를 위해 사용되는 몇 가지 디자인 패턴을 아래와 같이 정리할 수 있습니다.

1. 데이터 모델링 최적화

DynamoDB의 성능은 데이터 모델링에 크게 좌우됩니다. 이를 위해 몇 가지 핵심 원칙을 따라야 합니다.

a. Access Pattern 설계 우선

  • 데이터 모델을 설계하기 전에 어떤 쿼리를 실행할지 정의하세요.
  • DynamoDB는 Access Pattern 중심이므로 모든 쿼리와 검색 시나리오를 미리 정의한 후 테이블을 설계해야 합니다.

b. 단일 테이블 디자인

  • 단일 테이블 전략을 통해 여러 엔터티를 한 테이블에 저장합니다. 파티션 키와 정렬 키를 적절히 사용해 데이터를 구분하세요.
  • 예:

c. 키 설계의 중요성

  • 파티션 키 (Partition Key): 데이터 분포를 균일하게 하기 위해 고유한 값을 사용하세요. 예를 들어, userId 대신 userId + 날짜 같은 복합 키를 고려하세요.
  • 정렬 키 (Sort Key): 쿼리의 세부 조건에 따라 데이터를 그룹화할 수 있도록 설계하세요.

2. 주요 디자인 패턴

a. Composite Key Pattern

  • 파티션 키와 정렬 키를 결합하여 효율적인 쿼리를 지원합니다.
  • 예: userId + 날짜를 파티션 키로 사용하고, 세부 데이터를 정렬 키로 나눠 날짜별로 데이터를 검색합니다.

b. Secondary Index 활용

  • 글로벌 보조 인덱스 (GSI):
  • 다른 속성으로 검색할 필요가 있을 때 사용합니다.
  • 쿼리 성능에 영향을 주지 않도록 인덱스 업데이트를 최소화하세요.
  • 로컬 보조 인덱스 (LSI):
  • 동일한 파티션 키에서 다른 정렬 키로 검색할 때 유용합니다.

c. Query와 Scan 최소화

  • Query: 파티션 키를 기준으로 검색하며 효율적입니다.
  • Scan: 모든 데이터를 검색하므로 비용이 비싸고 느립니다. 반드시 피하세요!

3. 특정 사용 사례를 위한 패턴

a. Time-Series 데이터 관리

  • 데이터가 날짜별로 쌓이는 경우, 시간 단위로 테이블 분할을 고려합니다.
  • 예: orders_2023 테이블, orders_2024 테이블로 나누어 오래된 데이터에 대한 쿼리를 줄입니다.

b. Sparse Index Pattern

  • 모든 항목에 동일한 속성이 필요하지 않을 경우, 특정 속성에 대해 GSI를 활용해 효율적인 검색을 만듭니다.

c. Adjacency List Pattern

  • 복잡한 관계형 데이터를 관리할 때 사용합니다.
  • 예: 그래프 데이터를 DynamoDB로 표현하려면 노드와 엣지를 파티션 키와 정렬 키로 모델링합니다.

d. Aggregation Pattern

  • DynamoDB는 집계 연산(예: SUM, AVG)을 기본으로 제공하지 않으므로, 사전 계산된 집계 데이터를 저장하는 방식으로 쿼리를 최적화합니다.
  • 예: 판매 데이터를 매번 계산하지 않고, MonthlySales 또는 DailySales를 별도로 업데이트하여 빠르게 집계 결과를 조회합니다.

e. Materialized View Pattern

  • DynamoDB는 조인을 지원하지 않으므로, 자주 사용되는 쿼리에 필요한 데이터를 미리 준비하는 방식입니다.
  • 예: 사용자 정보와 주문 내역을 별도로 저장하지 않고, 한 레코드에 통합하여 저장:
{
"PK": "User#123",
"SK": "OrderSummary",
"UserName": "John Doe",
"Orders": [
{"OrderId": "Order#1", "Total": 100},
{"OrderId": "Order#2", "Total": 200}
]
}

4. 데이터 모델링 사례

a. eCommerce 애플리케이션

요구사항:

  • 사용자별로 주문 내역을 검색.
  • 특정 날짜 범위 내 주문을 검색.

데이터 모델:

쿼리:

  • 특정 사용자의 주문 내역 검색:
const params = {
TableName: "Orders",
KeyConditionExpression: "PK = :pk",
ExpressionAttributeValues: { ":pk": "User#123" }
};
dynamoDB.query(params);
  • 날짜 범위로 검색:
const params = {
TableName: "Orders",
KeyConditionExpression: "PK = :pk AND SK BETWEEN :startDate AND :endDate",
ExpressionAttributeValues: {
":pk": "User#123",
":startDate": "Order#2023-01-01",
":endDate": "Order#2023-12-31"
}
};
dynamoDB.query(params);

5. 성능 및 비용 최적화 전략

a. 읽기/쓰기 용량 최적화

  • 프로비저닝된 용량 모드온디맨드 용량 모드 중 트래픽 패턴에 맞는 옵션을 선택하세요.
  • 예: 일정하지 않은 트래픽에는 온디맨드 모드가 적합.

b. 파티션 키 균등 분포

  • 핫 파티션 문제를 피하기 위해 랜덤 접미사해싱된 파티션 키를 사용하세요.
  • 예: User#123-A, User#123-B와 같이 접미사를 추가해 데이터 분산을 유도.

c. TTL(Time to Live) 활용

  • 오래된 데이터를 자동 삭제하여 테이블 크기를 줄이고 쿼리 성능을 유지하세요.

6. DynamoDB의 장단점 요약

장점

  • 무한 확장성: 데이터 크기가 증가해도 성능이 유지됩니다.
  • 저지연 읽기/쓰기: 초당 수백만 요청을 처리할 수 있습니다.
  • 서버리스 아키텍처: 관리 오버헤드가 적습니다.

단점

  • 복잡한 데이터 관계 처리의 제한: 조인이 없으므로 데이터 중복을 감수해야 할 때가 많습니다.
  • 설계 의존성: 데이터 액세스 패턴을 정확히 이해하고 설계해야 최적화 가능.

Time-Series 데이터 관리

Time-Series 데이터 관리시간 기반 데이터를 저장하고 조회하는 데 최적화된 방식으로, DynamoDB에서 이 패턴을 효과적으로 활용하면 성능과 비용을 크게 개선할 수 있습니다.

Time-Series 데이터의 특징

  1. 시간에 따라 생성: 데이터는 일정한 주기(예: 초, 분, 시간, 일, 월 등)로 생성됩니다.
  2. 읽기 및 쓰기 집중: 최신 데이터에 대한 읽기 및 쓰기 요청이 많습니다.
  3. 오래된 데이터 관리: 오래된 데이터는 자주 조회되지 않으므로 보관 방식이 중요합니다.

DynamoDB에서 Time-Series 데이터를 설계하는 방법

1. 테이블 분할 전략 (Table Partitioning)

시간에 따라 데이터를 물리적으로 다른 테이블에 저장하여 성능을 개선합니다.

  • 방법: 데이터를 기간 단위로 테이블을 나눕니다.
  • 예: logs_2023_01, logs_2023_02 등 월 단위 테이블 생성.
  • 장점:
  • 최신 테이블에 집중된 쿼리로 성능 최적화.
  • 오래된 테이블을 쉽게 백업하거나 삭제 가능.
  • 구현:
  • 애플리케이션에서 데이터 생성 시점에 따라 테이블 이름을 동적으로 결정.
  • 예: TableName = logs_${currentYear}_${currentMonth}

2. 복합 키 설계 (Composite Key Design)

단일 테이블에 데이터를 저장하면서 시간 기반으로 정렬되도록 키를 설계합니다.

  • 파티션 키: 주로 특정 그룹(예: userId, deviceId)을 기준으로 설정.
  • 정렬 키: 시간 기반 값(예: timestamp)을 사용하여 데이터를 정렬.
  • 쿼리: 최신 데이터 검색:
const params = {
TableName: "TimeSeriesTable",
KeyConditionExpression: "PK = :pk AND SK BETWEEN :start AND :end",
ExpressionAttributeValues: {
":pk": "Device#123",
":start": "2023-01-01T12:00",
":end": "2023-01-01T12:59"
}
};
dynamoDB.query(params);

3. 파티션 키 해싱 (Sharded Partition Key)

데이터가 특정 파티션에 집중되는 핫 파티션 문제를 방지하기 위해 파티션 키를 샤딩합니다.

  • 방법: 파티션 키에 해시 값을 추가하여 데이터 분산을 유도.
  • 예: PK = Device#123-1, Device#123-2
  • 쿼리: 여러 샤드에서 병렬로 데이터를 조회한 뒤 애플리케이션에서 병합.

4. 데이터 수명 주기 관리

a. TTL (Time to Live) 설정

오래된 데이터를 자동으로 삭제합니다.

  • 설정 방법: DynamoDB 테이블에서 TTL 필드(예: expirationTime)를 활성화하고, 삭제 대상이 되는 항목의 Unix 타임스탬프를 저장.
  • 장점:
  • 오래된 데이터를 수동으로 관리할 필요가 없습니다.
  • 테이블 크기 관리에 유용.

b. S3로 아카이브

오래된 데이터를 DynamoDB에서 삭제하고 S3에 백업합니다.

  • 방법:
  • AWS Lambda를 트리거로 설정하여 TTL 만료 시 데이터를 S3로 이동.
  • S3에 저장된 데이터는 Athena 또는 QuickSight로 분석 가능.

5. Time-Series 데이터의 예제 사용 사례

a. IoT 데이터 저장

  • 요구사항: 수백만 IoT 디바이스에서 실시간으로 센서 데이터를 수집.
  • 설계:
  • Partition Key: DeviceId (예: Device#123)
  • Sort Key: Timestamp (예: 2023-01-01T12:00)
  • TTL: 30일로 설정하여 오래된 데이터를 자동 삭제.

b. 애플리케이션 로그

  • 요구사항: 애플리케이션에서 발생하는 로그를 저장하고 최근 7일의 데이터를 조회.
  • 설계:
  • 테이블 분할: 월별로 로그 테이블 생성 (예: Logs_2023_01, Logs_2023_02).
  • 오래된 테이블은 S3에 백업.

c. 사용자 활동 추적

  • 요구사항: 사용자의 활동(예: 페이지 방문, 클릭)을 시간별로 저장.
  • 설계:
  • Partition Key: UserId (예: User#456)
  • Sort Key: ActionTimestamp (예: 2023-01-01T12:00)
  • 최신 데이터를 빠르게 검색.

6. 성능 최적화 요약

  1. 핫 파티션 방지: 파티션 키를 균등 분포하도록 설계.
  2. TTL 활용: 오래된 데이터의 자동 삭제로 테이블 크기 관리.
  3. 아카이브: 오래된 데이터를 S3로 이동하여 비용 절감.
  4. 인덱스 최소화: 보조 인덱스(GSI/LSI)는 필요한 경우에만 추가.

Time-Series 데이터 관리는 DynamoDB에서 자주 쓰이는 패턴으로, 위 전략을 적용하면 성능을 극대화할 수 있습니다.

로컬 보조 인덱스 (Local Secondary Index, LSI)

로컬 보조 인덱스(LSI)는 DynamoDB에서 같은 파티션 키(Partition Key)를 공유하면서 다른 정렬 키(Sort Key)를 사용해 데이터를 조회할 수 있게 해주는 기능입니다. 기본 테이블에 의존하는 보조 인덱스이며, 쿼리 효율성을 높이는 데 유용합니다.

LSI의 주요 특징

  1. 파티션 키 재사용: LSI는 기본 테이블과 동일한 파티션 키를 사용합니다.
  2. 다른 정렬 키 사용: LSI를 정의할 때 다른 속성을 정렬 키(Sort Key)로 설정할 수 있습니다.
  3. 테이블 생성 시 정의: LSI는 테이블을 생성할 때만 추가할 수 있으며, 이후에는 수정할 수 없습니다.
  4. 최대 5개: 하나의 테이블에서 최대 5개의 LSI를 정의할 수 있습니다.
  5. 항목 크기 제한: LSI에 의해 영향을 받는 기본 테이블의 항목 크기는 400KB로 제한됩니다.

LSI의 구조

  • 기본 테이블
  • 파티션 키(Partition Key): 데이터의 기본 그룹화 기준.
  • 기본 정렬 키(Sort Key): 기본적으로 데이터를 정렬하는 기준.
  • 로컬 보조 인덱스(LSI)
  • 동일한 파티션 키를 사용하지만, 다른 정렬 키를 정의하여 다양한 쿼리 시나리오를 지원.

LSI 사용 예제

사용 시나리오

온라인 학습 플랫폼에서 사용자별 활동 로그를 저장한다고 가정합니다.

  • 기본 요구사항:
  1. 사용자의 활동 데이터를 시간순으로 정렬하여 조회.
  2. 사용자의 특정 활동 유형(예: “VideoWatched”)을 기준으로 조회.

테이블 설계

기본 테이블 스키마:

  • Partition Key: UserId (사용자 ID)
  • Sort Key: Timestamp (활동 시간)

로컬 보조 인덱스 (LSI):

  • Partition Key: 동일한 UserId
  • Sort Key: ActivityType (활동 유형)

데이터 예제:

LSI 정의:

  • LSI 이름: ActivityTypeIndex
  • Partition Key: UserId
  • Sort Key: ActivityType

쿼리 예제

1. 기본 테이블에서 활동 시간 기준으로 조회:

  • 특정 사용자의 모든 활동을 시간 순으로 가져옵니다.
const params = {
TableName: "UserActivityTable",
KeyConditionExpression: "UserId = :userId AND Timestamp BETWEEN :start AND :end",
ExpressionAttributeValues: {
":userId": "User#1",
":start": "2025-01-01T00:00",
":end": "2025-01-02T23:59"
}
};
dynamoDB.query(params);

2. LSI를 사용해 특정 활동 유형 조회:

  • 특정 사용자의 “VideoWatched” 활동만 조회.
const params = {
TableName: "UserActivityTable",
IndexName: "ActivityTypeIndex",
KeyConditionExpression: "UserId = :userId AND ActivityType = :activityType",
ExpressionAttributeValues: {
":userId": "User#1",
":activityType": "VideoWatched"
}
};
dynamoDB.query(params);

LSI의 장점

1. 다양한 정렬 조건 지원:

  • 파티션 키는 그대로 유지하면서, 다양한 정렬 키를 사용해 데이터를 검색할 수 있습니다.

2. 저비용 인덱스:

  • 기본 테이블과 데이터를 공유하므로 추가적인 저장 공간 비용이 낮습니다.

3. 빠른 쿼리:

  • 특정 파티션 내에서의 검색 성능이 뛰어납니다.

LSI의 단점

1. 설계의 유연성 부족:

  • 테이블 생성 이후에는 LSI를 추가하거나 수정할 수 없습니다.

2. 항목 크기 제한:

  • 기본 테이블 항목 크기가 400KB를 초과하면 DynamoDB는 항목을 저장할 수 없습니다.

3. 동일 파티션 키만 사용 가능:

  • LSI는 항상 기본 테이블의 파티션 키와 동일해야 하므로, 제한된 검색 시나리오에 적합합니다.

LSI와 GSI(Global Secondary Index)의 비교

사용 시 유의사항

1. 테이블 설계 시 필요한 인덱스 예측:

  • 테이블 생성 단계에서 LSI를 정의하므로, 앞으로 필요한 모든 정렬 키를 미리 고려해야 합니다.

2. 항목 크기 관리:

  • 저장하려는 데이터가 400KB를 초과하지 않도록 데이터 모델을 설계하세요.

LSI는 특정 파티션 내에서 다양한 정렬 조건으로 데이터를 효율적으로 검색해야 하는 경우 유용합니다.

--

--

Namu CHO
Namu CHO

Written by Namu CHO

외노자 개발자 나무 🇸🇬

No responses yet