이전 모듈에서는 게임 애플리케이션의 액세스 패턴을 정의했습니다. 이 모듈에서는 DynamoDB 테이블의 기본 키를 설계하고 핵심 액세스 패턴을 지원합니다.

모듈 완료 시간: 20분


DynamoDB 테이블의 기본 키를 설계할 때는 다음과 같은 모범 사례를 기억하십시오.

  • 테이블의 다양한 엔터티에서 시작. 서로 다른 유형의 여러 데이터를 단일 테이블에 저장하는 경우(예: 직원, 부서, 고객 및 주문) 기본 키에 각 엔터티를 고유하게 식별하고 개별 항목에 대해 핵심 작업을 수행할 수 있는 방법이 있는지 확인합니다.
  • 접두사를 사용하여 엔터티 유형 구분. 접두사를 사용하여 엔터티 유형을 구분하면 충돌을 방지하고 쿼리를 지원할 수 있습니다. 예를 들어 고객과 직원이 모두 동일한 테이블에 있는 경우 고객의 기본 키는 CUSTOMER#<CUSTOMERID>이고 직원의 기본 키는 EMPLOYEE#<EMPLOYEEID>일 수 있습니다.
  • 단일 항목 작업을 먼저 수행한 다음 가능한 경우 여러 항목 작업을 추가. 기본 키의 경우 단일 항목 API, 즉 GetItem, PutItem, UpdateItemDeleteItem을 사용하여 단일 항목에 대한 읽기 및 쓰기 작업을 충족하는 것이 중요합니다. 또한 Query를 사용하여 기본 키로 다중 항목 읽기 패턴을 충족할 수도 있습니다. 그렇지 않은 경우 Query 사용 사례를 처리할 보조 인덱스를 추가할 수 있습니다.

