Amazon DynamoDB: Internal Architecture 분석


Amazon DynamoDB: Internal Architecture 분석

Amazon DynamoDB는 완전 관리형 NoSQL 데이터베이스 서비스로, 대규모 애플리케이션에 필요한 일관된 한 자릿수 밀리초 성능과 뛰어난 확장성을 제공합니다. 이 포스팅에서는 DynamoDB의 핵심 요소, 내부 요청 처리 메커니즘, 데이터 필터링 방식, 그리고 인덱싱 메커니즘을 분석하여 DynamoDB가 어떻게 작동하는지 설명합니다.


1. DynamoDB의 핵심 요소

DynamoDB는 키-값 및 문서 데이터 모델을 지원하며, 트랜잭션 워크로드에 최적화되어 있습니다. 이러한 DynamoDB의 기반이 되는 핵심 요소들은 다음과 같습니다.

  • 테이블 : DynamoDB 테이블은 아이템들의 논리적 그룹화를 나타냅니다. 관계형 데이터베이스의 테이블과 유사하지만, NoSQL 특성상 모든 아이템이 사전 정의된 스키마를 따를 필요가 없어 유연성이 높습니다.
  • 아이템 : 아이템은 테이블 내의 개별 레코드를 의미하며, 고유하게 식별되는 속성들의 집합입니다. 각 아이템은 최대 400KB의 크기 제한을 가집니다.
  • 속성: 속성은 아이템을 구성하는 기본적인 데이터 요소로, 키-값 쌍의 형태를 가집니다. DynamoDB는 schemaless이므로, 아이템마다 속성이 다를 수 있어 데이터 모델링의 유연성을 극대화합니다.
  • 기본 키 (Primary Key): 모든 DynamoDB 테이블은 기본 키를 가져야 하며, 이는 테이블 내의 각 아이템을 고유하게 식별합니다. 기본 키는 단일 속성으로 구성된 파티션 키 또는 파티션 키와 정렬 키의 조합으로 구성될 수 있습니다.
  • Partition Key (Hash Key): 파티션 키는 DynamoDB가 아이템을 물리적으로 저장할 위치를 결정하는 데 사용되는 단일 속성입니다. DynamoDB는 파티션 키의 값을 내부 해시 함수에 입력하여 아이템이 저장될 특정 파티션을 결정합니다. 이 해싱 메커니즘은 데이터를 클러스터 전반에 걸쳐 균등하게 분산하여 효율적인 데이터 관리를 가능하게 합니다.
  • Sort Key (Range Key, Optional): 정렬 키는 파티션 키와 함께 복합 기본 키를 형성하는 선택적 속성입니다. 동일한 파티션 키 값을 가진 아이템들은 정렬 키 값을 기준으로 정렬되어 물리적으로 가깝게 저장됩니다. 이는 특정 파티션 내에서 효율적인 범위 쿼리 및 정렬된 데이터 검색을 가능하게 합니다.
  • 파티션: 파티션은 DynamoDB 테이블을 위한 전용 스토리지 할당 단위이며, SSD에 데이터를 저장하는 물리적 단위입니다. 각 파티션은 테이블 데이터의 일부를 호스팅하며, AWS 리전 내 여러 가용 영역에 자동으로 복제되어 높은 내구성과 가용성을 보장합니다. DynamoDB는 모든 파티션 관리를 자동으로 처리합니다. 각 파티션은 최대 10GB의 데이터를 저장할 수 있으며, 단일 파티션은 최대 3,000 읽기 용량 단위(RCUs) 또는 1,000 쓰기 용량 단위(WCUs)의 처리량을 처리할 수 있습니다.
  • Capacity Units: DynamoDB의 처리량은 읽기 용량 단위(RCUs)와 쓰기 용량 단위(WCUs)로 측정됩니다.
    읽기 용량 단위 (Read Capacity Units, RCUs): 강력한 일관성 읽기는 4KB 크기 아이템당 1 RCU를 소비하고, 결과적 일관성 읽기는 0.5 RCU를 소비하며, 트랜잭션 읽기는 2 RCU를 소비합니다. 아이템 크기는 다음 4KB 배수로 반올림되어 계산됩니다.– 쓰기 용량 단위 (Write Capacity Units, WCUs): 표준 쓰기는 1KB 크기 아이템당 1 WCU를 소비하고, 트랜잭션 쓰기는 2 WCU를 소비합니다. 아이템 크기는 다음 1KB 배수로 반올림됩니다.
  • Capacity Modes: DynamoDB는 워크로드의 특성에 맞춰 온디맨드 용량 모드와 프로비저닝된 용량 모드의 두 가지 용량 모드를 제공합니다. 온디맨드 모드는 예측 불가능한 워크로드에 적합하며 사용량에 따라 요금이 부과되고, 프로비저닝된 모드는 예측 가능한 워크로드에 적합하며 사용자가 미리 용량을 지정할 수 있습니다.

