DynamoDB: 구조, 인덱싱, 원자성, pagenation

ian·2023년 9월 23일
0

DynamoDB 구조

  • 테이블 (Table): 데이터베이스에서 구조화된 정보를 저장하는 표형태의 데이터 저장소입니다.
  • 아이템 (Item): 테이블 내에서 개별적인 데이터 단위로, 속성(어트리뷰트)의 모음입니다.
  • 어트리뷰트 (Attribute): 아이템의 속성 또는 필드를 나타내며, 특정 데이터 유형을 포함합니다.
- Table: UserTable
  - Item: 
    - Partition Key: UserId
      - Attribute: 
        - UserId: "123"
        - Username: "JohnDoe"
        - Address: 
          - Street: "123 Main St"
          - City: "Cityville"
          - ZipCode: "12345"
    - Partition Key: UserId
      - Attribute: 
        - UserId: "456"
        - Username: "JaneSmith"
        - Address: 
          - Street: "456 Oak St"
          - City: "Towndale"
          - ZipCode: "67890"
    - Partition Key: UserId
      - Attribute: 
        - UserId: "789"
        - Username: "BobJohnson"
        - Address: 
          - Street: "789 Pine St"
          - City: "Villagetown"
          - ZipCode: "54321"

Index & Policies

Primary Keys

NoSql 인 만큼 기본키 외에는 별도의 스키마가 존재하지 않습니다.
PK를 사용하여 데이터를 쿼리합니다.
테이블 생성시 고유 식별자를 가져야 하는데 이는 2종류로 나뉩니다.

파티션 키 (Partition Key):

DynamoDB 테이블에서 각 아이템을 식별하는 데 사용되는 주요 키입니다. 테이블을 여러 파티션으로 분할하고, 각 파티션은 고유한 파티션 키 값을 가지며 데이터를 저장합니다.

  • 예시: 테이블이 "UserId"를 파티션 키로 가질 경우, 각 사용자의 아이템은 해당 사용자의 고유한 "UserId" 값을 가집니다.

복합 키 (Composite Key):

파티션 키와 함께 정렬 키(또는 범위 키)를 조합하여 사용하는 키입니다. 복합 키를 사용하면 파티션 내에서 데이터를 정렬하여 저장할 수 있습니다.

예시: "UserId"를 파티션 키로 하고 "Timestamp"를 정렬 키로 하는 경우, 특정 사용자의 아이템을 시간순으로 정렬하여 저장할 수 있습니다.

파티션 키만 사용하는 경우:

테이블: "UserTable"
파티션 키: UserId
예시 아이템:
UserId: "123", Username: "JohnDoe", Age: 25

복합 키를 사용하는 경우:

테이블: "UserActivityTable"
파티션 키: UserId
정렬 키: Timestamp
예시 아이템:
UserId: "123", Timestamp: "2023-01-01T12:00:00", Action: "Login"
UserId: "123", Timestamp: "2023-01-02T09:30:00", Action: "Purchase"
UserId: "456", Timestamp: "2023-01-01T15:45:00", Action: "Logout"

보조 인덱스(Secondary Index)

보조 인덱스는 테이블의 기본 키 이외의 다른 어트리뷰트를 기반으로 데이터에 대한 추가적인 쿼리 및 정렬을 지원하는 인덱스입니다. DynamoDB는 기본 키 외에 보조 인덱스를 생성하여 여러 쿼리 패턴을 지원하고 성능을 최적화할 수 있게 합니다.

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

테이블의 기본 키와 같은 파티션 키를 공유하면서 정렬 키를 다르게 설정한 인덱스입니다.
파티션 키는 원래 테이블과 동일하며, 정렬 키만 다를 수 있습니다.

params = {
  TableName: 'Books',
  AttributeDefinitions: [
    { AttributeName: 'Author', AttributeType: 'S' },   // 테이블 기본 키의 파티션 키
    { AttributeName: 'Title', AttributeType: 'S' },    // 테이블 기본 키의 정렬 키
    { AttributeName: 'PublicationYear', AttributeType: 'N' }, // 로컬 보조 인덱스의 정렬 키
  ],
  KeySchema: [
    { AttributeName: 'Author', KeyType: 'HASH' },      // 테이블 기본 키의 파티션 키
    { AttributeName: 'Title', KeyType: 'RANGE' },      // 테이블 기본 키의 정렬 키
  ],
  LocalSecondaryIndexes: [
    {
      IndexName: 'PublicationYearIndex',
      KeySchema: [
        { AttributeName: 'Author', KeyType: 'HASH' },      // 로컬 보조 인덱스의 파티션 키 (테이블 기본 키와 동일해야 함)
        { AttributeName: 'PublicationYear', KeyType: 'RANGE' },  // 로컬 보조 인덱스의 정렬 키
      ],
      Projection: { ProjectionType: 'ALL' }  // 모든 어트리뷰트를 포함
    }
  ],
  ProvisionedThroughput: {
    ReadCapacityUnits: 5,
    WriteCapacityUnits: 5,
  },
};

