

# DynamoDB에서 대량 데이터 작업을 사용하는 모범 사례
<a name="BestPractices_BulkDataOperations"></a>

DynamoDB는 최대 25개의 `PutItem` 및 `DeleteItem` 요청을 함께 수행할 수 있는 `BatchWriteItem`과 같은 배치 작업을 지원합니다. 그러나 `BatchWriteItem`은 `UpdateItem` 작업을 지원하지 않습니다. 대량 업데이트의 경우 업데이트 요구 사항과 특성 면에서 차이가 있습니다. `TransactWriteItems`를 비롯한 다른 DynamoDB API를 사용하여 배치 크기를 최대 100까지 늘릴 수 있습니다. 더 많은 항목이 관련된 경우 AWS Glue, Amazon EMR, AWS Step Functions와 같은 서비스를 사용하거나 DynamoDB-shell과 같은 사용자 지정 스크립트 및 도구를 사용하여 대량 업데이트를 수행할 수 있습니다.

**Topics**
+ [조건부 배치 업데이트](BestPractices_ConditionalBatchUpdate.md)
+ [효율적인 대량 작업](BestPractices_EfficientBulkOperations.md)

# 조건부 배치 업데이트
<a name="BestPractices_ConditionalBatchUpdate"></a>

DynamoDB는 최대 25개의 `PutItem` 및 `DeleteItem` 요청을 단일 배치로 수행할 수 있는 `BatchWriteItem`과 같은 배치 작업을 지원합니다. 그러나 `BatchWriteItem`은 `UpdateItem` 작업을 지원하지 않으며 조건식을 지원하지 않습니다. 해결 방법으로 `TransactWriteItems`를 비롯한 다른 DynamoDB API를 사용하여 배치 크기를 최대 100까지 늘릴 수 있습니다.

더 많은 항목이 관련되고 주요 데이터 청크를 변경해야 하는 경우 AWS Glue, Amazon EMR, AWS Step Functions와 같은 서비스를 사용하거나 DynamoDB-shell과 같은 사용자 지정 스크립트 및 도구를 사용하여 효율적인 대량 업데이트를 수행할 수 있습니다.

**이 패턴을 사용해야 하는 경우**
+ DynamoDB-shell은 프로덕션 사용 사례에서 지원되지 않습니다.
+ `TransactWriteItems` - 조건 유무에 관계없이 최대 100개의 개별 업데이트로, 전부 아니면 전무 방식의 ACID 번들로 실행됩니다. 애플리케이션에 멱등성이 필요한 경우 `TransactWriteItems` 직접 호출을 `ClientRequestToken`과 함께 제공하여 동일한 여러 호출이 하나의 호출과 같은 효과를 갖도록 할 수 있습니다. 이렇게 하면 동일한 트랜잭션을 여러 번 실행하지 않고 잘못된 데이터 상태로 끝나지 않습니다.

  단점 - 추가 처리량이 사용됩니다. 표준 1KB 쓰기당 1WGU 대신 1KB 쓰기당 2WCU.
+ PartiQL `BatchExecuteStatement` - 조건 유무에 관계없이 최대 25개의 업데이트입니다. `BatchExecuteStatement`는 항상 전체 요청에 대한 성공 응답을 반환하고 순서를 유지하는 개별 작업 응답 목록도 반환합니다.

  단점 - 더 큰 배치의 경우 요청을 25개 배치로 배포하려면 추가 클라이언트측 로직이 필요합니다. 재시도 전략을 결정하려면 개별 오류 응답을 고려해야 합니다.

## 코드 예제
<a name="bp-conditional-code-examples"></a>

이러한 코드 예제에서는 AWS SDK for Python인 boto3 라이브러리를 사용합니다. 이러한 예제에서는 boto3를 설치하고 적절한 AWS 자격 증명으로 구성했다고 가정합니다.

유럽 도시 곳곳에 여러 창고를 둔 전자 제품 공급업체의 재고 데이터베이스를 생각해 보겠습니다. 여름이 끝났기 때문에 공급업체는 탁상용 선풍기를 정리하여 다른 재고를 위한 공간을 확보하려고 합니다. 이 공급업체는 이탈리아의 창고에서 공급되는 모든 탁상용 선풍기에 대해 가격 할인을 제공하고자 하지만 탁상용 선풍기 예비 재고가 20개 있는 경우에 한해 이렇게 하려고 합니다. **inventory**라는 이름의 DynamoDB 테이블에 각 제품의 고유 식별자인 파티션 키 **sku**와 웨어하우스의 식별자인 정렬 키 **warehouse**의 키 스키마가 있습니다.