2. DynamoDB의 내부 요청 분석

DynamoDB는 분산된 아키텍처에서 작동하며, 데이터는 여러 물리적 노드에 걸쳐 분할되어 효율적인 데이터 관리와 선형적 확장성을 보장합니다. 아래 그림은 앞서 살펴본 기본적 핵심 요소와 아래 서술할 요청 라우터를 결합 시킨 aws dynamoDB paper에서 제공하는 이미지입니다. 주황색 박스로 처리된 부분들은 Request Router와 통신하는 각기 다른 기능을 제공하는 마이크로 서비스입니다.

아래 이미지를 앞선 앞선 이미지를 참고하여 핵심 요소들과 함께 이해할 수 있게 그린 그림입니다. 아래의 요청 흐름은 write 요청 또는 Strong consitancy를 설정해 항상 최신값을 읽어야 하는 요청을 표현했습니다.

요청 라우터의 핵심 기능

클라이언트가 DynamoDB에 요청을 보내면, 이 요청은 요청 라우터 서비스에 도달합니다. 요청 라우터의 주된 책임은 들어오는 각 요청을 관련 데이터를 보유하는 적절한 파티션(스토리지 노드)으로 효율적으로 라우팅하는 것입니다. 이러한 라우팅 결정을 위해 요청 라우터는 다음과 같은 내부 마이크로 서비스들과 상호 작용합니다.

  • 인증 서비스: 요청의 유효성을 검사하고 AWS IAM에 대해 권한 부여를 확인하여 주어진 테이블에서 작업이 허용되는지 확인합니다.
  • 메타데이터 서비스: 라우터는 "메타데이터 서비스"를 참조하여 중요한 라우팅 정보를 가져옵니다. 이 서비스는 테이블의 기본 키와 해당 스토리지 노드 간의 매핑을 저장하며, 인덱스 및 복제 그룹에 대한 세부 정보도 포함합니다.
  • 글로벌 승인 제어: 라우팅하기 전에 요청 라우터는 글로벌 승인 제어와도 확인하여 요청이 테이블의 프로비저닝된 또는 온디맨드 처리량 한도를 초과하지 않는지 확인합니다.
  • 스토리지 서비스 상호 작용: 모든 검사가 통과되면 요청 라우터는 기본 스토리지 서비스를 호출하여 지정된 스토리지 노드 풀에서 실제 데이터 저장 또는 검색 작업을 수행합니다.

요청 라우터의 기능은 기본적으로 요청을 기본 키를 기반으로 미리 결정된 올바른 물리적 위치(파티션)로 디스패치하는 것입니다. 이는 요청 경로에서 데이터 조작이나 분석을 수행하는 것이 아닙니다. 이러한 설계 선택은 규모에 따른 일관된 낮은 지연 시간 성능을 달성하는 데 중요하며, 라우팅 계층에서 계산 집약적인 작업을 피합니다.

파티셔닝 및 데이터 분산이 요청 처리에 미치는 영향

DynamoDB는 데이터 파티셔닝을 자동으로 관리하며, 테이블에 대한 스토리지를 할당하고 고가용성 및 내구성을 위해 AWS 리전 내 최소 세 가지 다른 가용 영역(AZ)에 복제합니다.

  • 파티션 키(Partition Key): DynamoDB는 파티션 키 값에 대한 내부 해시 함수를 사용하여 항목이 저장될 파티션(DynamoDB 내부의 물리적 스토리지)을 결정합니다.
  • 정렬 키(Sort Key): 동일한 파티션 키 값을 가진 모든 항목은 정렬 키가 있는 경우 정렬 키 값에 따라 "정렬된 순서"로 함께 저장됩니다.
  • 리더-팔로워 모델: DynamoDB는 일관성을 유지하기 위해 각 파티션에 대해 "리더-팔로워 모델"을 사용합니다. 쓰기 요청이 이루어지면 파티션의 리더 노드로 전달되며, 리더는 쓰기를 처리하고 WAL(Write-Ahead Log)을 사용하여 복제본으로의 동기식 전파를 보장합니다. 내부적으로 PAXOS 알고리즘에 의해 합의 알고리즘이 구현됩니다.

요청 라우터의 역할은 단순히 올바른 파티션과 해당 리더를 식별하는 것입니다. 실제 일관성 및 내구성 로직(WAL, 동기식 전파)은 스토리지 노드 자체의 파티션 수준에 있으며 리더 및 팔로워 복제본에 의해 관리됩니다.