이러한 모범 사례를 염두에 두고, 게임 애플리케이션 테이블의 기본 키를 설계하고 몇 가지 기본적인 작업을 수행해보도록 하겠습니다.


  • 1단계. 기본 키 설계

    앞에서 소개한 대로 다양한 엔터티를 고려해봅시다. 게임에는 다음과 같은 엔터티가 있습니다.

    • User
    • Game
    • UserGameMapping

    UserGameMapping은 게임에 참여한 사용자를 나타내는 레코드입니다. UserGame 사이에는 다대다 관계가 존재합니다.

    다대다 매핑이 있다는 것은 일반적으로 쿼리 패턴 2개를 충족시켜야 한다는 것을 나타냅니다. 이 게임도 예외가 아닙니다. 게임에 참여한 모든 사용자를 찾아야 하는 액세스 패턴 1개와 사용자가 플레이한 모든 게임을 찾는 액세스 패턴 1개가 있습니다.

    데이터 모델에 서로 관계가 있는 여러 엔터티가 있는 경우 일반적으로 복합 기본 키HASHRANGE 값과 함께 사용합니다. 복합 기본 키가 있으면 Query 기능을 HASH 키에 사용하여 필요한 쿼리 패턴 중 하나를 충족할 수 있습니다. DynamoDB 설명서에서는 파티션 키를 HASH라고 하고 정렬 키를 RANGE라고 합니다. 이 안내서에서는 API 용어를, 특히 코드 또는 DynamoDB JSON 쓰기 프로토콜 형식을 설명할 때 바꿔서 사용합니다.

    다른 2개의 데이터 엔터티인 UserGame에는 RANGE 값에 대한 본래 속성이 없습니다. User 또는 Game에 대한 액세스 패턴은 키-값 조회이기 때문입니다. RANGE 값이 필요하므로 RANGE 키에 대한 필러 값을 제공하면 됩니다.

    이를 염두에 두고 각 엔터티 유형에 대해 HASHRANGE 값에 대한 다음과 같은 패턴을 사용해보겠습니다.

    엔터티 HASH RANGE
    User USER#<USERNAME> #METADATA#<USERNAME>
    Game GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    이전 테이블을 살펴보도록 하겠습니다.

    User 엔터티의 경우 HASH 값은 USER#<USERNAME>입니다. 접두사를 사용하여 엔터티를 식별하고 엔터티 유형 전체에서 가능한 충돌을 방지하고 있습니다.

    User 엔터티의 RANGE 값으로는 정적 접두사 #METADATA#를 사용하고 USERNAME 값을 그 뒤에 사용하고 있습니다. RANGE 값의 경우 USERNAME과 같이 알려진 값이 있어야 합니다. 알려진 값이 있으면 단일 항목 작업(예: GetItem, PutItemDeleteItem)이 가능해집니다.

    그러나 이 열을 인덱스의 HASH로 사용하는 경우 서로 다른 User 엔터티 전체에서 값이 다른 RANGE 값을 사용하여 균등한 파티셔닝을 수행할 수도 있어야 합니다. 이를 위해 USERNAME을 추가합니다.

    Game 엔터티의 기본 키 설계는 User 엔터티의 설계와 유사합니다. USERNAME과 다른 접두사(GAME#)와 GAME_ID를 사용하지만 원칙은 같습니다.

    마지막으로, UserGameMappingGame 엔터티와 동일한 HASH 키를 사용합니다. 따라서 Game에 대한 메타데이터는 물론 Game의 모든 사용자를 단일 쿼리로 가져올 수 있습니다. 그런 다음 UserGameMappingRANGE 키에 User 엔터티를 사용하여 특정 게임에 참여한 사용자를 식별할 수 있습니다.

    다음 단계에서는 이 기본 키 설계를 사용하여 테이블을 생성합니다. 

  • 2단계: 테이블 생성

    이제 기본 키가 설계되었으니 테이블을 생성해보겠습니다.

    모듈 1의 3단계에서 다운로드한 코드의 scripts/ 디렉터리에 create_table.py라는 Python 스크립트가 있습니다. 이 Python 스크립트의 콘텐츠는 다음과 같습니다.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 1,
                "WriteCapacityUnits": 1
            }
        )
        print("Table created successfully.")
    except Exception as e:
        print("Could not create table. Error:")
        print(e)
    

    이전 스크립트는 AWS SDK for Python인 Boto 3을 사용하여 CreateTable 작업을 사용합니다. 이 작업은 기본 키에 사용할 입력 속성인 2개의 속성 정의를 선언합니다. DynamoDB에는 스키마가 없지만 기본 키에 사용되는 속성의 이름과 유형을 선언해야 합니다. 이러한 속성은 테이블에 기록되는 모든 항목에 포함되어야 하므로 테이블을 생성할 때 속성을 지정해야 합니다.

    서로 다른 엔터티를 단일 테이블에 저장할 것이므로 UserId와 같은 기본 키 속성 이름을 사용할 수 없습니다. 속성은 저장되는 엔터티 유형에 따라 서로 다른 것을 의미합니다. 예를 들어 사용자의 기본 키는 USERNAME일 수 있고, 게임의 기본 키는 GAMEID일 수 있습니다. 따라서 속성에는 PK(파티션 키)와 SK(정렬 키) 같은 일반적인 이름을 사용합니다.

    키 스키마에서 속성을 구성한 후에는 테이블의 프로비저닝된 처리량을 지정합니다. DynamoDB는 프로비저닝된 용량 및 온디맨드 용량의 두 가지 용량 모드를 제공합니다. 프로비저닝된 용량 모드에서는 원하는 읽기 및 쓰기 처리량을 정확히 지정합니다. 사용 여부에 관계 없이 이 용량에 대한 요금이 부과됩니다.

    DynamoDB 온디맨드 용량 모드에서는 요청 단위로 요금이 부과됩니다. 요청당 요금은 프로비저닝된 처리량을 완전히 사용할 때보다 조금 높지만 용량 계획을 수행하거나 조절을 고려하는 데 시간을 쓰지 않아도 됩니다. 온디맨드 모드는 수요가 급증하는 워크로드 또는 예측할 수 없는 워크로드에 매우 적합합니다. 이 실습에서는 DynamoDB 프리 티어 범위에 포함되는 프로비저닝된 용량을 사용합니다.

    테이블을 생성하려면 다음 명령을 사용하여 Python 스크립트를 실행합니다.

    python scripts/create_table.py

    스크립트는 “Table created successfully”라는 메시지를 반환합니다.

    다음 단계에서는 몇 가지 예제 데이터를 테이블로 대량 업로드합니다. 

  • 3단계: 데이터를 테이블로 대량 업로드

    이 단계에서는 이전 단계에서 생성한 DynamoDB로 일부 데이터를 대량 업로드합니다. 따라서 후속 단계에서는 샘플 데이터를 사용할 수 있게 됩니다.

    scripts/ 디렉터리에 items.json 파일이 있습니다. 이 파일에는 이 실습을 위해 무작위로 생성된 835개의 예제 항목이 포함되어 있습니다. 이러한 항목에는 User, GameUserGameMapping 엔터티가 있습니다. 예제 항목 일부를 보려면 파일을 엽니다.

    scripts/ 디렉터리에는 bulk_load_table.py 파일도 있습니다. 이 파일은 items.json 파일의 항목을 읽고 이러한 항목을 대량으로 DynamoDB 테이블에 씁니다. 이 파일의 콘텐츠는 다음과 같습니다.

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('battle-royale')
    
    items = []
    
    with open('scripts/items.json', 'r') as f:
        for row in f:
            items.append(json.loads(row))
    
    with table.batch_writer() as batch:
        for item in items:
            batch.put_item(Item=item)
    

    이 스크립트에서는 Boto 3의 세부적인 클라이언트를 사용하는 대신 간략한 Resource 객체를 사용합니다. Resource 객체는 AWS API를 사용할 수 있는 간편한 인터페이스를 제공합니다. Resource 객체는 요청을 배치 처리하므로 이 상황에 유용합니다. BatchWriteItem 작업은 단일 요청에서 최대 25개 항목을 허용합니다. 그러나 Resource 객체를 통해 이 배치가 처리되므로 데이터를 25개 항목 이하의 요청으로 분할하지 않아도 됩니다.

    터미널에서 다음 명령을 실행하여 bulk_load_table.py 스크립트를 실행하고 데이터를 테이블에 로드합니다.

    python scripts/bulk_load_table.py

    Scan 작업을 실행하고 개수를 확인하여 모든 데이터가 테이블에 로드되었는지 확인할 수 있습니다.

    aws dynamodb scan \
     --table-name battle-royale \
     --select COUNT

    다음과 같은 결과가 표시됩니다.

    {
        "Count": 835, 
        "ScannedCount": 835, 
        "ConsumedCapacity": null
    }
    

    Count가 835개인 것을 볼 수 있습니다. 즉, 모든 항목이 성공적으로 로드되었습니다.

    다음 단계에서는 단일 요청에서 여러 엔터티 유형을 검색하는 방법을 알아봅니다. 이 방법을 사용하면 애플리케이션에서 수행하는 총 네트워크 요청 수를 줄여 애플리케이션 성능을 개선할 수 있습니다.

  • 4단계: 단일 요청에서 여러 엔터티 유형 검색

    이전 모듈에서 설명한 것과 같이 수신 요청 수에 맞춰 DynamoDB 테이블을 최적화해야 합니다. 또한 DynamoDB에는 관계형 데이터베이스에 있는 조인이 없다는 것도 설명했습니다. 대신 요청에서 조인과 유사한 동작을 허용하는 테이블을 설계합니다.

    이 단계에서는 단일 요청으로 여러 엔터티 유형을 검색합니다. 게임에서는 게임 세션에 대한 세부 정보를 가져와야 할 수 있습니다. 이러한 세부 정보에는 게임의 시작 시간, 종료 시간, 참여한 사용자 및 게임에서 플레이한 사용자에 대한 세부 정보 등 게임 자체에 대한 정보가 포함됩니다.

    이 요청은 Game 엔터티와 UserGameMapping 엔터티의 2가지 엔터티 유형에 걸쳐 수행됩니다. 그러나 요청을 여러 번 수행해야 하는 것은 아닙니다.

    다운로드한 코드의 application/ 디렉터리에는 fetch_game_and_players.py 스크립트가 있습니다. 이 스크립트에는 단일 요청에서 게임의 Game 엔터티와 UserGameMapping 엔터티를 모두 검색하는 코드를 구성하는 방법이 나와 있습니다.

    fetch_game_and_players.py 스크립트는 다음과 같은 코드로 구성됩니다.

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "3d4285f0-e52b-401a-a59b-112b38c4a26b"
    
    
    def fetch_game_and_users(game_id):
        resp = dynamodb.query(
            TableName='battle-royale',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :users",
            ExpressionAttributeValues={
                ":pk": { "S": "GAME#{}".format(game_id) },
                ":metadata": { "S": "#METADATA#{}".format(game_id) },
                ":users": { "S": "USER$" },
            },
            ScanIndexForward=True
        )
    
        game = Game(resp['Items'][0])
        game.users = [UserGameMapping(item) for item in resp['Items'][1:]]
    
        return game
    
    
    game = fetch_game_and_users(GAME_ID)
    
    print(game)
    for user in game.users:
        print(user)
    

    이 스크립트의 시작 부분에서는 Boto 3 라이브러리와 애플리케이션 코드의 객체를 나타내는 몇 가지 단순한 클래스를 가져옵니다. application/entities.py 파일에서 이러한 엔터티에 대한 정의를 확인할 수 있습니다.

    스크립트의 실제 작업은 모듈에 정의된 fetch_game_and_users 함수에서 수행됩니다. 이 함수는 이 데이터를 필요로 하는 모든 엔드포인트에서 사용할 애플리케이션에서 정의하는 함수와 유사합니다.

    fetch_game_and_users 함수는 몇 가지 작업을 수행합니다. 먼저, DynamoDB로 Query 요청을 제출합니다. 이 Query에는 GAME#<GameId>PK가 사용됩니다. 그런 다음 #METADATA#<GameId> USER$ 사이에 정렬 키가 있는 모든 엔터티를 요청합니다. 그러면 정렬 키가 #METADATA#<GameId>Game 엔터티와 키가 USER#로 시작되는 모든 UserGameMappings 엔터티가 반환됩니다. 문자열 유형의 정렬 키는 ASCII 문자 코드로 정렬됩니다. ASCII에서는 우물정자 기호(#) 바로 뒤에 달러 기호($)가 오므로 UserGameMapping 엔터티의 모든 매핑을 가져올 수 있습니다.

    응답을 수신하면 데이터 엔터티를 애플리케이션이 알고 있는 객체로 어셈블링합니다. Game 엔터티가 첫 번째로 반환될 것이므로 엔터티에서 Game 객체를 생성합니다. 나머지 엔터티에 대해서는 각 엔터티에 대한 UserGameMapping 객체를 생성한 다음 Game 객체에 사용자 어레이를 연결합니다.

    스크립트의 끝에는 함수 사용법이 나오고 결과 객체가 인쇄되어 있습니다. 터미널에서 다음 명령을 사용하여 스크립트를 실행할 수 있습니다.

    python application/fetch_game_and_players.py

    Game 객체와 모든 UserGameMapping 객체가 콘솔에 인쇄됩니다.

    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- branchmichael>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- deanmcclure>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emccoy>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emma83>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- iherrera>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- jeremyjohnson>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- lisabaker>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- maryharris>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- mayrebecca>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- meghanhernandez>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- nruiz>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- pboyd>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- richardbowman>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- roberthill>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- robertwood>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- victoriapatrick>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- waltervargas>
    

    이 스크립트는 테이블을 모델링하고 쿼리를 작성하여 단일 DynamoDB 요청에서 여러 엔터티 유형을 검색하는 방법을 보여줍니다. 관계형 데이터베이스에서는 단일 요청으로 서로 다른 테이블에서 여러 엔터티 유형을 검색할 때 조인을 사용합니다. DynamoDB에서는 데이터를 구체적으로 모델링하므로 함께 액세스하는 엔터티가 단일 테이블에 나란히 배치됩니다. 이 접근 방식을 사용하면 일반적인 관계형 데이터베이스의 조인이 필요하지 않으며 애플리케이션 확장 시 높은 성능을 유지할 수 있습니다.


    이 모듈에서는 기본 키를 설계하고 테이블을 생성했습니다. 그런 다음 데이터를 테이블로 대량 로드하고 단일 요청에서 여러 엔터티 유형을 쿼리하는 방법을 확인했습니다.

    현재 기본 키 설계에서는 다음과 같은 액세스 패턴을 충족할 수 있습니다.

    • 사용자 프로필 생성(쓰기)
    • 사용자 프로필 업데이트(쓰기)
    • 사용자 프로필 가져오기(읽기)
    • 게임 생성(쓰기)
    • 게임 보기(읽기)
    • 사용자의 게임 참여(쓰기)
    • 게임 시작(쓰기)
    • 사용자의 게임 업데이트(쓰기)
    • 게임 업데이트(쓰기)

    다음 모듈에서는 보조 인덱스를 추가하고 스파스 인덱스 기법에 대해 알아봅니다. 보조 인덱스를 사용하면 DynamoDB 테이블에 대한 추가 액세스 패턴을 지원할 수 있습니다.