DynamoDB 및 NoSQL을 처음 접하는 사용자가 수행하는 가장 큰 조정 작업 중 한 가지는, 전체 데이터 세트에서 필터링하도록 데이터를 모델링하는 방법입니다. 예를 들어, 게임에서 사용자가 참여할 수 있는 게임 세션을 사용자에게 표시할 수 있도록 열린 슬롯이 있는 게임 세션을 찾아야 합니다.
관계형 데이터베이스에서 데이터를 쿼리하는 몇 가지 SQL을 작성합니다.

SELECT * FROM games
	WHERE status = “OPEN”

DynamoDB는 쿼리 또는 스캔 작업에서 결과를 필터링하지만, DynamoDB는 관계형 데이터베이스와 같이 작동하지 않습니다. DynamoDB 필터는 쿼리 또는 스캔 작업과 일치하는 초기 항목을 검색한 후 적용됩니다. 필터는 DynamoDB 서비스에서 전송된 페이로드 크기를 줄이지만, 처음에 검색된 항목 수는 DynamoDB 크기 제한에 종속됩니다.

다행히 DynamoDB의 데이터 세트에서 필터링된 쿼리를 허용할 수 있는 여러 가지 방법이 있습니다. DynamoDB 테이블에서 효율적인 필터를 제공하려면 처음부터 테이블의 데이터 모델에 대한 필터를 계획해야 합니다. 이 실습의 두 번째 모듈에서 학습한 내용(액세스 패턴)을 고려하며 테이블을 설계합니다.

다음 단계에서는 열린 게임을 찾기 위해 글로벌 보조 인덱스를 사용합니다. 특별히, 희소 인덱스 기술을 사용하여 이 액세스 패턴을 처리합니다.