Consistant Hashing? Key-Range?

위 이미지는 DynamoDB의 개념적 근간인 Dynamo라는 분산 시스템에 대한 논문에서 가져왔습니다. 논문의 내용을 읽어보면 단순히 위 그림과 같이 하나의 파티션이 해시링 위의 한 노드에만 매핑되는 방식이 아니라, N개의 Virtual Node를 해시링 위에 배치하는 방식을 사용합니다. 그래서 저 또한 DynamoDB가 당연히 Dynamo와 동일하게 Consistant Hashing을 통해 물리적 파티션에 데이터를 분산시키는것으로 이해했습니다.

하지만 아래 두 결정적인 경우를 확인하며, DynamoDB에서는 Key Range 기반으로 데이터를 분산시키는것을 확인했습니다. 먼저 DynamoDB Paper에서 발췌한 부분입니다.

A DynamoDB table is divided into multiple partitions to handle the throughput and storage requirements of the table. Each partition of the table hosts a disjoint and contiguous part of the table’s key-range.

간단히 이야기하면 각각의 파티션들은 table의 key-range에 의해 연속적으로 분리되어 있는 구조라고 얘기합니다. 즉, key-range라는 용어를 사용합니다. 또한 re:Invent 세션에서 DynamoDB와 관련된 발표 영상에서 아래와 같은 이미지를 사용합니다. 각각의 파티션에 아이템들이 분산되어 있고 파티션들은 자신이 가지는 key-range가 존재합니다.

여기서 나오는 key-range의 개념은 redis의 hash-slot과 동일한 개념이라 생각합니다. 해시 함수를 적용한 값이 포함되는 key-range에 해당 key의 data가 저장되는 방식입니다. 이 또한 파티션(노드)이 추가 또는 제거 되더라도 변경된 key-range 일부에 대한 데이터들만 옮겨주면 됩니다.


3. DynamoDB의 읽기 및 쓰기 작업 내부 동작

DynamoDB는 분산 시스템의 복잡성을 추상화하면서도 높은 성능과 일관성을 제공하기 위해 정교한 읽기 및 쓰기 작업 흐름을 갖추고 있습니다.

3.1. 쓰기 작업 흐름 (Write Operation Flow)

DynamoDB에서 쓰기 요청이 처리되는 과정은 다음과 같은 단계를 거칩니다.

  1. 요청 라우터 (Request Router) 수신: 클라이언트로부터 쓰기 요청이 도착하면, 먼저 요청 라우터 서비스가 이를 수신합니다. 요청 라우터는 AWS Identity and Access Management (IAM)과 연동된 인증 서비스를 호출하여 요청의 유효성과 사용자 권한을 확인합니다.
  2. 메타데이터 서비스 조회 및 파티션 식별: 요청 라우터는 메타데이터 서비스로부터 테이블, 인덱스, 복제 그룹에 대한 라우팅 정보를 가져와 요청을 처리할 적절한 스토리지 노드(즉, 파티션)를 식별합니다. DynamoDB는 파티션 키 값의 해시를 계산하여 아이템이 저장될 특정 파티션을 결정합니다.
  3. 글로벌 승인 제어 (Global Admission Control): 요청 라우터는 전역 승인 제어를 통해 요청이 테이블의 프로비저닝된 또는 온디맨드 처리량 한도를 초과하지 않는지 확인합니다.
  4. 스토리지 서비스 지시 및 리더 복제본 처리: 모든 검증이 완료되면, 요청 라우터는 스토리지 서비스에 데이터를 저장하도록 지시합니다. DynamoDB의 각 파티션은 여러 가용 영역에 분산된 복제본(replica) 그룹을 형성하며, 이 그룹 내에는 하나의 리더 복제본이 존재합니다. 모든 쓰기 요청은 이 리더 복제본을 통해 처리됩니다. 리더는 Multi-Paxos 알고리즘을 사용하여 선출되며, 시스템이 어떤 복제본이 리더 역할을 수행할지에 대해 합의하도록 보장합니다.
  5. Write-Ahead Log (WAL) 기록 및 전파: 리더 복제본은 쓰기 요청을 받으면, 해당 작업을 자체 **Write-Ahead Log (WAL)**에 기록합니다. WAL은 각 복제본의 쓰기 작업을 기록하는 핵심 구성 요소로, 데이터의 내구성을 보장하고 장애 발생 시 복구를 가능하게 합니다. 리더는 WAL에 기록한 후, 이 로그 레코드를 팔로워 복제본들에게 전파합니다.
  6. 동기식 쓰기 전파 및 클라이언트 응답: 각 팔로워 복제본도 이 요청을 자체 WAL에 기록하고 수신 확인(ACK)을 리더에게 보냅니다. 리더는 모든 복제본으로부터 (또는 쿼럼을 충족하는 수의 복제본으로부터) WAL 기록에 대한 수신 확인을 받은 후에야 클라이언트에게 쓰기 성공 응답을 보냅니다. 이는 쓰기 작업이 복제본들 간에 동기적으로 전파되어 강력한 일관성과 내구성을 보장함을 의미합니다. 실제 데이터가 메인 데이터베이스에 영구적으로 저장되는 과정은 비동기적으로 발생할 수 있습니다. 추가적인 내구성을 위해 WAL은 주기적으로 S3에 아카이빙됩니다.
  7. 쓰기 가용성 유지: 파티션의 쓰기 가용성은 건강한 리더와 서로 다른 AZ에 분산된 복제본들로 구성된 건강한 쓰기 쿼럼(일반적으로 3개 중 2개)의 존재에 달려 있습니다. 하나의 복제본이 다운되더라도, 리더는 즉시 새로운 로그 복제본을 그룹에 추가하여 쓰기 쿼럼이 항상 유지되도록 신속하게 대응합니다.