다음 Python 코드는 `BatchExecuteStatement` API 직접 호출을 사용하여 이 조건부 배치 업데이트를 수행하는 방법을 보여줍니다.

```
import boto3

client=boto3.client("dynamodb")

before_image=client.query(TableName='inventory', KeyConditionExpression='sku=:pk_val AND begins_with(warehouse, :sk_val)', ExpressionAttributeValues={':pk_val':{'S':'F123'},':sk_val':{'S':'WIT'}}, ProjectionExpression='sku,warehouse,quantity,price')
print("Before update: ", before_image['Items'])

response=client.batch_execute_statement(
        Statements=[
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITTUR1'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITROM1'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITROM2'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITROM5'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITVEN1'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITVEN2'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
            {'Statement': 'UPDATE inventory SET price=price-5 WHERE sku=? AND warehouse=? AND quantity > 20', 'Parameters': [{'S':'F123'}, {'S':'WITVEN3'}], 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'},
        ],
        ReturnConsumedCapacity='TOTAL'
    )

after_image=client.query(TableName='inventory', KeyConditionExpression='sku=:pk_val AND begins_with(warehouse, :sk_val)', ExpressionAttributeValues={':pk_val':{'S':'F123'},':sk_val':{'S':'WIT'}}, ProjectionExpression='sku,warehouse,quantity,price')
print("After update: ", after_image['Items'])
```

실행 시 샘플 데이터에 대해 아래와 같은 출력이 생성됩니다.

```
Before update:  [{'quantity': {'N': '20'}, 'warehouse': {'S': 'WITROM1'}, 'price': {'N': '40'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '25'}, 'warehouse': {'S': 'WITROM2'}, 'price': {'N': '40'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '28'}, 'warehouse': {'S': 'WITROM5'}, 'price': {'N': '38'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '26'}, 'warehouse': {'S': 'WITTUR1'}, 'price': {'N': '40'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '10'}, 'warehouse': {'S': 'WITVEN1'}, 'price': {'N': '38'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '20'}, 'warehouse': {'S': 'WITVEN2'}, 'price': {'N': '38'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '50'}, 'warehouse': {'S': 'WITVEN3'}, 'price': {'N': '35'}, 'sku': {'S': 'F123'}}]
After update:  [{'quantity': {'N': '20'}, 'warehouse': {'S': 'WITROM1'}, 'price': {'N': '40'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '25'}, 'warehouse': {'S': 'WITROM2'}, 'price': {'N': '35'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '28'}, 'warehouse': {'S': 'WITROM5'}, 'price': {'N': '33'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '26'}, 'warehouse': {'S': 'WITTUR1'}, 'price': {'N': '35'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '10'}, 'warehouse': {'S': 'WITVEN1'}, 'price': {'N': '38'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '20'}, 'warehouse': {'S': 'WITVEN2'}, 'price': {'N': '38'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '50'}, 'warehouse': {'S': 'WITVEN3'}, 'price': {'N': '30'}, 'sku': {'S': 'F123'}}]
```

이는 내부 시스템에 대한 제한된 작업이므로 멱등성 요구 사항은 고려되지 않았습니다. 가격이 35보다 크고 40보다 작은 경우에만 가격 업데이트가 진행되어야 하는 것과 같은 추가 가드레일을 배치하여 업데이트를 더 견고하게 만들 수 있습니다.

또는 더 엄격한 멱등성 및 ACID 요구 사항이 있는 경우 `TransactWriteItems`를 사용하여 동일한 배치 업데이트 작업을 수행할 수 있습니다. 그러나 트랜잭션 번들의 모든 작업이 진행되거나 전체 번들이 실패한다는 점에 유의해야 합니다.

이탈리아에 폭염이 발생하여 탁상용 선풍기 수요가 급격히 증가했다고 해보겠습니다. 공급업체는 이탈리아의 모든 창고에서 출고되는 탁상용 선풍기의 가격을 20유로 인상하기를 원하지만 규제 기관은 전체 재고에서 현재 가격이 70유로 미만인 경우에 한해 이러한 가격 인상을 허용하고 있습니다. 각 창고에서 가격이 70유로 미만인 경우에만 재고 전체에 걸쳐 한 번만 가격을 업데이트해야 합니다.