모듈 완료 시간: 40분


  • 1단계: 희소 보조 인덱스 모델링

    희소 인덱스는 DynamoDB에서 중요한 데이터 모델링 도구입니다. 이를 통해 대체 쿼리 패턴에 대해 허용되도록 데이터 형태를 다시 구성할 수 있습니다. 보조 인덱스를 생성하려면 이전에 테이블을 생성할 때와 같이 인덱스의 기본 키를 지정합니다. 글로벌 보조 인덱스의 기본 키는 각 항목에서 고유하지 않아도 됩니다. 그런 다음, DynamoDB는 지정된 속성에 따라 인덱스로 항목을 복사하며, 테이블에서와 같이 이를 쿼리할 수 있습니다.

    DynamoDB에서는 희소 보조 인덱스 사용이 고급 전략입니다. DynamoDB는 보조 인덱스를 통해 보조 인덱스에 기본 키의 요소가 있는 경우에만 원래 테이블에서 항목을 복사합니다. 기본 키 요소가 없는 항목은 복사되지 않으며, 그래서 이러한 보조 인덱스를 “희소” 인덱스라고 합니다.

    이 항목이 여기에서 어떻게 작동하는지 살펴보겠습니다. 기억하실 수도 있지만, 열린 게임을 찾는 2개의 액세스 패턴이 있었습니다.

    • 열린 게임 찾기(읽기)
    • 지도별 열린 게임 찾기(읽기)

    HASH 키가 게임의 map 속성이고 RANGE 키가 게임의 open_timestamp 속성(게임이 열린 시간 표시)인 복합 기본 키를 사용하여 보조 인덱스를 생성할 수 있습니다.

    게임이 가득차면 open_timestamp 속성이 삭제된다는 점이 중요합니다. 속성이 삭제되면 가득찬 게임은 보조 인덱스에서 제거됩니다. RANGE 키 속성에 대한 값이 없기 때문입니다. 그래서 희소 인덱스를 사용합니다. 이 인덱스는 open_timestamp 속성이 있는 열린 게임만 포함합니다.

    다음 단계에서는, 보조 인덱스를 생성합니다.

  • 2단계: 희소 보조 인덱스 생성

    이 단계에서는 열린 게임(아직 가득차지 않은 게임)에 대한 희소 보조 인덱스를 생성합니다.

    보조 인덱스 생성은 테이블 생성과 유사합니다. 다운로드한 코드에서 scripts/ 디렉터리에 이름이 add_secondary_index.py인 스크립트 파일이 있습니다. 해당 파일의 콘텐츠는 다음과 같습니다.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.update_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "map",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "open_timestamp",
                    "AttributeType": "S"
                }
            ],
            GlobalSecondaryIndexUpdates=[
                {
                    "Create": {
                        "IndexName": "OpenGamesIndex",
                        "KeySchema": [
                            {
                                "AttributeName": "map",
                                "KeyType": "HASH"
                            },
                            {
                                "AttributeName": "open_timestamp",
                                "KeyType": "RANGE"
                            }
                        ],
                        "Projection": {
                            "ProjectionType": "ALL"
                        },
                        "ProvisionedThroughput": {
                            "ReadCapacityUnits": 1,
                            "WriteCapacityUnits": 1
                        }
                    }
                }
            ],
        )
        print("Table updated successfully.")
    except Exception as e:
        print("Could not update table. Error:")
        print(e)

    보조 인덱스 또는 테이블의 기본 키에서 속성이 사용될 때마다 AttributeDefinitions에 정의해야 합니다. 그러면 GlobalSecondaryIndexUpdates 속성에서 새 보조 인덱스를 생성합니다. 이 보조 인덱스에 대해 인덱스 이름, 기본 키 스키마, 프로비저닝된 처리량 및 계획하려는 속성을 지정합니다.

    보조 인덱스를 희소 인덱스로 사용할 의도임을 지정할 필요는 없습니다. 그저 입력하는 데이터의 기능일 뿐입니다. 보조 인덱스에 대한 속성이 없는 테이블에 항목을 쓰면 보조 인덱스에 포함되지 않습니다.

    다음 명령을 실행하여 보조 인덱스를 생성합니다.

    python scripts/add_secondary_index.py

    콘솔에 다음 메시지가 나타납니다. “Table updated successfully.”

    다음 단계에서는 지도별로 열린 게임을 찾기 위해 희소 인덱스를 사용합니다.

  • 3단계: 희소 보조 인덱스 쿼리

    보조 인덱스를 구성했으므로 몇 가지 액세스 패턴을 충족하도록 이를 사용합니다.

    보조 인덱스를 사용하려면 QueryScan과 같은 두 개의 API 호출을 사용할 수 있습니다. Query를 사용하면 HASH 키를 지정해야 합니다. 그러면 대상이 지정된 결과를 반환합니다. Scan을 사용하면 HASH 키를 지정하지 않아도 되며, 전체 테이블에서 작업이 실행됩니다. Scan은 데이터베이스의 모든 항목에 액세스하므로 특정 상황을 제외하고는 DynamoDB에서 사용하지 않는 것이 좋습니다. 테이블에 데이터가 많은 경우 스캔 작업이 오래 걸릴 수 있습니다. 다음 단계에서는 희소 인덱스에서 Scan을 사용할 때 강력한 도구가 될 수 있는 이유를 보여줍니다.

    이전 단계에서 생성한 보조 인덱스에서 Query API를 사용하여 지도 이름별로 모든 열린 게임을 찾을 수 있습니다. 보조 인덱스는 지도 이름별로 파티션되므로, 이를 통해 열린 게임을 찾기 위한 대상이 지정된 쿼리를 작성할 수 있습니다.

    다운로드한 코드에서 find_open_games_by_map.py 파일은 application/ 디렉터리에 있습니다. 다음은 이 스크립트의 콘텐츠입니다.

    import boto3
    
    from entities import Game
    
    dynamodb = boto3.client('dynamodb')
    
    def find_open_games_by_map(map_name):
        resp = dynamodb.query(
            TableName='battle-royale',
            IndexName="OpenGamesIndex",
            KeyConditionExpression="#map = :map",
            ExpressionAttributeNames={
                "#map": "map"
            },
            ExpressionAttributeValues={
                ":map": { "S": map_name },
            },
            ScanIndexForward=True
        )
    
        games = [Game(item) for item in resp['Items']]
    
        return games
    
    games = find_open_games_by_map("Green Grasslands")
    for game in games:
        print(game)

    이전 스크립트에서 find_open_games_by_map 함수는 애플리케이션에서 사용하던 함수와 비슷합니다. 함수는 지도 이름을 승인하고 지도에 대한 모든 열린 게임을 찾기 위해 OpenGamesIndex에서 쿼리를 수행합니다. 그런 다음, 반환된 엔터티를 애플리케이션에서 사용할 수 있는 Game 객체로 어셈블링합니다.

    터미널에서 다음 명령을 실행하여 이 스크립트를 실행합니다.

    python application/find_open_games_by_map.py

    터미널에는 Green Grasslands 지도에 대한 4개의 열린 게임을 포함하는 다음 출력이 표시됩니다.

    Open games for Green Grasslands:
    Game<14c7f97e-8354-4ddf-985f-074970818215 -- Green Grasslands>
    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    Game<683680f0-02b0-4e5e-a36a-be4e00fc93f3 -- Green Grasslands>
    Game<0ab37cf1-fc60-4d93-b72b-89335f759581 -- Green Grasslands>
    sudo cp -r wordpress/* /var/www/html/

    다음 단계에서 Scan API를 사용하여 희소 보조 인덱스를 스캔합니다.

  • 4단계: 희소 보조 인덱스 스캔

    이전 단계에서는 특정 지도에 대한 게임을 찾는 방법을 알아보았습니다. 특정 지도에서 플레이하려는 플레이어도 있으므로, 이러한 때 이 방법이 유용합니다. 모든 지도에서 게임을 플레이하려는 플레이어도 있습니다. 이 섹션에서는 지도 유형에 상관없이 애플리케이션에서 모든 열린 게임을 찾는 방법을 살펴봅니다. 이를 위해 Scan API를 사용합니다.

    일반적으로 DynamoDB Scan 작업을 사용하여 테이블을 설계하지 않는 편이 좋습니다. DynamoDB는 필요한 정확한 엔터티를 사용하는 정밀한 쿼리를 위해 구축되었기 때문입니다. Scan 작업은 테이블에서 무작위 엔터티 컬렉션을 사용하므로, 필요한 엔터티를 찾으려면 데이터베이스를 여러 번 왕복해야 할 수 있습니다.

    하지만 Scan이 유용한 때가 있습니다. 이 상황에서는 희소 보조 인덱스를 사용합니다. 즉, 인덱스는 안에 많은 엔터티를 포함해서는 안 됩니다. 또한 인덱스는 열린 게임 및 정확히 필요한 게임만 포함합니다.

    이 사용 사례에서 Scan은 매우 유용합니다. 작동 방식을 살펴보겠습니다. 다운로드한 코드에서 find_open_games.py 파일은 application/ 디렉터리에 있습니다. 파일 콘텐츠는 다음과 같습니다.

    import boto3
    
    from entities import Game
    
    dynamodb = boto3.client('dynamodb')
    
    def find_open_games():
        resp = dynamodb.scan(
            TableName='battle-royale',
            IndexName="OpenGamesIndex",
        )
    
        games = [Game(item) for item in resp['Items']]
    
        return games
    
    games = find_open_games()
    print("Open games:")
    for game in games:
        print(game)

    이 코드는 이전 단계의 코드와 비슷합니다. 그러나 DynamoDB 클라이언트에서 query() 메서드를 사용하는 대신, scan() 메서드를 사용합니다. scan()을 사용하기 때문에 query()에서와 같이 키 조건에 대해 아무 것도 지정하지 않아도 됩니다. DynamoDB에서 정해진 순서 없이 많은 항목을 반환하기만 하면 됩니다.

    터미널에서 다음 명령으로 스크립트를 실행합니다.

    python application/find_open_games.py

    터미널에서는 여러 지도에서 9개의 열린 게임 목록을 출력해야 합니다.

    Open games:
    Game<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- Urban Underground>
    Game<d06af94a-2363-441d-a69b-49e3f85e748a -- Dirty Desert>
    Game<873aaf13-0847-4661-ba26-21e0c66ebe64 -- Dirty Desert>
    Game<fe89e561-8a93-4e08-84d8-efa88bef383d -- Dirty Desert>
    Game<248dd9ef-6b17-42f0-9567-2cbd3dd63174 -- Juicy Jungle>
    Game<14c7f97e-8354-4ddf-985f-074970818215 -- Green Grasslands>
    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    Game<683680f0-02b0-4e5e-a36a-be4e00fc93f3 -- Green Grasslands>
    Game<0ab37cf1-fc60-4d93-b72b-89335f759581 -- Green Grasslands>
    

    이 단계에서는 Scan 작업을 사용하는 것이 특정 상황에서 올바른 선택이 될 수 있는 경우를 보여줍니다. Scan을 사용하여 희소 보조 인덱스에서 엔터티 모음을 사용해 플레이어에게 열린 게임을 표시했습니다.

    다음 단계에서는 다음 2개의 액세스 패턴을 충족합니다.

    • 사용자의 게임 참여(쓰기)
    • 게임 시작(쓰기)

    다음 단계에서 "사용자의 게임 참여" 액세스 패턴을 충족하기 위해 DynamoDB 트랜잭션을 사용하려고 합니다. 트랜잭션은 한 번에 여러 데이터 요소에 영향을 주는 작업에 대해 관계형 시스템에서 널리 사용됩니다. 예를 들어, 은행을 운영한다고 가정합니다. 고객인 Alejandra는 다른 고객, Ana에게 100 USD를 이체합니다. 이 트랜잭션을 기록하는 경우 한 고객이 아니라, 두 고객 모두의 잔고에 변경 사항을 적용하도록 트랜잭션을 사용합니다.

    DynamoDB 트랜잭션을 통해 단일 작업의 일부로 여러 항목을 변경하는 애플리케이션을 더 쉽게 구축할 수 있습니다. 트랜잭션을 통해 단일 트랜잭션 요청의 일부로 최대 10개 항목에서 작업을 수행할 수 있습니다.

    TransactWriteItem API 호출에서 다음 작업을 사용할 수 있습니다.

    • Put: 항목을 삽입하거나 겹쳐씁니다.
    • Update: 기존 항목을 업데이트합니다.
    • Delete: 항목을 제거합니다.
    • ConditionCheck: 항목을 변경하지 않고 기존 항목에서 조건을 확인합니다.

     

    다음 단계에서는 게임 인원을 초과하지 않으면서 게임에 새 사용자를 추가하는 경우 DynamoDB 트랜잭션을 사용합니다.

  • 5단계: 게임에 사용자 추가

    이 모듈에서 다루는 첫 번째 액세스 패턴은 게임에 새 사용자를 추가하는 것입니다.

    게임에 새 사용자를 추가하는 경우 다음을 수행해야 합니다.

    • 게임에 아직 50명의 플레이어가 없는지 확인합니다(각 게임에서 허용되는 최대 플레이어 수는 50명).
    • 사용자가 아직 게임에 없는지 확인합니다.
    • UserGameMapping 엔터티를 생성하여 게임에 사용자를 추가합니다.
    • Game 엔터티의 people 속성을 늘려 게임에 있는 플레이어 수를 추적합니다.

    이 모든 작업을 수행하기 위해 기존 Game 엔터티 및 새 UserGameMapping 엔터티는 물론, 각 엔터티에 대한 조건부 논리에서 쓰기 작업이 필요합니다. 동일한 요청에서 여러 엔터티에 대한 작업을 수행해야 하므로 전체 요청의 성공이나 실패를 원하기 때문에 이는 DynamoDB 트랜잭션에 가장 적합한 작업 유형입니다.

    다운로드한 코드에서 join_game.py 스크립트는 application/ 디렉터리에 있습니다. 이 스크립트의 함수는 DynamoDB 트랜잭션을 사용하여 게임에 사용자를 추가합니다.

    다음은 스크립트의 콘텐츠입니다.

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646"
    USERNAME = 'vlopez'
    
    
    def join_game_for_user(game_id, username):
        try:
            resp = dynamodb.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": "battle-royale",
                            "Item": {
                                "PK": {"S": "GAME#{}".format(game_id) },
                                "SK": {"S": "USER#{}".format(username) },
                                "game_id": {"S": game_id },
                                "username": {"S": username }
                            },
                            "ConditionExpression": "attribute_not_exists(SK)",
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        },
                    },
                    {
                        "Update": {
                            "TableName": "battle-royale",
                            "Key": {
                                "PK": { "S": "GAME#{}".format(game_id) },
                                "SK": { "S": "#METADATA#{}".format(game_id) },
                            },
                            "UpdateExpression": "SET people = people + :p",
                            "ConditionExpression": "people <= :limit",
                            "ExpressionAttributeValues": {
                                ":p": { "N": "1" },
                                ":limit": { "N": "50" }
                            },
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        }
                    }
                ]
            )
            print("Added {} to game {}".format(username, game_id))
            return True
        except Exception as e:
            print("Could not add user to game")
    
    join_game_for_user(GAME_ID, USERNAME)

    스크립트의 join_game_for_user 함수에서 transact_write_items() 메서드는 쓰기 트랜잭션을 수행합니다. 이 트랜잭션에는 2개 작업이 있습니다.

    트랜잭션의 첫 번째 작업에서 Put 작업을 사용하여 새 UserGameMapping 엔터티를 삽입합니다. 해당 작업의 일부로 이 엔터티에 대해 SK 속성이 없어야 하는 조건을 지정합니다. 그러면 이 PKSK를 포함하는 엔터티는 아직 존재하지 않습니다. 이러한 엔터티가 존재하지 않으면 이 사용자는 이미 게임에 참여했음을 의미합니다.

    두 번째 작업은 Game 엔터티에서 Update 작업으로, people 속성을 하나씩 늘립니다. 이 작업의 일부로 people의 현재 값이 50을 초과하지 않도록 조건부 검사를 추가합니다. 50명의 사용자가 게임에 참여하는 즉시, 게임은 가득차고 시작 준비를 합니다.

    터미널에서 다음 명령으로 이 스크립트를 실행합니다.

    python application/join_game.py

    터미널에서 출력은 게임에 사용자가 추가되었음을 표시해야 합니다.

    Added vlopez to game c6f38a6a-d1c5-4bdf-8468-24692ccc4646

    다시 스크립트를 실행하려는 경우 함수에 실패합니다. vlopez 사용자가 게임에 이미 추가되었으므로, 다시 사용자를 추가하려는 시도가 여기에서 지정한 조건을 충족하지 않습니다.

    DynamoDB 트랜잭션을 추가하면 이와 같이 복잡한 작업에 관한 워크플로를 크게 단순화합니다. 트랜잭션이 없으면 복잡한 조건으로 API 호출을 여러 번 수행하고 충돌이 발생할 경우 수동으로 롤백해야 합니다. 이제 이러한 복잡한 작업을 50행 미만의 코드로 구현할 수 있습니다.

    다음 단계에서 “게임 시작(쓰기)” 액세스 패턴을 처리합니다.

  • 6단계: 게임 시작

    게임에 50명의 사용자가 존재하는 즉시, 게임 제작자는 게임 플레이를 시작하도록 게임을 시작할 수 있습니다. 이 단계에서는 이 액세스 패턴을 처리하는 방법을 보여줍니다.

    애플리케이션 백엔드 서비스가 게임을 시작하는 요청을 수신하면 다음 세 가지를 검사합니다.

    • 게임에 50명의 사용자가 참가했습니다.
    • 요청한 사용자가 게임 제작자입니다.
    • 게임이 아직 시작되지 않았습니다.

    요청의 조건식에서 이러한 검사를 각각 처리하여 게임을 업데이트할 수 있습니다. 이러한 검사에 모두 통과하면 다음 방식으로 엔터티를 업데이트해야 합니다.

    • 이전 모듈에서 희소 보조 인덱스에서 열린 게임으로 나타나지 않도록 open_timestamp 속성을 제거합니다.
    • start_time 속성을 추가하여 게임이 시작된 시점을 표시합니다.

    다운로드한 코드에서 start_game.py 스크립트는 application/ 디렉터리에 있습니다. 파일 콘텐츠는 다음과 같습니다.

    import datetime
    
    import boto3
    
    from entities import Game
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646"
    CREATOR = "gstanley"
    
    def start_game(game_id, requesting_user, start_time):
        try:
            resp = dynamodb.update_item(
                TableName='battle-royale',
                Key={
                    "PK": { "S": "GAME#{}".format(game_id) },
                    "SK": { "S": "#METADATA#{}".format(game_id) }
                },
                UpdateExpression="REMOVE open_timestamp SET start_time = :time",
                ConditionExpression="people = :limit AND creator = :requesting_user AND attribute_not_exists(start_time)",
                ExpressionAttributeValues={
                    ":time": { "S": start_time.isoformat() },
                    ":limit": { "N": "50" },
                    ":requesting_user": { "S": requesting_user }
                },
                ReturnValues="ALL_NEW"
            )
            return Game(resp['Attributes'])
        except Exception as e:
            print('Could not start game')
            return False
    
    game = start_game(GAME_ID, CREATOR, datetime.datetime(2019, 4, 16, 10, 15, 35))
    
    if game:
        print("Started game: {}".format(game))

    이 스크립트에서 start_game 함수는 애플리케이션에서 사용하는 함수와 비슷합니다. 여기에서는 game_id, requesting_userstart_time을 사용하고, 게임을 시작하기 위해 Game 엔터티를 업데이트하는 요청을 실행합니다.

    update_item() 호출의 ConditionExpression 파라미터는 이 단계에서 초반에 나열된 각 세 가지 검사를 지정합니다(게임에는 50명의 사용자가 존재하고, 게임 시작을 요청하는 사용자는 게임 제작자여야 하며, 게임은 이미 시작되었음을 나타내는 start_time 속성을 포함할 수 없음).

    UpdateExpression 파라미터를 통해 엔터티에서 변경하려는 사항을 확인할 수 있습니다. 먼저, 엔터티에서 open_timestamp 속성을 제거한 후, start_time 속성을 게임 시작 시간으로 설정합니다.

    터미널에서 다음 명령으로 이 스크립트를 실행합니다.

    python application/start_game.py

    터미널에 게임이 시작되었음을 나타내는 출력이 표시됩니다.

    Started game: Game<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- Urban Underground>

    터미널에서 두 번째로 스크립트를 실행하려고 합니다. 지금은 게임을 시작할 수 없음을 나타내는 오류 메시지가 표시됩니다. 게임이 이미 시작했기 때문에 start_time 속성이 존재합니다. 결과적으로 요청은 엔터티에 대한 조건부 검사를 통과하지 못합니다.

    Game 엔터티 및 연관된 User 엔터티 사이의 다대다 관계가 존재하고 UserGameMapping 엔터티로 관계를 표시한다는 점을 기억하실 것입니다.

    관계의 양측을 쿼리하고 싶은 경우가 종종 있습니다. 기본 키 설정을 통해 Game에서 모든 User 엔터티를 찾을 수 있습니다. 반전된 인덱스를 사용하여 User에 대한 모든 Game 엔터티를 쿼리할 수도 있습니다.

    DynamoDB에서 반전된 인덱스는 기본 키의 역에 해당하는 보조 인덱스입니다. RANGE 키는 HASH 키가 되고, 그 반대도 마찬가지입니다. 이 패턴은 테이블을 반전시키고, 이를 통해 다대다 관계의 다른 방향에서 쿼리할 수 있습니다.

    다음 단계에서는 반전된 인덱스를 테이블에 추가하고 특정 User에 대한 모든 Game 엔터티를 검색하도록 이를 사용하는 방법을 보여줍니다. 

  • 7단계: 반전된 인덱스 추가

    이 단계에서는 테이블에 반전된 인덱스를 추가합니다. 반전된 인덱스는 다른 보조 인덱스와 같이 생성됩니다.

    다운로드한 코드에서 add_inverted_index.py 스크립트는 scripts/ 디렉터리에 있습니다. 이 Python 스크립트는 테이블에 반전된 인덱스를 추가합니다.

    해당 파일 콘텐츠는 다음과 같습니다.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.update_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            GlobalSecondaryIndexUpdates=[
                {
                    "Create": {
                        "IndexName": "InvertedIndex",
                        "KeySchema": [
                            {
                                "AttributeName": "SK",
                                "KeyType": "HASH"
                            },
                            {
                                "AttributeName": "PK",
                                "KeyType": "RANGE"
                            }
                        ],
                        "Projection": {
                            "ProjectionType": "ALL"
                        },
                        "ProvisionedThroughput": {
                            "ReadCapacityUnits": 1,
                            "WriteCapacityUnits": 1
                        }
                    }
                }
            ],
        )
        print("Table updated successfully.")
    except Exception as e:
        print("Could not update table. Error:")
        print(e)
    

    이 스크립트에서는 DynamoDB 클라이언트에서 update_table() 메서드를 호출합니다. 메서드에서는 인덱스의 키 스키마, 프로비저닝된 처리량 및 인덱스에 계획하는 속성을 포함하여 생성하려는 보조 인덱스에 대한 세부 정보를 전달합니다.

    터미널에서 다음 명령을 입력하여 스크립트를 실행합니다.

    python scripts/add_inverted_index.py

    터미널에서는 인덱스가 생성되었다는 출력을 표시합니다.

    Table updated successfully.

    다음 단계에서는 반전된 인덱스를 사용하여 특정 User에 대한 모든 Game 엔터티를 검색합니다.

  • 8단계: 사용자의 게임 검색

    반전된 인덱스를 생성했으므로, 이를 사용하여 User가 플레이한 Game 엔터티를 검색할 수 있습니다. 이를 처리하려면 Game 엔터티를 보려는 User의 반전된 인덱스를 쿼리해야 합니다.

    다운로드한 코드에서 find_games_for_user.py 스크립트는 application/ 디렉터리에 있습니다. 파일 콘텐츠는 다음과 같습니다.

    import boto3
    
    from entities import UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    USERNAME = "carrpatrick"
    
    
    def find_games_for_user(username):
        try:
            resp = dynamodb.query(
                TableName='battle-royale',
                IndexName='InvertedIndex',
                KeyConditionExpression="SK = :sk",
                ExpressionAttributeValues={
                    ":sk": { "S": "USER#{}".format(username) }
                },
                ScanIndexForward=True
            )
        except Exception as e:
            print('Index is still backfilling. Please try again in a moment.')
            return None
    
        return [UserGameMapping(item) for item in resp['Items']]
    
    games = find_games_for_user(USERNAME)
    
    if games:
        print("Games played by {}:".format(USERNAME))
        for game in games:
            print(game)
    

    이 스크립트에서는 게임에서 사용하는 함수와 비슷한 find_games_for_user() 함수를 사용합니다. 이 함수는 사용자 이름을 사용하고 지정된 사용자가 플레이한 모든 게임을 반환합니다.

    터미널에서 다음 명령으로 스크립트를 실행합니다.

    python application/find_games_for_user.py

    스크립트는 carrpatrick 사용자가 플레이한 모든 게임을 출력합니다.

    Games played by carrpatrick:
    UserGameMapping<25cec5bf-e498-483e-9a00-a5f93b9ea7c7 -- carrpatrick -- SILVER>
    UserGameMapping<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- carrpatrick>
    UserGameMapping<c9c3917e-30f3-4ba4-82c4-2e9a0e4d1cfd -- carrpatrick>
    

    이 모듈에서는 테이블에 보조 인덱스를 추가했습니다. 이를 통해 다음 2개의 액세스 패턴을 충족시켰습니다.

    • 지도별 열린 게임 찾기(읽기)
    • 열린 게임 찾기(읽기)

    이를 위해 추가 플레이어에게 여전히 열려 있는 게임만 포함하는 희소 인덱스를 사용했습니다. 그런 다음, 인덱스에서 QueryScan API 모두를 사용하여 열린 게임을 찾았습니다.

    또한 애플리케이션에서 두 개의 고급 쓰기 작업을 충족시키는 방법을 알아보았습니다. 먼저, 사용자가 게임에 참여하면 DynamoDB 트랜잭션을 사용했습니다. 트랜잭션을 통해 단일 요청으로 여러 엔터티에서 복잡한 조건부 쓰기를 처리했습니다.

    두 번째로, 준비가 되면 게임 제작자가 게임을 시작하는 함수를 구현했습니다. 이 액세스 패턴에서는 3개의 속성 값을 검사하고 2개의 속성을 업데이트해야 하는 업데이트 작업을 포함합니다. 조건식 및 업데이트 식을 통해 단일 요청으로 이 복잡한 논리를 표현할 수 있습니다.

    세 번째로, User가 플레이한 모든 Game 엔터티를 검색하여 최종 액세스 패턴을 충족했습니다. 이 액세스 패턴을 처리하기 위해 반전된 인덱스 패턴을 사용하는 보조 인덱스를 생성하여 User 엔터티 및 Game 엔터티 간에 다대다 관계의 반대편에서 쿼리할 수 있습니다.

    다음 모듈에서는 생성한 리소스를 정리합니다.