요청 예시 (PutItem API):

{
    "TableName": "Users", // 필수: 작업 대상 테이블 이름
    "Item": {             // 필수: 추가하거나 업데이트할 아이템 데이터 (Map<String, AttributeValue>)
        "UserId": {"S": "user123"},     // 필수: 파티션 키 (기본 키의 일부)
        "Username": {"S": "Alice"},     // 선택: 아이템의 다른 속성 (유연한 스키마)
        "Email": {"S": "alice@example.com"} // 선택: 아이템의 다른 속성
    },
    "ReturnConsumedCapacity": "TOTAL" // 선택: 소비된 용량 단위 정보 반환 여부 ("NONE", "INDEXES", "TOTAL")
    // 이 외에도 ConditionExpression (선택), ReturnValues (선택), ReturnItemCollectionMetrics (선택) 등 다양한 선택 인자가 존재
}

PutItem 요청은 "Users" 테이블에 "UserId"를 파티션 키로 하여 새로운 아이템을 추가하거나 기존 아이템을 대체합니다. 요청 라우터는 "user123"이라는 파티션 키 값을 해싱하여 해당 아이템이 저장될 파티션을 식별하고, 해당 파티션의 리더 복제본에게 요청을 전달합니다. 리더는 WAL에 쓰기 작업을 기록하고 다른 복제본으로 로그를 동기식으로 전파한 후, 클라이언트에게 성공 응답을 보냅니다.

3.2. 읽기 작업 흐름 (Read Operation Flow)

DynamoDB의 읽기 작업은 기본 키와 인덱스를 활용하여 효율적으로 데이터를 검색합니다.

  1. 파티션 키 기반 조회 (GetItem): 단일 아이템을 읽을 때, 클라이언트는 해당 아이템의 파티션 키 값을 제공해야 합니다. DynamoDB는 이 값을 내부 해시 함수에 입력하여 아이템이 저장된 정확한 파티션을 찾아냅니다. 키-값 아키텍처 덕분에 DynamoDB는 검색할 위치를 정확히 알고 있어 전체 테이블을 스캔하는 것보다 훨씬 효율적인 쿼리를 가능하게 합니다.
  2. 정렬 키 기반 조회 (Query): 복합 기본 키를 가진 테이블에서 동일한 파티션 키를 공유하는 여러 아이템을 읽으려면 Query 작업을 사용합니다. DynamoDB는 먼저 파티션 키를 사용하여 올바른 파티션을 찾은 다음, 정렬 키를 사용하여 해당 파티션 내의 B-트리 구조를 탐색하여 특정 아이템 또는 아이템 범위를 효율적으로 검색합니다. 기본적으로 아이템은 정렬 키 오름차순으로 반환되지만, 내림차순도 요청할 수 있습니다. 또한, 정렬 키에 조건을 적용하여 특정 범위의 값 내에 있는 아이템만 검색할 수도 있습니다.
  3. 스캔 (Scan): Scan 작업은 테이블 또는 인덱스의 전체 데이터셋을 탐색하여 요청된 아이템을 찾습니다. 이는 특정 파티션 키를 기반으로 최적화된 Query 작업에 비해 효율성이 떨어지며, 특히 대규모 테이블의 경우 리소스 집약적이므로 가능한 한 Scan 작업은 피하고 Query 또는 GetItem과 같은 더 효율적인 작업을 사용하는 것이 권장됩니다.
  4. FilterExpression 적용: Query 또는 Scan 작업의 결과를 반환하기 전에 FilterExpression을 사용하여 추가적인 속성 제약을 적용하여 결과를 필터링할 수 있습니다. 하지만 이는 DynamoDB가 읽는 데이터의 양을 줄이지 않고 사용자에게 반환되는 데이터만 줄인다는 점을 유의해야 합니다.
  5. 읽기 일관성 모델 (Read Consistency Models): DynamoDB는 애플리케이션의 요구사항에 따라 두 가지 읽기 일관성 모델을 제공합니다.
    결과적 일관성 (Eventually Consistent Reads): 모든 읽기 작업의 기본 모델입니다. 최근 완료된 쓰기 작업의 결과가 즉시 반영되지 않을 수 있으며, 짧은 시간 후에 다시 요청하면 최신 데이터를 반환합니다. 강력한 일관성 읽기보다 비용이 절반입니다.– 강력한 일관성 (Strongly Consistent Reads): ConsistentRead 파라미터를 true로 설정하면 수행할 수 있습니다. 이 경우 DynamoDB는 모든 이전 성공적인 쓰기 작업이 반영된 최신 데이터를 반환합니다. 강력한 일관성 읽기는 테이블과 로컬 보조 인덱스(LSIs)에서만 지원됩니다.– 읽기 커밋 격리 (Read-Committed Isolation): DynamoDB는 읽기 커밋 격리를 제공하여, 읽기 작업이 항상 아이템에 대해 커밋된 값을 반환하도록 보장합니다.