Books 테이블에서 Author와 Title로 구성된 기본 키를 가지고 있습니다.
로컬 보조 인덱스 PublicationYearIndex를 생성하여 같은 Author 기준으로 PublicationYear를 정렬 키로 사용합니다.
이로써 같은 저자의 책들을 출판 연도를 기준으로 쿼리할 수 있게 됩니다.

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

테이블의 기본 키와 다른 파티션 키 및 정렬 키를 가지는 독립적인 인덱스입니다.
기본 키와 완전히 독립적이며, 다른 파티션 키 및 정렬 키를 사용하여 다양한 쿼리 패턴을 지원합니다.

params = {
  TableName: 'Orders',
  AttributeDefinitions: [
    { AttributeName: 'OrderID', AttributeType: 'S' },  // 테이블 기본 키의 파티션 키
    { AttributeName: 'OrderDate', AttributeType: 'S' },  // 글로벌 보조 인덱스의 정렬 키
  ],
  KeySchema: [
    { AttributeName: 'OrderID', KeyType: 'HASH' },       // 테이블 기본 키의 파티션 키
  ],
  GlobalSecondaryIndexes: [
    {
      IndexName: 'OrderByDateIndex',
      KeySchema: [
        { AttributeName: 'OrderDate', KeyType: 'HASH' },  // 글로벌 보조 인덱스의 파티션 키
      ],
      Projection: { ProjectionType: 'ALL' },   // 모든 어트리뷰트를 포함
      ProvisionedThroughput: {
        ReadCapacityUnits: 5,
        WriteCapacityUnits: 5,
      },
    }
  ],
  ProvisionedThroughput: {
    ReadCapacityUnits: 5,
    WriteCapacityUnits: 5,
  },
};

Orders 테이블에서 OrderID로 구성된 기본 키를 가지고 있습니다.
글로벌 보조 인덱스 OrderByDateIndex를 생성하여 OrderDate를 정렬 키로 사용합니다.
이로써 주문을 주문 날짜를 기준으로 쿼리할 수 있게 됩니다.

  • 쿼리 효율성 향상
  • 다양한 쿼리 패턴 지원
  • 데이터 모델의 유연성
  • 읽기/쓰기 처리량 분산

Dynamo table 생성 시 필요한 설정

AttributeDefinitions와 KeySchema는 DynamoDB 테이블을 생성할 때 필요한 설정입니다. 이 두 요소는 테이블의 구조를 정의하고 데이터를 저장하는 방식을 결정하는 데 중요한 역할을 합니다.

AttributeDefinitions (어트리뷰트 정의):

테이블에 저장될 각 아이템의 어트리뷰트(속성 또는 필드)를 정의합니다.
각 어트리뷰트의 데이터 유형을 지정하며, 이는 해당 어트리뷰트에 저장될 데이터의 형식을 나타냅니다.
예를 들어, 문자열인지 숫자인지를 지정할 수 있습니다.
필요한 경우 인덱스나 기타 기능들에 대한 설정도 여기에 추가될 수 있습니다.

KeySchema (키 스키마):

테이블의 주요 키(Primary Key)를 정의합니다.
주요 키는 테이블 내의 각 아이템을 고유하게 식별하는 데 사용됩니다.
주요 키는 하나 이상의 어트리뷰트로 구성되며, 이를 통해 DynamoDB는 데이터를 저장하고 쿼리할 때 어떻게 효율적으로 수행할지 결정합니다.
주로 해시 키(Hash Key)와 범위 키(Range Key)로 구성됩니다

tableDefinition = {
  TableName: 'UserTable',
  AttributeDefinitions: [
    { AttributeName: 'UserID', AttributeType: 'S' },
    { AttributeName: 'Timestamp', AttributeType: 'N' },
  ],
  KeySchema: [
    { AttributeName: 'UserID', KeyType: 'HASH' },  // UserID를 해시 키로 사용
    { AttributeName: 'Timestamp', KeyType: 'RANGE' },  // Timestamp를 범위 키로 사용
  ],
  ProvisionedThroughput: {
    ReadCapacityUnits: 5,
    WriteCapacityUnits: 5,
  },
};

