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 데이터의 특징
- 시간에 따라 생성: 데이터는 일정한 주기(예: 초, 분, 시간, 일, 월 등)로 생성됩니다.
- 읽기 및 쓰기 집중: 최신 데이터에 대한 읽기 및 쓰기 요청이 많습니다.
- 오래된 데이터 관리: 오래된 데이터는 자주 조회되지 않으므로 보관 방식이 중요합니다.
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. 성능 최적화 요약
- 핫 파티션 방지: 파티션 키를 균등 분포하도록 설계.
- TTL 활용: 오래된 데이터의 자동 삭제로 테이블 크기 관리.
- 아카이브: 오래된 데이터를 S3로 이동하여 비용 절감.
- 인덱스 최소화: 보조 인덱스(GSI/LSI)는 필요한 경우에만 추가.
Time-Series 데이터 관리는 DynamoDB에서 자주 쓰이는 패턴으로, 위 전략을 적용하면 성능을 극대화할 수 있습니다.
로컬 보조 인덱스 (Local Secondary Index, LSI)
로컬 보조 인덱스(LSI)는 DynamoDB에서 같은 파티션 키(Partition Key)를 공유하면서 다른 정렬 키(Sort Key)를 사용해 데이터를 조회할 수 있게 해주는 기능입니다. 기본 테이블에 의존하는 보조 인덱스이며, 쿼리 효율성을 높이는 데 유용합니다.
LSI의 주요 특징
- 파티션 키 재사용: LSI는 기본 테이블과 동일한 파티션 키를 사용합니다.
- 다른 정렬 키 사용: LSI를 정의할 때 다른 속성을 정렬 키(Sort Key)로 설정할 수 있습니다.
- 테이블 생성 시 정의: LSI는 테이블을 생성할 때만 추가할 수 있으며, 이후에는 수정할 수 없습니다.
- 최대 5개: 하나의 테이블에서 최대 5개의 LSI를 정의할 수 있습니다.
- 항목 크기 제한: LSI에 의해 영향을 받는 기본 테이블의 항목 크기는 400KB로 제한됩니다.
LSI의 구조
- 기본 테이블
- 파티션 키(Partition Key): 데이터의 기본 그룹화 기준.
- 기본 정렬 키(Sort Key): 기본적으로 데이터를 정렬하는 기준.
- 로컬 보조 인덱스(LSI)
- 동일한 파티션 키를 사용하지만, 다른 정렬 키를 정의하여 다양한 쿼리 시나리오를 지원.
LSI 사용 예제
사용 시나리오
온라인 학습 플랫폼에서 사용자별 활동 로그를 저장한다고 가정합니다.
- 기본 요구사항:
- 사용자의 활동 데이터를 시간순으로 정렬하여 조회.
- 사용자의 특정 활동 유형(예: “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는 특정 파티션 내에서 다양한 정렬 조건으로 데이터를 효율적으로 검색해야 하는 경우 유용합니다.