요청 예시 (Query API):

{
    "TableName": "Users", // 필수: 작업 대상 테이블 이름
    "KeyConditionExpression": "UserId = :uid AND RegistrationDate BETWEEN :startDate AND :endDate",
 // 필수: 파티션 키 및 (선택적으로) 정렬 키 조건. ExpressionAttributeValues와 함께 사용.
    "ExpressionAttributeValues": { // 필수: KeyConditionExpression 및 FilterExpression에 사용되는 속성 값
        ":uid": {"S": "user123"},
        ":startDate": {"S": "2024-01-01T00:00:00Z"},
        ":endDate": {"S": "2024-12-31T23:59:59Z"},
        ":uname": {"S": "Alice"} // FilterExpression에서 사용될 값
    },
    "FilterExpression": "Username = :uname",, // 선택: 읽어온 아이템에 추가 필터링 적용 (RCU 절감 없음)
    "ConsistentRead": true, // 선택: 강력한 일관성 읽기 요청 여부 (기본값은 결과적 일관성)
    "ProjectionExpression": "UserId, Username, Email, RegistrationDate",, // 선택: 반환할 속성 지정 (콤마로 구분된 리스트)
    "Limit": 10, // 선택: 반환할 최대 아이템 개수
    "ScanIndexForward": false, // 선택: 정렬 키 순서 (true = 오름차순, false = 내림차순. 기본값은 true)
    "ReturnConsumedCapacity": "TOTAL", // 선택: 소비된 용량 단위 정보 반환 여부
    "IndexName": "UserDataIndex" // 선택: 글로벌 보조 인덱스(GSI) 또는 로컬 보조 인덱스(LSI) 사용 시 지정
    // 이 외에도 ExclusiveStartKey (선택), Select (선택) 등 다양한 선택 인자가 존재
}

Query 요청은 "Orders" 테이블에서 "CustomerId"가 "customer123"이고 "OrderDate"가 특정 범위 내에 있는 주문들을 조회합니다. 여기서 KeyConditionExpression은 파티션 키(CustomerId)와 정렬 키(OrderDate)를 사용하여 스토리지에서 읽어올 데이터의 양을 미리 줄입니다. 데이터가 스토리지에서 검색된 후, FilterExpression이 "OrderStatus"가 "COMPLETED"인 항목만을 최종 결과로 필터링하여 반환합니다. ConsistentRead: true는 최신 데이터를 읽도록 강력한 일관성을 요청합니다.

요청 예시 (Scan API):