다음 Python 코드는 `TransactWriteItems` API 직접 호출을 사용하여 이 배치 업데이트를 수행하는 방법을 보여줍니다.

```
import boto3

client=boto3.client("dynamodb")

before_image=client.query(TableName='inventory', KeyConditionExpression='sku=:pk_val AND begins_with(warehouse, :sk_val)', ExpressionAttributeValues={':pk_val':{'S':'F123'},':sk_val':{'S':'WIT'}}, ProjectionExpression='sku,warehouse,quantity,price')
print("Before update: ", before_image['Items'])

response=client.transact_write_items(
        ClientRequestToken='UUIDAWS124',
        TransactItems=[
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITTUR1'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITROM1'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITROM2'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITROM5'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITVEN1'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITVEN2'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
            {'Update': { 'Key': {'sku': {'S':'F123'}, 'warehouse': {'S':'WITVEN3'}}, 'UpdateExpression': 'SET price = price + :inc', 'ConditionExpression': 'price < :cap', 'ExpressionAttributeValues': { ':inc': {'N': '20'}, ':cap': {'N': '70'}}, 'TableName': 'inventory', 'ReturnValuesOnConditionCheckFailure': 'ALL_OLD'}},
        ],
        ReturnConsumedCapacity='TOTAL'
    )

after_image=client.query(TableName='inventory', KeyConditionExpression='sku=:pk_val AND begins_with(warehouse, :sk_val)', ExpressionAttributeValues={':pk_val':{'S':'F123'},':sk_val':{'S':'WIT'}}, ProjectionExpression='sku,warehouse,quantity,price')
print("After update: ", after_image['Items'])
```

실행 시 샘플 데이터에 대해 아래와 같은 출력이 생성됩니다.

```
Before update:  [{'quantity': {'N': '20'}, 'warehouse': {'S': 'WITROM1'}, 'price': {'N': '60'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '25'}, 'warehouse': {'S': 'WITROM2'}, 'price': {'N': '55'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '28'}, 'warehouse': {'S': 'WITROM5'}, 'price': {'N': '53'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '26'}, 'warehouse': {'S': 'WITTUR1'}, 'price': {'N': '55'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '10'}, 'warehouse': {'S': 'WITVEN1'}, 'price': {'N': '58'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '20'}, 'warehouse': {'S': 'WITVEN2'}, 'price': {'N': '58'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '50'}, 'warehouse': {'S': 'WITVEN3'}, 'price': {'N': '50'}, 'sku': {'S': 'F123'}}]
After update:  [{'quantity': {'N': '20'}, 'warehouse': {'S': 'WITROM1'}, 'price': {'N': '80'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '25'}, 'warehouse': {'S': 'WITROM2'}, 'price': {'N': '75'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '28'}, 'warehouse': {'S': 'WITROM5'}, 'price': {'N': '73'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '26'}, 'warehouse': {'S': 'WITTUR1'}, 'price': {'N': '75'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '10'}, 'warehouse': {'S': 'WITVEN1'}, 'price': {'N': '78'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '20'}, 'warehouse': {'S': 'WITVEN2'}, 'price': {'N': '78'}, 'sku': {'S': 'F123'}}, {'quantity': {'N': '50'}, 'warehouse': {'S': 'WITVEN3'}, 'price': {'N': '70'}, 'sku': {'S': 'F123'}}]
```

DynamoDB에서 배치 업데이트를 수행하는 방법에는 여러 가지가 있습니다. 적절한 접근 방식은 ACID 및/또는 멱등성 요구 사항, 업데이트할 항목 수, API에 대한 친숙도와 같은 요인에 따라 달라집니다.

# 효율적인 대량 작업
<a name="BestPractices_EfficientBulkOperations"></a>

**이 패턴을 사용해야 하는 경우**

이러한 패턴은 DynamoDB 항목에 대한 대량 업데이트를 효율적으로 수행하는 데 유용합니다.
+ DynamoDB-shell은 프로덕션 사용 사례에서 지원되지 않습니다.
+ `TransactWriteItems` - 조건 유무에 관계없이 최대 100개의 개별 업데이트로, 전부 아니면 전무 방식의 ACID 번들로 실행됩니다.

  단점 - 1KB 쓰기당 2WCU로, 추가 처리량이 사용됩니다.