developer guide

DynamoDB에서 데이터를 읽는 방법 (REST API)

CRUD

AWS SDK를 사용하며 api를 통해 crud를 수행합니다. tableDefinition을 기반

const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB();

const createItem = async () => {
  const params = {
    TableName: 'UserTable',
    Item: {
      'UserID': { S: 'user123' },
      'Timestamp': { N: '1641578400' },
      'Name': { S: 'John Doe' },
      'Email': { S: 'john.doe@example.com' },
    },
  };

  try {
    const result = await dynamodb.putItem(params).promise();
    console.log('Item created successfully:', result);
  } catch (error) {
    console.error('Error creating item:', error);
  }
};


const getItem = async () => {
  const params = {
    TableName: 'UserTable',
    Key: {
      'UserID': { S: 'user123' },
      'Timestamp': { N: '1641578400' },
    },
  };

  try {
    const result = await dynamodb.getItem(params).promise();
    console.log('Item retrieved successfully:', result.Item);
  } catch (error) {
    console.error('Error retrieving item:', error);
  }
};

const updateItem = async () => {
  const params = {
    TableName: 'UserTable',
    Key: {
      'UserID': { S: 'user123' },
      'Timestamp': { N: '1641578400' },
    },
    UpdateExpression: 'SET #name = :newName',
    ExpressionAttributeNames: { '#name': 'Name' },
    ExpressionAttributeValues: {
      ':newName': { S: 'Updated Name' },
    },
    ReturnValues: 'ALL_NEW',
  };

  try {
    const result = await dynamodb.updateItem(params).promise();
    console.log('Item updated successfully. New data:', result.Attributes);
  } catch (error) {
    console.error('Error updating item:', error);
  }
};

const deleteItem = async () => {
  const params = {
    TableName: 'UserTable',
    Key: {
      'UserID': { S: 'user123' },
      'Timestamp': { N: '1641578400' },
    },
  };

  try {
    const result = await dynamodb.deleteItem(params).promise();
    console.log('Item deleted successfully:', result);
  } catch (error) {
    console.error('Error deleting item:', error);
  }
};

업데이트 시 acid의 원칙 중 원자성을 보장하기 위한 방법

ExpressionAttributeNames & ExpressionAttributeValues

DynamoDB에 전달되는 업데이트 표현식에서 사용되는 플레이스홀더와 값에 해당합니다. 이러한 구성은 업데이트가 부분적으로 실행되지 않도록 보장합니다.

UpdateExpression

페이로드 내에서 여러 작업(SET, REMOVE, ADD, DELETE)에 대한 표현식을 생성합니다. 이때 각 작업은 원자적으로 수행되어야 함을 고려합니다.

Conditional Expressions 사용:

UpdateItem 작업을 수행하기 전에 조건 표현식(Conditional Expression)을 사용하여 특정 조건이 충족되었을 때만 업데이트를 허용하도록 설정할 수 있습니다. 이는 업데이트가 특정 조건을 만족하지 않으면 실패하도록 만듭니다.

const params = {
  TableName: 'UserTable',
  Key: {
    'UserID': { S: 'user123' },
    'Timestamp': { N: '1641578400' },
  },
  UpdateExpression: 'SET #name = :newName',
  ConditionExpression: '#name = :oldName',
  ExpressionAttributeNames: {
    '#name': 'Name',
  },
  ExpressionAttributeValues: {
    ':newName': { S: 'Updated Name' },
    ':oldName': { S: 'Current Name' },
  },
  ReturnValues: 'ALL_NEW',
};

TransactionExpression 사용:

DynamoDB는 트랜잭션을 지원하며, 여러 작업을 단일 트랜잭션으로 그룹화하여 원자성을 보장할 수 있습니다. 하나의 트랜잭션에서 여러 개의 UpdateItem 또는 다른 작업을 수행할 수 있습니다.

const AWS = require('aws-sdk');

const dynamodb = new AWS.DynamoDB();