{
    "TableName": "Users", // 필수: 작업 대상 테이블 이름
    "FilterExpression": "Username = :uname AND begins_with(Email, :emailPrefix)", // 선택: 읽어온 아이템에 필터링 적용 (RCU 절감 없음)
    "ExpressionAttributeValues": { // FilterExpression에 사용되는 속성 값
        ":uname": {"S": "Alice"},
        ":emailPrefix": {"S": "alice@"}
    },
    "ProjectionExpression": "UserId, Username, Email", // 선택: 반환할 속성 지정
    "Limit": 2, // 선택: 반환할 최대 아이템 개수 (페이지네이션 시 유용)
    "ReturnConsumedCapacity": "TOTAL", // 선택: 소비된 용량 단위 정보 반환 여부
    "ConsistentRead": false, // 선택: 강력한 일관성 읽기 요청 여부 (Scan은 기본적으로 결과적 일관성만 지원)
    "Segment": 0, // 선택: 병렬 스캔 시 스캔할 세그먼트 번호 (0부터 TotalSegments-1까지)
    "TotalSegments": 1 // 선택: 병렬 스캔 시 총 세그먼트 수
    // 이 외에도 ExclusiveStartKey (선택), Select (선택), ConditionalOperator (선택) 등 다양한 선택 인자가 존재
}

Scan 요청은 "Products" 테이블의 모든 아이템을 스캔합니다. FilterExpression을 사용하여 “”Username가 "Alice"이고 "emailPrefix"가 “alice@“로 시작하는 항목만 최종 결과로 필터링합니다. ProjectionExpression을 통해 반환할 속성을 제한하고, Limit을 통해 최대 50개의 아이템만 반환하도록 설정할 수 있습니다. Scan 작업은 테이블의 모든 파티션을 읽으므로 FilterExpression이 RCU를 절감하지 않습니다. SegmentTotalSegments는 대규모 테이블에서 병렬 스캔을 수행하여 스캔 속도를 높일 때 사용됩니다.

읽기 요청에 대한 응답

DynamoDB의 Query와 Scan은 한 번의 요청으로 반환할 수 있는 데이터의 최대 크기가 1MB로 제한됩니다. 특정 파티션 키에 해당하는 항목들의 총 크기가 1MB를 초과하면, DynamoDB는 1MB까지만 데이터를 채워서 반환하고 나머지 데이터의 시작 위치를 LastEvaluatedKey로 알려줍니다. 또한 Limit 파라미터를 사용해서 반환받을 항목의 최대 개수를 직접 지정한 경우에도 LastEvaluatedKey가 반환될 수 있습니다. 예를 들어, 특정 파티션에서 Scan된 100개의 항목이 있는데 Limit=20으로 설정하여 Query를 요청하면, DynamoDB는 20개의 항목과 함께 아직 조회할 항목이 남아있다는 의미로 LastEvaluatedKey를 반환합니다.

만약 LastEvaluatedKey를 어플리케이션 단에서 받았다면, 이를 ExclusiveStartKey의 값으로 넣어 그 뒤로부터의 데이터를 받아오도록 수행할 수 있습니다.

{
    "Items": [
        {
            "Category": {
                "S": "Electronics"
            },
            "Price": {
                "N": "1299.99"
            },
            "ProductID": {
                "S": "E-001"
            },
            "InStock": {
                "BOOL": true
            }
        },
        {
            "Category": {
                "S": "Books"
            },
            "Price": {
                "N": "24.50"
            },
            "ProductID": {
                "S": "B-001"
            },
            "InStock": {
                "BOOL": true
            }
        }
    ],
    "Count": 2,
    "ScannedCount": 2,
    "LastEvaluatedKey": {
        "ProductID": {
            "S": "B-001"
        }
    }
}

4. DynamoDB의 데이터 필터링: 메커니즘 및 실행

DynamoDB는 Query 및 Scan 작업에서 반환되는 데이터를 정제하는 메커니즘을 제공하지만, 이러한 필터가 언제 그리고 어디서 적용되는지 이해하는 것이 중요하며, 이는 성능과 비용에 직접적인 영향을 미칩니다.

4.1. KeyConditionExpression (Query 작업)

Query 작업은 특정 파티션 키 값을 가진 항목을 대상으로 하므로 매우 효율적입니다. KeyConditionExpression 내에서 정렬 키와 비교 연산자를 사용하여 결과를 추가로 좁힐 수 있습니다. KeyConditionExpression은 데이터가 기본 스토리지 파티션에서 읽히기 전에 적용됩니다. 이는 물리적 스토리지 계층에서 검색되는 데이터 양을 효과적으로 줄여서 소비되는 읽기 용량 단위(RCU)를 직접적으로 감소시킵니다.

4.2. FilterExpression (Query 및 Scan 작업)

FilterExpression은 Query 또는 Scan 작업이 스토리지 계층에서 데이터를 검색한 후에 적용되지만, 결과가 클라이언트에 반환되기 전에 적용됩니다. 중요한 점은 FilterExpression이 기본 스토리지에서 읽는 데이터 양이나 소비되는 RCU를 줄이지 않는다는 것입니다. RCU는 필터링 후 최종적으로 반환되는 항목과 관계없이 Query 또는 Scan 작업에 의해 평가된 모든 항목의 총 크기를 기준으로 소비됩니다.