+ PartiQL `BatchExecuteStatement` - 조건 유무에 관계없이 최대 25개의 업데이트입니다.

  단점 - 요청을 25개 배치로 배포하려면 추가 로직이 필요합니다.
+ AWS Step Functions - AWS Lambda에 익숙한 개발자를 위한, 속도가 제한된 대량 작업입니다.

  단점 - 실행 시간이 속도 제한에 반비례합니다. 최대 Lambda 함수 제한 시간으로 제한됩니다. 이 기능의 경우 읽기와 쓰기 사이에 발생하는 데이터 변경 사항을 덮어쓸 수 있습니다. 자세한 내용은 [Amazon EMR을 사용하여 Amazon DynamoDB Time to Live 속성 채우기: 2부](https://aws.amazon.com/blogs/database/part-2-backfilling-an-amazon-dynamodb-time-to-live-attribute-using-amazon-emr/)를 참조하세요.
+ AWS Glue 및 Amazon EMR - 관리형 병렬 처리를 사용하는, 속도가 제한된 대량 작업입니다. 시간에 민감하지 않은 애플리케이션 또는 업데이트의 경우 이러한 옵션을 백그라운드에서 실행하여 소량의 처리량만 사용할 수 있습니다. 두 서비스 모두 emr-dynamodb-connector를 사용하여 DynamoDB 작업을 수행합니다. 이러한 서비스는 속도 제한 옵션을 사용하여 업데이트된 항목의 대량 읽기를 수행한 후 대량 쓰기를 수행합니다.

  단점 - 실행 시간이 속도 제한에 반비례합니다. 이 기능의 경우 읽기와 쓰기 사이에 발생하는 데이터 변경 사항을 덮어쓸 수 있습니다. 글로벌 보조 인덱스(GSI)에서는 읽을 수 없습니다. [Amazon EMR을 사용하여 Amazon DynamoDB Time to Live 속성 채우기: 2부](https://aws.amazon.com/blogs/database/part-2-backfilling-an-amazon-dynamodb-time-to-live-attribute-using-amazon-emr/)를 참조하세요.
+ DynamoDB Shell - SQL과 유사한 쿼리를 사용하는, 속도가 제한된 대량 작업입니다. 효율성을 높이기 위해 GSI에서 읽을 수 있습니다.

  단점 - 실행 시간이 속도 제한에 반비례합니다. [DynamoDB Shell의 속도가 제한된 대량 작업](https://aws.amazon.com/blogs/database/rate-limited-bulk-operations-in-dynamodb-shell/)을 참조하세요.

## 패턴 사용
<a name="BestPractices_EfficientBulkOperations_UsingThePattern"></a>

대량 업데이트는 특히 온디맨드 처리량 모드를 사용하는 경우 비용에 상당한 영향을 미칠 수 있습니다. 프로비저닝된 처리량 모드를 사용하는 경우 속도와 비용 간에 절충이 있습니다. 속도 제한 파라미터를 매우 엄격하게 설정하면 처리 시간이 매우 길어질 수 있습니다. 평균 항목 크기와 속도 제한을 사용하여 업데이트 속도를 대략적으로 결정할 수 있습니다.

또는 업데이트 프로세스의 예상 기간과 평균 항목 크기를 기반으로 프로세스에 필요한 처리량을 결정할 수 있습니다. 각 패턴에 대해 공유된 참조 블로그에서 패턴 사용 전략, 구현 및 제한 사항과 관련한 세부 정보를 제공합니다. 자세한 내용은 [Amazon DynamoDB를 사용한 비용 효율적인 대량 처리](https://aws.amazon.com/blogs/database/cost-effective-bulk-processing-with-amazon-dynamodb/)를 참조하세요.

라이브 DynamoDB 테이블에 대해 대량 업데이트를 수행하는 방법에는 여러 가지가 있습니다. 적절한 접근 방식은 ACID 및/또는 멱등성 요구 사항, 업데이트할 항목 수, API에 대한 친숙도와 같은 요인에 따라 달라집니다. 비용과 시간 간의 균형을 고려하는 것이 중요하며, 위에서 설명한 대부분의 접근 방식은 대량 업데이트 작업에 사용되는 처리량에 대한 속도 제한 옵션을 제공합니다.