const updateWithTransaction = async () => {
  const transactionParams = {
    TransactItems: [
      {
        Update: {
          TableName: 'UserTable',
          Key: {
            'UserID': { S: 'user123' },
            'Timestamp': { N: '1641578400' },
          },
          UpdateExpression: 'SET #name = :newName',
          ConditionExpression: '#name = :oldName',
          ExpressionAttributeNames: {
            '#name': 'Name',
          },
          ExpressionAttributeValues: {
            ':newName': { S: 'Updated Name' },
            ':oldName': { S: 'Current Name' },
          },
          ReturnValues: 'ALL_NEW',
        },
      },
      // Add more TransactItems if needed
    ],
  };

  try {
    const result = await dynamodb.transactWriteItems(transactionParams).promise();
    console.log('Transaction executed successfully:', result);
  } catch (error) {
    console.error('Error executing transaction:', error);
  }
};

Query / Scan

DynamoDB에서 데이터를 읽어오는 2가지 방식

Query

Query는 DynamoDB 테이블에서 특정 파티션 키 및 정렬 키 값을 기반으로 항목을 읽어오는 작업입니다.
기본 키에 해당하는 값을 제공하면 DynamoDB는 해당 파티션 키에 속하는 항목 중 일치하는 정렬 키 값을 찾아 반환합니다.
주로 정렬된 데이터에서 원하는 항목을 조회할 때 사용합니다.

params = {
  TableName: 'Books',
  KeyConditionExpression: 'Author = :author AND Title = :title',
  ExpressionAttributeValues: {
    ':author': 'John Doe',
    ':title': 'Introduction to DynamoDB',
  },
};

dynamodb.query(params, (err, data) => {
  if (err) console.error(err);
  else console.log(data.Items);
});

Scan

Scan은 DynamoDB 테이블의 전체 항목을 검색하는 작업입니다.
특정 조건을 지정하지 않으면 전체 테이블을 스캔하게 되므로 주의가 필요합니다.
주로 특정 쿼리 조건이 없거나, 테이블의 일부 데이터를 불규칙적으로 조회해야 할 때 사용합니다.

params = {
  TableName: 'Orders',
  FilterExpression: 'OrderStatus = :status',
  ExpressionAttributeValues: {
    ':status': 'Shipped',
  },
};

dynamodb.scan(params, (err, data) => {
  if (err) console.error(err);
  else console.log(data.Items);
});

Query는 기본 키 값으로 특정 항목을 신속하게 가져오는 데에 사용되며, 정렬된 테이블에서 가장 효과적입니다.
Scan은 전체 테이블을 스캔하므로 비용과 성능에 영향을 미칠 수 있습니다. 최대한 Query를 사용하여 정확한 데이터만을 검색하도록 노력하는 것이 좋습니다.

pagenation

DynamoDB에서의 페이지네이션은 Query 또는 Scan 작업에서 결과 집합이 큰 경우에 이를 처리하는 방법입니다. 큰 결과 집합을 모두 한 번에 가져오는 것은 비용과 성능 면에서 비효율적일 수 있으므로, 페이지네이션을 통해 일부 결과만 가져오고 다음 페이지로 넘어가는 것이 일반적입니다.

Limit 및 ExclusiveStartKey

Query 또는 Scan 작업을 수행할 때 Limit 매개변수를 사용하여 가져올 아이템의 최대 개수를 지정할 수 있습니다. 또한, ExclusiveStartKey를 사용하여 특정 키에서부터 시작하여 결과를 가져올 수 있습니다.

페이지 단위로 반복

결과 집합이 여전히 다음 페이지에 대한 키가 있을 경우, 이전 페이지에서 얻은 LastEvaluatedKey를 사용하여 새로운 Query 또는 Scan 작업을 수행합니다. 이를 통해 페이지를 계속해서 가져올 수 있습니다.

모든 페이지 처리

LastEvaluatedKey가 더 이상 반환되지 않을 때까지 페이지를 반복적으로 가져옵니다. 이 때, 각 페이지의 LastEvaluatedKey를 저장하고 이를 이용해 다음 페이지를 가져오는 방식으로 전체 결과 집합을 처리합니다.

const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();

async function paginateQuery() {
  const params = {
    TableName: 'YourTableName',
    KeyConditionExpression: 'YourKeyConditionExpression',
    Limit: 10, // Number of items to fetch per page
  };

  let lastEvaluatedKey = null;
  do {
    if (lastEvaluatedKey) {
      params.ExclusiveStartKey = lastEvaluatedKey;
    }

    const result = await dynamoDB.query(params).promise();
    const items = result.Items;

    // Process the items on the current page

    // Set lastEvaluatedKey for the next iteration
    lastEvaluatedKey = result.LastEvaluatedKey;
  } while (lastEvaluatedKey);
}

// Call the pagination function
paginateQuery();
profile
Backend Developer

0개의 댓글