Scan 호출 시 필터링 적용 시점 및 위치:

  1. 데이터 Fetch (스토리지 계층): 클라이언트가 Scan 요청을 보내면, DynamoDB는 해당 테이블 또는 인덱스의 모든 파티션에 걸쳐 모든 아이템을 읽어옵니다. 이 과정에서 읽기 용량 단위(RCU)가 소비됩니다. 즉, 필터링되지 않은 원본 데이터에 대해 RCU가 부과됩니다. Scan 작업은 한 번에 최대 1MB의 데이터를 읽어오며, 더 많은 데이터가 있는 경우 LastEvaluatedKey를 사용하여 후속 Scan 요청을 통해 데이터를 계속 읽어옵니다.
  2. 필터링 적용 (DynamoDB 서비스 내부): 각 파티션에서 읽어온 데이터는 DynamoDB 서비스의 내부 프로세싱 유닛으로 전송됩니다. 이 단계에서 클라이언트가 지정한 FilterExpression이 해당 데이터에 적용됩니다. 서비스는 FilterExpression의 조건에 일치하지 않는 항목들을 평가하고 즉시 폐기합니다. 이 필터링 작업은 데이터가 클라이언트에게 반환되기 직전에 이루어지며, 서비스 계층에서 추가적인 RCU를 소비하지 않습니다. 참고로 “DynamoDB 서비스의 내부”에 대한 구현 방식이나 위치 등은 조사하기 어려운 부분이였습니다.
  3. 최종 결과 반환 (클라이언트): 필터링을 거쳐 조건에 부합하는 항목들만이 네트워크를 통해 클라이언트에게 전송됩니다. 결과적으로 클라이언트는 필요한 데이터만 받게 되어 네트워크 전송량을 줄일 수 있지만, 이미 읽어온 데이터에 대한 RCU는 모두 지불됩니다.

이러한 메커니즘은 FilterExpression이 서버 측에서 실행되어 클라이언트가 불필요한 데이터를 받지 않도록 하지만, 데이터베이스가 실제 데이터를 읽어오는 비용(RCU)은 줄여주지 못한다는 한계가 있습니다.

결과적으로, 대규모의 데이터들이 산재된 테이블에서 Scan 작업은 FilterExpression에 크게 의존하여 결과를 좁히는 경우 매우 비효율적이고 비용이 많이 들 수 있습니다. 이는 필터를 적용하기 전에 모든 데이터를 읽기 때문입니다. FilterExpression의 성능 영향은 신중한 스키마 설계를 필요로 하며, 종종 None-Key 속성에 대한 효율적인 쿼리를 지원하기 위해 GSI를 사용하게 됩니다. GSI는 다른 기본 키를 가진 데이터의 새로운 "뷰"를 효과적으로 생성하며, 이는 비용이 많이 드는 Scan 작업을 피하기 위한 일반적인 아키텍처 패턴입니다.


5. DynamoDB의 세컨더리 인덱스

DynamoDB의 기본 키는 특정 접근 패턴에 최적화되어 있지만, 다양한 쿼리 요구사항을 충족하기 위해 **Secondary Index**를 활용할 수 있습니다. 애플리케이션이 테이블의 기본 키 외의 속성을 기준으로 데이터를 효율적으로 쿼리해야 할 때 보조 인덱스가 필요합니다.

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

  • 특징: LSI는 기본 테이블과 동일한 파티션 키를 가지지만, 다른 정렬 키를 지정할 수 있습니다. 이는 기본 테이블의 파티션 키를 기준으로 데이터를 조회하면서, 다른 속성을 기준으로 정렬하거나 범위 쿼리를 수행해야 할 때 유용합니다.
  • 내부 동작:
  1. 공동 배치 (Co-location): LSI는 기본 테이블의 파티션과 물리적으로 동일한 스토리지 파티션에 공동 배치됩니다. 즉, LSI의 데이터는 기본 테이블 데이터와 같은 파티션 내에 저장됩니다. LSI는 B Tree 구조로 동일한 파티션 내에서 유지됩니다.
  2. 데이터 복제 및 일관성: 기본 테이블에 쓰기 작업(PutItem, UpdateItem, DeleteItem)이 발생하면, DynamoDB는 해당 변경 사항을 기본 테이블 파티션과 LSI에 동기적으로 적용합니다. 이는 LSI에 대한 읽기 작업 시 항상 **강력한 일관성(Strongly Consistent Reads)**을 보장한다는 것을 의미합니다. 기본 테이블에 쓰기가 성공하면, 해당 LSI에도 즉시 반영되어 최신 데이터를 읽을 수 있습니다.
  3. 데이터 포함: LSI는 기본적으로 기본 테이블의 파티션 키와 정렬 키, 그리고 LSI 자체의 정렬 키를 포함합니다. 필요에 따라 기본 테이블의 다른 속성들을 "투영(project)"하여 인덱스에 포함시킬 수 있습니다. 이렇게 투영된 속성은 인덱스 자체에서 직접 읽을 수 있으므로, 기본 테이블로 돌아가서 fetch 작업을 피할 수 있어 읽기 성능을 최적화할 수 있습니다.
  4. 제한 사항: 테이블당 최대 5개까지 생성 가능하며, 테이블 생성 시점에 함께 정의되어야 합니다. LSI의 크기는 기본 테이블의 파티션 크기(10GB) 제한에 포함됩니다.

5.2. 글로벌 보조 인덱스 (Global Secondary Index, GSI)

  • 특징: GSI는 기본 테이블과 완전히 다른 파티션 키와 선택적 정렬 키를 가질 수 있는 인덱스입니다. 이는 기본 테이블의 기본 키로는 효율적으로 쿼리할 수 없는 속성을 기준으로 데이터를 조회해야 할 때 사용됩니다. 예를 들어, '사용자 ID'가 기본 키인 테이블에서 '이메일 주소'로 사용자를 찾고 싶을 때 GSI를 활용할 수 있습니다. 주의할 점으로는 테이블에 사용될 파티션 키를 “성별”, “상태” 등의 카디널리티가 작은 데이터로 삼을 경우, Hot Partition이 유발되어 비효율적으로 데이터가 관리될 수 있습니다.
  • 내부 동작:
  1. 독립적인 파티션: GSI는 기본 테이블의 "그림자 테이블"처럼 구현되며, 기본 테이블과 물리적으로 완전히 다른 스토리지 파티션에 저장됩니다. 이는 GSI가 자체적인 파티션 구조와 용량 단위를 가진 독립적인 테이블처럼 작동한다는 것을 의미합니다. 하나의 GSI가 몇개의 파티션으로 이루어질지는 사용자가 설정할 수 없고 aws에 의해서 알아서 관리됩니다.
  2. 데이터 복제 및 일관성: 기본 테이블에 쓰기 작업이 발생하면, DynamoDB는 해당 변경 사항을 백그라운드에서 GSI로 비동기적으로 전파합니다. 이는 GSI에 대한 읽기 작업 시 항상 **결과적 일관성(Eventually Consistent Reads)**을 가질 수 있다는 것을 의미합니다. 즉, 기본 테이블에 쓰기가 성공했더라도, GSI에 해당 변경 사항이 반영되기까지 약간의 지연 시간이 발생할 수 있습니다. 따라서 GSI를 통해 읽는 데이터는 가장 최신 데이터가 아닐 수 있습니다.
  3. 데이터 포함: LSI와 마찬가지로 GSI도 기본적으로 기본 테이블의 파티션 키와 정렬 키, 그리고 GSI 자체의 파티션 키와 정렬 키를 포함합니다. 또한, 필요에 따라 기본 테이블의 다른 속성들을 "투영(project)"하여 인덱스에 포함시킬 수 있습니다. 투영되는 속성이 많아질수록 GSI의 스토리지 및 용량 단위 비용이 증가할 수 있습니다.
  4. 제한 사항: 테이블당 최대 20개까지 생성 가능하며, 테이블 생성 후 언제든지 추가하거나 제거할 수 있습니다. GSI는 자체적인 읽기 및 쓰기 용량 단위를 프로비저닝하거나 온디맨드 모드를 설정해야 합니다.

6. 마무리

Amazon DynamoDB의 아키텍처는 고가용성, 확장성 및 성능을 위해 설계되어 최신 애플리케이션을 위한 강력한 선택이 될 수 있습니다. 파티셔닝, 리더-팔로워 복제, 온디맨드 또는 프로비저닝된 용량 모드를 활용함으로써 DynamoDB는 지연 시간이 짧은 액세스를 유지하면서 대규모 워크로드를 효율적으로 처리합니다.

이러한 아키텍처 구성 요소를 이해하면 개발자가 데이터 모델링, 용량 계획 및 성능 최적화에 대해 정보에 입각한 결정을 내리는 데 도움이 됩니다. 다음 포스팅에서는 다른 No-SQL에 대한 포스팅 또한 진행해보겠습니다.


Ref