

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# DynamoDB 향상된 클라이언트 API의 기본 사항 알아보기
<a name="ddb-en-client-use"></a>

이 항목에서는 DynamoDB 향상된 클라이언트 API의 기본 기능을 설명하고 이를 [표준 DynamoDB 클라이언트 API](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/package-summary.html)와 비교합니다.

DynamoDB 향상된 클라이언트 API를 처음 사용하는 경우 [입문 자습서](ddb-en-client-getting-started.md)를 통해 기본 클래스를 익히는 것이 좋습니다.

## Java에서 DynamoDB 항목
<a name="ddb-en-client-use-usecase"></a>

DynamoDB 테이블은 항목을 저장합니다. 사용 사례에 따라 Java 측 항목은 정적으로 구조화된 데이터 또는 동적으로 생성된 구조의 형태를 취할 수 있습니다.

사용 사례에서 일관된 속성 집합을 가진 항목을 요구하는 경우 [주석이 달린 클래스](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-anno-bean)를 사용하거나 [빌더](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-builder)를 사용하여 적절한 정적 형식의 `TableSchema`을 생성하세요.

또는 다양한 구조로 구성된 항목을 저장해야 하는 경우 `DocumentTableSchema`를 생성하세요. `DocumentTableSchema`는 [향상된 문서 API](ddb-en-client-doc-api.md) 일부이며 정적으로 입력된 기본 키만 필요하며 `EnhancedDocument` 인스턴스와 함께 작동하여 데이터 요소를 보관합니다. 향상된 문서 API에 대해서는 다른 [항목](ddb-en-client-doc-api.md)에서 다룹니다.

## 데이터 모델 클래스의 속성 유형
<a name="ddb-en-client-use-types"></a>

DynamoDB는 Java의 다양한 형식 시스템에 비해 [적은 수의 속성 유형](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes)을 지원하지만 DynamoDB 향상된 클라이언트 API는 Java 클래스의 멤버를 DynamoDB 속성 유형으로 또는 DynamoDB 속성 유형에서 변환하는 메커니즘을 제공합니다.

Java 데이터 클래스의 속성 유형은 프리미티브가 아닌 객체 유형이어야 합니다. 예를 들어 항상 `long` 및 `int` 프리미티브가 아닌 `Long` 및 `Integer` 객체 데이터 형식을 사용합니다.

기본적으로 DynamoDB 향상된 클라이언트 API는 [정수](https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html), [문자열](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html), [BigDecimal](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/BigDecimalAttributeConverter.html), and [인스턴트](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/InstantAsStringAttributeConverter.html)같은 다양한 유형에 대한 속성 변환기를 지원합니다. 목록은 [AttributeConverter 인터페이스의 알려진 구현 클래스](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/AttributeConverter.html)에 표시됩니다. 목록에는 지도, 목록, 세트 등 다양한 유형과 컬렉션이 포함됩니다.

기본적으로 지원되지 않거나 JavaBean 규칙을 준수하지 않는 속성 유형에 대한 데이터를 저장하려면 사용자 정의 `AttributeConverter` 구현을 작성하여 변환을 수행할 수 있습니다. [예제](ddb-en-client-adv-features-conversion.md#ddb-en-client-adv-features-conversion-example)는 속성 변환 단원을 참조하세요.

클래스가 Java Beans 사양을 준수하는 속성 유형(또는 [변경할 수 없는 데이터 클래스](ddb-en-client-use-immut.md))의 데이터를 저장하려면 두 가지 방법을 사용할 수 있습니다.
+ 소스 파일에 액세스할 수 있는 경우 `@DynamoDbBean`(또는 `@DynamoDbImmutable`)로 클래스에 주석을 달 수 있습니다. 중첩 속성에 대해 설명하는 단원에서는 주석이 달린 클래스를 사용하는 [예제](ddb-en-client-adv-features-nested.md#ddb-en-client-adv-features-nested-map-anno)를 보여줍니다.
+ 속성에 대한 JavaBean 데이터 클래스의 소스 파일에 대한 액세스 권한이 없는 경우(또는 액세스 권한이 있는 클래스의 소스 파일에 주석을 달고 싶지 않은 경우) 빌더 접근 방식을 사용할 수 있습니다. 이렇게 하면 키를 정의하지 않고 테이블 스키마가 생성됩니다. 그런 다음 이 테이블 스키마를 다른 테이블 스키마에 중첩하여 매핑을 수행할 수 있습니다. 중첩 속성 단원에는 중첩 스키마 사용을 보여주는 [예제](ddb-en-client-adv-features-nested.md#ddb-en-client-adv-features-nested-map-builder)가 있습니다.

### Null 값
<a name="ddb-en-client-use-types-nulls"></a>

`putItem` 메서드를 사용할 때 향상된 클라이언트는 매핑된 데이터 객체의 null 값 속성을 DynamoDB에 대한 요청에 포함하지 않습니다.

`updateItem` 요청에 대한 SDK의 기본 동작은 `updateItem` 메서드에서 제출하는 객체에서 null로 설정된 DynamoDB의 항목에서 속성을 제거합니다. 일부 속성 값을 업데이트하고 다른 속성 값을 변경하지 않으려면 2가지 옵션을 참고할 수 있습니다.
+ 값을 변경하기 전에 항목을 검색합니다(`getItem` 사용). SDK는 이 접근 방식을 사용하여 모든 업데이트된 값과 이전 값을 DynamoDB에 제출합니다.
+ 항목을 업데이트하기 위한 요청을 구축할 때 `[IgnoreNullsMode](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/IgnoreNullsMode.html).SCALAR_ONLY` 또는 `IgnoreNullsMode.MAPS_ONLY`를 사용합니다. 두 모드 모두 DynamoDB의 스칼라 속성을 나타내는 객체의 null 값 속성을 무시합니다. 이 가이드의 [복잡한 유형이 포함된 항목 업데이트](ddb-en-client-adv-features-nested.md#ddb-en-client-adv-features-nested-updates) 주제에는 `IgnoreNullsMode` 값에 대한 자세한 내용과 복잡한 유형으로 작업하는 방법이 포함되어 있습니다.

다음 예제는 `updateItem()` 메서드의 `ignoreNullsMode()`를 보여줍니다.

```
    public static void updateItemNullsExample() {
        Customer customer = new Customer();
        customer.setCustName("CustomerName");
        customer.setEmail("email");
        customer.setId("1");
        customer.setRegistrationDate(Instant.now());

        logger.info("Original customer: {}", customer);

        // Put item with values for all attributes.
        try {
            customerAsyncDynamoDbTable.putItem(customer).join();
        } catch (RuntimeException rte) {
            logger.error("A exception occurred during putItem: {}", rte.getCause().getMessage(), rte);
            return;
        }

        // Create a Customer instance with the same 'id' and 'email' values, but a different 'name' value.
        // Do not set the 'registrationDate' attribute.
        Customer customerForUpdate = new Customer();
        customerForUpdate.setCustName("NewName");
        customerForUpdate.setEmail("email");
        customerForUpdate.setId("1");

        // Update item without setting the 'registrationDate' property and set IgnoreNullsMode to SCALAR_ONLY.
        try {
            Customer updatedWithNullsIgnored = customerAsyncDynamoDbTable.updateItem(b -> b
                            .item(customerForUpdate)
                            .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY))
                    .join();
            logger.info("Customer updated with nulls ignored: {}", updatedWithNullsIgnored.toString());
        } catch (RuntimeException rte) {
            logger.error("An exception occurred during updateItem: {}", rte.getCause().getMessage(), rte);
            return;
        }

        // Update item without setting the registrationDate attribute and not setting ignoreNulls to true.
        try {
            Customer updatedWithNullsUsed = customerAsyncDynamoDbTable.updateItem(customerForUpdate)
                    .join();
            logger.info("Customer updated with nulls used: {}", updatedWithNullsUsed.toString());
        } catch (RuntimeException rte) {
            logger.error("An exception occurred during updateItem: {}", rte.getCause().getMessage(), rte);
        }
    }


// Logged lines. 
Original customer: Customer [id=1, name=CustomerName, email=email, regDate=2024-10-11T14:12:30.222858Z]
Customer updated with nulls ignored: Customer [id=1, name=NewName, email=email, regDate=2024-10-11T14:12:30.222858Z]
Customer updated with nulls used: Customer [id=1, name=NewName, email=email, regDate=null]
```

## DynamoDB 향상된 클라이언트의 기본 메서드
<a name="ddb-en-client-use-basic-ops"></a>

향상된 클라이언트의 기본 메서드는 이름이 붙은 DynamoDB 서비스 작업에 매핑합니다. 다음 예제는 각 방법의 가장 간단한 변형을 보여줍니다. 향상된 요청 개체를 전달하여 각 메서드를 사용자 지정할 수 있습니다. 향상된 요청 객체는 표준 DynamoDB 클라이언트에서 사용할 수 있는 대부분의 기능을 제공합니다. 이는 AWS SDK for Java 2.x API 참조에 완전히 문서화되어 있습니다.

이 예제에서는 이전에 표시된 [`Customer` 클래스](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-anno-bean-cust)을 사용합니다.

```
// CreateTable
customerTable.createTable();

// GetItem
Customer customer = customerTable.getItem(Key.builder().partitionValue("a123").build());

// UpdateItem
Customer updatedCustomer = customerTable.updateItem(customer);

// PutItem
customerTable.putItem(customer);

// DeleteItem
Customer deletedCustomer = customerTable.deleteItem(Key.builder().partitionValue("a123").sortValue(456).build());

// Query
PageIterable<Customer> customers = customerTable.query(keyEqualTo(k -> k.partitionValue("a123")));

// Scan
PageIterable<Customer> customers = customerTable.scan();

// BatchGetItem
BatchGetResultPageIterable batchResults = 
    enhancedClient.batchGetItem(r -> r.addReadBatch(ReadBatch.builder(Customer.class)
                                      .mappedTableResource(customerTable)
                                      .addGetItem(key1)
                                      .addGetItem(key2)
                                      .addGetItem(key3)
                                      .build()));

// BatchWriteItem
batchResults = enhancedClient.batchWriteItem(r -> r.addWriteBatch(WriteBatch.builder(Customer.class)
                                                   .mappedTableResource(customerTable)
                                                   .addPutItem(customer)
                                                   .addDeleteItem(key1)
                                                   .addDeleteItem(key1)
                                                   .build()));

// TransactGetItems
transactResults = enhancedClient.transactGetItems(r -> r.addGetItem(customerTable, key1)
                                                        .addGetItem(customerTable, key2));

// TransactWriteItems
enhancedClient.transactWriteItems(r -> r.addConditionCheck(customerTable, 
                                                           i -> i.key(orderKey)
                                                                 .conditionExpression(conditionExpression))
                                        .addUpdateItem(customerTable, customer)
                                        .addDeleteItem(customerTable, key));
```

## DynamoDB 향상된 클라이언트와 표준 DynamoDB 클라이언트 비교
<a name="ddb-en-client-use-compare"></a>

[표준](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/package-summary.html) 및 [고급](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/package-summary.html) DynamoDB 클라이언트 API를 모두 사용하면 DynamoDB 테이블을 사용하여 CRUD(생성, 읽기, 업데이트 및 삭제) 데이터 수준 작업을 수행할 수 있습니다. 클라이언트 API 간의 차이는 이를 수행하는 방법에 있습니다. 표준 클라이언트를 사용하면 저수준 데이터 속성으로 직접 작업할 수 있습니다. 향상된 클라이언트 API는 친숙한 Java 클래스를 사용하고 이면의 하위 수준 API에 매핑됩니다.

두 클라이언트 API 모두 데이터 수준 작업을 지원하지만 표준 DynamoDB 클라이언트는 리소스 수준 작업도 지원합니다. 리소스 수준 작업은 백업 생성, 테이블 나열, 테이블 업데이트와 같은 데이터베이스를 관리합니다. 향상된 클라이언트 API는 테이블 생성, 설명, 삭제와 같은 엄선된 리소스 수준 작업을 지원합니다.

두 클라이언트 API에서 사용하는 다양한 접근 방식을 설명하기 위해 다음 코드 예제는 표준 클라이언트와 향상된 클라이언트를 사용하여 동일한 `ProductCatalog` 테이블을 생성하는 방법을 보여줍니다.

### 비교: 표준 DynamoDB 클라이언트를 사용하여 테이블 생성
<a name="ddb-en-client-use-compare-cs1"></a>

```
DependencyFactory.dynamoDbClient().createTable(builder -> builder
        .tableName(TABLE_NAME)
        .attributeDefinitions(
                b -> b.attributeName("id").attributeType(ScalarAttributeType.N),
                b -> b.attributeName("title").attributeType(ScalarAttributeType.S),
                b -> b.attributeName("isbn").attributeType(ScalarAttributeType.S)
        )
        .keySchema(
                builder1 -> builder1.attributeName("id").keyType(KeyType.HASH),
                builder2 -> builder2.attributeName("title").keyType(KeyType.RANGE)
        )
        .globalSecondaryIndexes(builder3 -> builder3
                        .indexName("products_by_isbn")
                        .keySchema(builder2 -> builder2
                                .attributeName("isbn").keyType(KeyType.HASH))
                        .projection(builder2 -> builder2
                                .projectionType(ProjectionType.INCLUDE)
                                .nonKeyAttributes("price", "authors"))
                        .provisionedThroughput(builder4 -> builder4
                                .writeCapacityUnits(5L).readCapacityUnits(5L))
        )
        .provisionedThroughput(builder1 -> builder1
                .readCapacityUnits(5L).writeCapacityUnits(5L))
);
```

### 비교: DynamoDB 향상된 클라이언트를 사용하여 테이블 생성
<a name="ddb-en-client-use-compare-cs2"></a>

```
DynamoDbEnhancedClient enhancedClient = DependencyFactory.enhancedClient();
productCatalog = enhancedClient.table(TABLE_NAME, TableSchema.fromImmutableClass(ProductCatalog.class));
productCatalog.createTable(b -> b
        .provisionedThroughput(b1 -> b1.readCapacityUnits(5L).writeCapacityUnits(5L))
        .globalSecondaryIndices(b2 -> b2.indexName("products_by_isbn")
                .projection(b4 -> b4
                        .projectionType(ProjectionType.INCLUDE)
                        .nonKeyAttributes("price", "authors"))
                .provisionedThroughput(b3 -> b3.writeCapacityUnits(5L).readCapacityUnits(5L))
        )
);
```

향상된 클라이언트는 다음과 같은 주석이 달린 데이터 클래스를 사용합니다. DynamoDB 향상된 클라이언트는 Java 데이터 형식을 DynamoDB 데이터 형식에 매핑하므로 코드가 복잡하지 않고 따라하기 쉽습니다. `ProductCatalog`는 DynamoDB 향상된 클라이언트에서 변경할 수 없는 클래스를 사용하는 예입니다. 매핑된 데이터 클래스에 변경할 수 없는 클래스를 사용하는 방법은 이 항목의 [뒷부분에서 설명합니다](ddb-en-client-use-immut.md).

### `ProductCatalog` 클래스
<a name="ddb-en-client-use-compare-cs3"></a>

```
package org.example.tests.model;

import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

import java.math.BigDecimal;
import java.util.Objects;
import java.util.Set;

@DynamoDbImmutable(builder = ProductCatalog.Builder.class)
public class ProductCatalog implements Comparable<ProductCatalog> {
    private Integer id;
    private String title;
    private String isbn;
    private Set<String> authors;
    private BigDecimal price;


    private ProductCatalog(Builder builder){
        this.authors = builder.authors;
        this.id = builder.id;
        this.isbn = builder.isbn;
        this.price = builder.price;
        this.title = builder.title;
    }

    public static Builder builder(){ return new Builder(); }

    @DynamoDbPartitionKey
    public Integer id() { return id; }
    
    @DynamoDbSortKey
    public String title() { return title; }
    
    @DynamoDbSecondaryPartitionKey(indexNames = "products_by_isbn")
    public String isbn() { return isbn; }
    public Set<String> authors() { return authors; }
    public BigDecimal price() { return price; }


    public static final class Builder {
      private Integer id;
      private String title;
      private String isbn;
      private Set<String> authors;
      private BigDecimal price;
      private Builder(){}

      public Builder id(Integer id) { this.id = id; return this; }
      public Builder title(String title) { this.title = title; return this; }
      public Builder isbn(String ISBN) { this.isbn = ISBN; return this; }
      public Builder authors(Set<String> authors) { this.authors = authors; return this; }
      public Builder price(BigDecimal price) { this.price = price; return this; }
      public ProductCatalog build() { return new ProductCatalog(this); }
  }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("ProductCatalog{");
        sb.append("id=").append(id);
        sb.append(", title='").append(title).append('\'');
        sb.append(", isbn='").append(isbn).append('\'');
        sb.append(", authors=").append(authors);
        sb.append(", price=").append(price);
        sb.append('}');
        return sb.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductCatalog that = (ProductCatalog) o;
        return id.equals(that.id) && title.equals(that.title) && Objects.equals(isbn, that.isbn) && Objects.equals(authors, that.authors) && Objects.equals(price, that.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, title, isbn, authors, price);
    }

    @Override
    @DynamoDbIgnore
    public int compareTo(ProductCatalog other) {
        if (this.id.compareTo(other.id) != 0){
            return this.id.compareTo(other.id);
        } else {
            return this.title.compareTo(other.title);
        }
    }
}
```

다음 두 개의 일괄 쓰기 코드 예제는 향상된 클라이언트와 달리 표준 클라이언트를 사용할 때 장황하고 형식 안전성이 부족하다는 것을 보여줍니다.

### 비교: 표준 DynamoDB 클라이언트를 사용하는 일괄 쓰기
<a name="ddb-en-client-use-compare-cs4"></a>

```
    public static void batchWriteStandard(DynamoDbClient dynamoDbClient, String tableName) {

        Map<String, AttributeValue> catalogItem = Map.of(
                "authors", AttributeValue.builder().ss("a", "b").build(),
                "id", AttributeValue.builder().n("1").build(),
                "isbn", AttributeValue.builder().s("1-565-85698").build(),
                "title", AttributeValue.builder().s("Title 1").build(),
                "price", AttributeValue.builder().n("52.13").build());

        Map<String, AttributeValue> catalogItem2 = Map.of(
                "authors", AttributeValue.builder().ss("a", "b", "c").build(),
                "id", AttributeValue.builder().n("2").build(),
                "isbn", AttributeValue.builder().s("1-208-98073").build(),
                "title", AttributeValue.builder().s("Title 2").build(),
                "price", AttributeValue.builder().n("21.99").build());

        Map<String, AttributeValue> catalogItem3 = Map.of(
                "authors", AttributeValue.builder().ss("g", "k", "c").build(),
                "id", AttributeValue.builder().n("3").build(),
                "isbn", AttributeValue.builder().s("7-236-98618").build(),
                "title", AttributeValue.builder().s("Title 3").build(),
                "price", AttributeValue.builder().n("42.00").build());

        Set<WriteRequest> writeRequests = Set.of(
                WriteRequest.builder().putRequest(b -> b.item(catalogItem)).build(),
                WriteRequest.builder().putRequest(b -> b.item(catalogItem2)).build(),
                WriteRequest.builder().putRequest(b -> b.item(catalogItem3)).build());

        Map<String, Set<WriteRequest>> productCatalogItems = Map.of(
                "ProductCatalog", writeRequests);

        BatchWriteItemResponse response = dynamoDbClient.batchWriteItem(b -> b.requestItems(productCatalogItems));

        logger.info("Unprocessed items: " + response.unprocessedItems().size());
    }
```

### 비교: DynamoDB 향상된 클라이언트를 사용하는 일괄 쓰기
<a name="ddb-en-client-use-compare-cs5"></a>

```
    public static void batchWriteEnhanced(DynamoDbTable<ProductCatalog> productCatalog) {
        ProductCatalog prod = ProductCatalog.builder()
                .id(1)
                .isbn("1-565-85698")
                .authors(new HashSet<>(Arrays.asList("a", "b")))
                .price(BigDecimal.valueOf(52.13))
                .title("Title 1")
                .build();
        ProductCatalog prod2 = ProductCatalog.builder()
                .id(2)
                .isbn("1-208-98073")
                .authors(new HashSet<>(Arrays.asList("a", "b", "c")))
                .price(BigDecimal.valueOf(21.99))
                .title("Title 2")
                .build();
        ProductCatalog prod3 = ProductCatalog.builder()
                .id(3)
                .isbn("7-236-98618")
                .authors(new HashSet<>(Arrays.asList("g", "k", "c")))
                .price(BigDecimal.valueOf(42.00))
                .title("Title 3")
                .build();

        BatchWriteResult batchWriteResult = DependencyFactory.enhancedClient()
                .batchWriteItem(b -> b.writeBatches(
                        WriteBatch.builder(ProductCatalog.class)
                                .mappedTableResource(productCatalog)
                                .addPutItem(prod).addPutItem(prod2).addPutItem(prod3)
                                .build()
                ));
        logger.info("Unprocessed items: " + batchWriteResult.unprocessedPutItemsForTable(productCatalog).size());
    }
```

# 변경할 수 없는 데이터 클래스로 작업
<a name="ddb-en-client-use-immut"></a>

DynamoDB 향상된 클라이언트 API의 매핑 기능은 변경할 수 없는 데이터 클래스와 함께 작동합니다. 불변 클래스에는 접근자만 포함되며 SDK가 클래스의 인스턴스를 생성하는 데 사용하는 빌더 클래스가 필요합니다. 변경 불가능한 클래스는 [Customer 클래스에](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-anno-bean-cust) 표시된 `@DynamoDbBean` 주석을 사용하는 대신 사용할 빌더 클래스를 나타내는 매개변수를 사용하는 `@DynamoDbImmutable` 주석을 사용합니다.

다음 클래스는 `Customer`의 변경할 수 없는 버전입니다.

```
package org.example.tests.model.immutable;

import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

import java.time.Instant;

@DynamoDbImmutable(builder = CustomerImmutable.Builder.class)
public class CustomerImmutable {
    private final String id;
    private final String name;
    private final String email;
    private final Instant regDate;

    private CustomerImmutable(Builder b) {
        this.id = b.id;
        this.email = b.email;
        this.name = b.name;
        this.regDate = b.regDate;
    }

    // This method will be automatically discovered and used by the TableSchema.
    public static Builder builder() { return new Builder(); }

    @DynamoDbPartitionKey
    public String id() { return this.id; }

    @DynamoDbSortKey
    public String email() { return this.email; }

    @DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")
    public String name() { return this.name; }

    @DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})
    public Instant regDate() { return this.regDate; }

    public static final class Builder {
        private String id;
        private String email;
        private String name;
        private Instant regDate;

        // The private Builder constructor is visible to the enclosing CustomerImmutable class.
        private Builder() {}

        public Builder id(String id) { this.id = id; return this; }
        public Builder email(String email) { this.email = email; return this; }
        public Builder name(String name) { this.name = name; return this; }
        public Builder regDate(Instant regDate) { this.regDate = regDate; return this; }

        // This method will be automatically discovered and used by the TableSchema.
        public CustomerImmutable build() { return new CustomerImmutable(this); }
    }
}
```

`@DynamoDbImmutable`를 사용해 데이터 클래스에 주석을 달 때는 다음 요구 사항을 충족해야 합니다.

1. `Object.class`의 오버라이드도 아니고 `@DynamoDbIgnore`를 사용해 주석이 추가되지도 않은 모든 메서드는 DynamoDB 테이블의 속성에 대한 접근자여야 합니다.

1. 모든 접근자는 빌더 클래스에 해당하는 대소문자를 구분하는 설정자를 가져야 합니다.

1. 다음 시공 조건 중 하나만 충족해야 합니다.
   + 빌더 클래스에는 공개 기본 생성자가 있어야 합니다.
   + 데이터 클래스에는 매개 변수를 사용하지 않고 빌더 클래스의 인스턴스를 반환하는 이름이 `builder()`로 지정된 공용 정적 메서드가 있어야 합니다. 이 옵션은 변경할 수 없는 `Customer` 클래스에 표시됩니다.

1.  빌더 클래스에는 매개 변수를 사용하지 않고 변경 불가능한 클래스의 인스턴스를 반환하는 `build()`로 이름이 지정된 공용 메서드가 있어야 합니다.

변경할 수 없는 클래스를 위한 `TableSchema`를 만들려면 다음 코드 조각과 같이 `TableSchema`의 `fromImmutableClass()` 메서드를 사용하세요.

```
static final TableSchema<CustomerImmutable> customerImmutableTableSchema = 
                         TableSchema.fromImmutableClass(CustomerImmutable.class);
```

변경 가능한 클래스에서 DynamoDB 테이블을 생성할 수 있는 것처럼, 다음 코드 조각 예제와 같이 `DynamoDbTable`의 `createTable()`를 *한 번* 호출하여 변경할 수 없는 클래스에서 테이블을 생성할 수 있습니다.

```
static void createTableFromImmutable(DynamoDbEnhancedClient enhancedClient, String tableName, DynamoDbWaiter waiter){
    // First, create an in-memory representation of the table using the 'table()' method of the DynamoDb Enhanced Client.
    // 'table()' accepts a name for the table and a TableSchema instance that you created previously.
    DynamoDbTable<CustomerImmutable> customerDynamoDbTable = enhancedClient
            .table(tableName, TableSchema.fromImmutableClass(CustomerImmutable.class));
        
    // Second, call the 'createTable()' method on the DynamoDbTable instance.
    customerDynamoDbTable.createTable();
    waiter.waitUntilTableExists(b -> b.tableName(tableName));
}
```

## Lombok과 같은 타사 라이브러리를 사용
<a name="ddb-en-client-use-immut-lombok"></a>

[Project Lombok과](https://projectlombok.org/) 같은 타사 라이브러리는 변경할 수 없는 객체와 관련된 보일러러플레이트 코드를 생성하는 데 도움이 됩니다. DynamoDB 향상된 클라이언트 API는 데이터 클래스가 이 단원에 자세히 설명된 규칙을 따르는 한 이러한 라이브러리와 함께 작동합니다.

다음 예제에서는 Lombok 주석이 있는 변경 불가능한 `CustomerImmutable` 클래스를 보여줍니다. Lombok의 `onMethod` 기능이 속성 기반 DynamoDB 주석(예:`@DynamoDbPartitionKey`)을 생성된 코드에 복사하는 방법에 주목하세요.

```
@Value
@Builder
@DynamoDbImmutable(builder = Customer.CustomerBuilder.class)
public class Customer {
    @Getter(onMethod_=@DynamoDbPartitionKey)
    private String id;

    @Getter(onMethod_=@DynamoDbSortKey)
    private String email;

    @Getter(onMethod_=@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name"))
    private String name;

    @Getter(onMethod_=@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"}))
    private Instant createdDate;
}
```

# 표현식 및 조건 사용
<a name="ddb-en-client-expressions"></a>

DynamoDB 향상된 클라이언트 API의 표현식은 [DynamoDB 식](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.html)을 Java로 표현한 것입니다.

DynamoDB 향상된 클라이언트 API는 세 가지 유형의 식을 사용합니다.

[표현식](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/Expression.html)  
`Expression` 클래스는 조건과 필터를 정의할 때 사용됩니다.

[https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.html)  
이 유형의 표현식은 쿼리 작업의 [주요 조건](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions)을 나타냅니다.

[https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.html)  
이 클래스는 DynamoDB [업데이트 표현식](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html)을 작성하는 데 도움이 되며, 현재 항목을 업데이트할 때 확장 프레임워크에서 사용됩니다.

## 표현 해부학
<a name="ddb-en-client-expressions-compoonents"></a>

표현식은 다음과 같이 구성됩니다.
+ 문자열 표현식(필수). 문자열에는 속성 이름 및 속성 값에 대한 자리 표시자 이름이 있는 DynamoDB 논리 표현식이 포함되어 있습니다.
+ 표현식 값 맵(일반적으로 필수).
+ 표현식 이름 맵(선택 사항).

빌더를 사용하여 다음과 같은 일반적인 형식을 취하는 `Expression` 객체를 생성합니다.

```
Expression expression = Expression.builder()
                            .expression(<String>)
                            .expressionNames(<Map>)
                            .expressionValues(<Map>)
                           .build()
```

`Expression`는 일반적으로 표현식 값의 맵이 필요합니다. 맵은 문자열 표현식의 자리 표시자 값을 제공합니다. 맵 키는 콜론(`:`) 이 붙은 자리 표시자 이름으로 구성되며 맵 값은 [AttributeValue](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/model/AttributeValue.html)의 인스턴스입니다. [AttributeValues](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/internal/AttributeValues.html) 클래스에는 리터럴에서 `AttributeValue` 인스턴스를 생성할 수 있는 편리한 메서드가 있습니다. 또는 `AttributeValue.Builder`를 사용하여 `AttributeValue` 인스턴스를 생성할 수도 있습니다.

다음 코드 조각은 주석 줄 2 뒤에 두 개의 항목이 있는 맵을 보여줍니다. `expression()` 메서드에 전달된 문자열(주석 줄 1 뒤에 표시됨)에는 DynamoDB가 작업을 수행하기 전에 확인하는 자리 표시자가 포함되어 있습니다. *가격*은 허용 가능한 속성 이름이므로 이 코드 조각에는 표현식 이름 맵이 포함되어 있지 않습니다.

```
    public static void scanAsync(DynamoDbAsyncTable productCatalog) {
        ScanEnhancedRequest request = ScanEnhancedRequest.builder()
                .consistentRead(true)
                .attributesToProject("id", "title", "authors", "price")
                .filterExpression(Expression.builder()
                        // 1. :min_value and :max_value are placeholders for the values provided by the map
                        .expression("price >= :min_value AND price <= :max_value")
                        // 2. Two values are needed for the expression and each is supplied as a map entry.
                        .expressionValues(
                                Map.of( ":min_value", numberValue(8.00),
                                        ":max_value", numberValue(400_000.00)))
                        .build())
                .build();
```

DynamoDB 테이블의 속성 이름이 예약어이거나, 숫자로 시작하거나, 공백이 포함된 경우에는 `Expression`에 대해 표현식 이름 맵이 필요합니다.

예를 들어 이전 코드 예제의 속성 이름이 `price`가 `1price`인 경우 다음의 예제와 같이 예제를 수정해야 합니다.

```
        ScanEnhancedRequest request = ScanEnhancedRequest.builder()
                .filterExpression(Expression.builder()
                        .expression("#price >= :min_value AND #price <= :max_value")
                        .expressionNames( Map.of("#price", "1price") )
                        .expressionValues(
                                Map.of(":min_value", numberValue(8.00),
                                        ":max_value", numberValue(400_000.00)))
                        .build())
                .build();
```

표현식 이름의 자리 표시자는 파운드 기호(`#`)로 시작합니다. 표현식 이름 맵의 항목은 자리 표시자를 키로 사용하고 속성 이름을 값으로 사용합니다. 맵은 `expressionNames()` 메서드를 사용하여 표현식 빌더에 추가됩니다. DynamoDB는 작업을 수행하기 전에 속성 이름을 확인합니다.

문자열 표현식에 함수를 사용하는 경우에는 표현식 값이 필요하지 않습니다. 표현식 함수의 예는 `attribute_exists(<attribute_name>)`과 같습니다.

다음 예제는 [DynamoDB 함수](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Functions)를 사용하여 `Expression`를 빌드합니다. 이 예제의 표현식 문자열은 자리 표시자를 사용하지 않습니다. 이 표현식은 `movie` 속성 값이 데이터 개체의 `movie` 속성과 같은 항목이 데이터베이스에 이미 존재하는지 확인하는 `putItem` 작업에 사용할 수 있습니다.

```
Expression exp = Expression.builder().expression("attribute_not_exists (movie)").build();
```

DynamoDB 개발자 안내서에는 DynamoDB와 함께 사용되는 [하위 수준 표현식](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.html)에 대한 전체 정보가 포함되어 있습니다.

## 조건 표현식 및 조건문
<a name="ddb-en-client-expressions-cond"></a>

`putItem()`, `updateItem()` 및 `deleteItem()` 메서드를 사용할 때와 트랜잭션 및 배치 작업을 사용할 때는 `[Expression](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/Expression.html)` 객체를 사용하여 DynamoDB가 작업을 진행하기 위해 충족해야 하는 조건을 지정합니다. 이러한 식을 조건식이라고 합니다. 예제는 이 가이드에 나와 있는 [트랜잭션 예제](ddb-en-client-use-multiop-trans.md#ddb-en-client-use-multiop-trans-writeitems-opcondition)의 `addDeleteItem()` 메서드(주석 줄 1 뒤)에 사용된 조건식을 참조하세요.

`query()` 메서드를 사용하여 작업할 경우 조건은 [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.html)로 표현됩니다. `QueryConditional` 클래스에는 DynamoDB에서 읽을 항목을 결정하는 기준을 작성하는 데 도움이 되는 몇 가지 정적 편의 메서드가 있습니다.

`QueryConditionals`의 예제는 이 가이드 [`Query` 메서드 예제](ddb-en-client-use-multirecord.md#ddb-en-client-use-multirecord-query-example) 단원의 첫 번째 코드 예제를 참조하세요.

## 필터 표현식
<a name="ddb-en-client-expressions-filter"></a>

필터 표현식은 스캔 및 쿼리 작업에서 반환되는 항목을 필터링하는 데 사용됩니다.

데이터베이스에서 모든 데이터를 읽은 후 필터 표현식이 적용되므로 읽기 비용은 필터가 없는 것과 동일합니다. *Amazon DynamoDB 개발자 안내서*에는 [쿼리](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.FilterExpression) 및 [스캔](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.FilterExpression) 작업 모두에 필터 표현식을 사용하는 방법에 대한 자세한 정보가 있습니다.

다음 예제는 스캔 요청에 추가된 필터 표현식을 보여줍니다. 기준에 따라 반품되는 품목은 가격이 8.00에서 80.00 사이인 품목으로 제한됩니다.

```
        Map<String, AttributeValue> expressionValues = Map.of(
                ":min_value", numberValue(8.00),
                ":max_value", numberValue(80.00));

        ScanEnhancedRequest request = ScanEnhancedRequest.builder()
                .consistentRead(true)
                // 1. the 'attributesToProject()' method allows you to specify which values you want returned.
                .attributesToProject("id", "title", "authors", "price")
                // 2. Filter expression limits the items returned that match the provided criteria.
                .filterExpression(Expression.builder()
                        .expression("price >= :min_value AND price <= :max_value")
                        .expressionValues(expressionValues)
                        .build())
                .build();
```

## 업데이트 표현식
<a name="ddb-en-client-expressions-update"></a>

DynamoDB 향상된 클라이언트의 `updateItem()` 메서드는 DynamoDB의 항목을 업데이트하는 표준 방법을 제공합니다. 하지만 더 많은 기능이 필요한 경우 [UpdateExpressions](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.html)는 DynamoDB [업데이트 표현식 구문](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html)을 형식에 구애받지 않고 표현할 수 있습니다. 예를 들어 DynamoDB에서 항목을 먼저 읽지 않고 값을 늘리거나 개별 구성원을 목록에 추가하는 데 `UpdateExpressions`을 사용할 수 있습니다. 업데이트 표현식은 `updateItem()` 메서드의 사용자 지정 확장에서 사용할 수 있습니다.

업데이트 표현식을 사용하는 예제는 이 가이드의 [사용자 지정 확장 예제](ddb-en-client-extensions-custom.md)를 참조하세요.

업데이트 표현식에 대한 자세한 내용은 [Amazon DynamoDB 개발자 안내서](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html)를 참조하세요.

# 페이지 매김된 결과 작업: 스캔 및 쿼리
<a name="ddb-en-client-use-multirecord"></a>

DynamoDB 향상된 클라이언트 API의 `scan`, `query` 및 `batch` 메서드는 하나 이상의 *페이지*가 포함된 응답을 반환합니다. 페이지에는 하나 이상의 항목이 포함되어 있습니다. 코드는 페이지별로 응답을 처리하거나 개별 항목을 처리할 수 있습니다.

동기 `DynamoDbEnhancedClient` 클라이언트가 반환한 페이지 단위 응답은 [PageIterable](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/PageIterable.html) 객체를 반환하고, 비동기 `DynamoDbEnhancedAsyncClient`에서 반환한 응답은 [PagePublisher](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/PagePublisher.html) 객체를 반환합니다.

이 단원에서는 페이지를 매긴 결과 처리를 살펴보고 스캔 및 쿼리 API를 사용하는 예제를 제공합니다.

## 테이블 스캔
<a name="ddb-en-client-use-multirecord-scan"></a>

SDK의 [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.html#scan(java.util.function.Consumer)](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.html#scan(java.util.function.Consumer)) 메서드는 동일한 이름의 [DynamoDB 작업](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html)에 해당합니다. DynamoDB 향상된 클라이언트 API는 동일한 옵션을 제공하지만 익숙한 객체 모델을 사용하고 페이지 매김을 자동으로 처리합니다.

먼저 동기 매핑 클래스 [DynamoDBTable](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.html)의 `scan` 메서드를 살펴보면서 `PageIterable` 인터페이스를 살펴봅니다.

### 동기식 API를 사용
<a name="ddb-en-client-use-multirecord-scan-sync"></a>

다음 예제는 [표현식](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/Expression.html)을 사용하여 반환되는 항목을 필터링하는 `scan` 메서드를 보여줍니다. [ProductCatalog](ddb-en-client-use.md#ddb-en-client-use-compare-cs3)는 이전에 표시된 모델 객체입니다.

주석 줄 2 뒤에 표시되는 필터링 표현식은 반환되는 `ProductCatalog` 항목을 가격 값이 8.00\$180.00인 항목으로 제한합니다.

또한이 예제에서는 주석 줄 1 다음에 표시된 `attributesToProject` 메서드를 사용하여 `isbn` 값을 제외합니다.

주석 줄 3 이후에는 `scan` 메서드가 `PageIterable` 객체, `pagedResults`를 반환합니다. `PageIterable`의 `stream` 메서드는 페이지를 처리하는 데 사용할 수 있는 [https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html) 객체를 반환합니다. 이 예제는 페이지 수를 세고 기록합니다.

이 예제는 주석 줄 4부터 시작하여 `ProductCatalog` 항목 액세스의 두 가지 변형을 보여줍니다. 주석 줄 4a 이후의 버전은 각 페이지를 스트리밍하고 각 페이지의 항목을 정렬하고 로깅합니다. 주석 줄 4b 이후의 버전은 페이지 반복을 건너뛰고 항목에 직접 액세스합니다.

`PageIterable` 인터페이스는 2개의 상위 인터페이스 [https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html](https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html) 및 [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/core/pagination/sync/SdkIterable.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/core/pagination/sync/SdkIterable.html)로 결과를 처리하는 다양한 방법을 제공합니다. `Iterable`은 `forEach`, `iterator` 및 `spliterator` 메서드를 가져오고 `SdkIterable`은 `stream` 메서드를 가져옵니다.

```
    public static void scanSync(DynamoDbTable<ProductCatalog> productCatalog) {

        Map<String, AttributeValue> expressionValues = Map.of(
                ":min_value", numberValue(8.00),
                ":max_value", numberValue(80.00));

        ScanEnhancedRequest request = ScanEnhancedRequest.builder()
                .consistentRead(true)
                // 1. the 'attributesToProject()' method allows you to specify which values you want returned.
                .attributesToProject("id", "title", "authors", "price")
                // 2. Filter expression limits the items returned that match the provided criteria.
                .filterExpression(Expression.builder()
                        .expression("price >= :min_value AND price <= :max_value")
                        .expressionValues(expressionValues)
                        .build())
                .build();

        // 3. A PageIterable object is returned by the scan method.
        PageIterable<ProductCatalog> pagedResults = productCatalog.scan(request);
        logger.info("page count: {}", pagedResults.stream().count());

        // 4. Log the returned ProductCatalog items using two variations.
        // 4a. This version sorts and logs the items of each page.
        pagedResults.stream().forEach(p -> p.items().stream()
                .sorted(Comparator.comparing(ProductCatalog::price))
                .forEach(
                        item -> logger.info(item.toString())
                ));
        // 4b. This version sorts and logs all items for all pages.
        pagedResults.items().stream()
                .sorted(Comparator.comparing(ProductCatalog::price))
                .forEach(
                        item -> logger.info(item.toString())
                );
    }
```

### 비동기 API 사용
<a name="ddb-en-client-use-multirecord-scan-async"></a>

비동기 `scan` 메서드는 결과를 `PagePublisher` 객체로 반환합니다. `PagePublisher` 인터페이스에는 응답 페이지를 처리하는 데 사용할 수 있는 두 가지 `subscribe` 메서드가 있습니다. 한 가지 `subscribe` 메서드는 `org.reactivestreams.Publisher` 상위 인터페이스에서 가져온 것입니다. 이 첫 번째 옵션을 사용하여 페이지를 처리하려면 `subscribe` 메서드에 `[Subscriber](https://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/org/reactivestreams/Subscriber.html)` 인스턴스를 전달하세요. 다음 예제는 `subscribe` 메서드 사용을 보여 줍니다.

두 번째 `subscribe` 메서드는 [SdkPublisher](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/core/async/SdkPublisher.html) 인터페이스에서 가져온 것입니다. `subscribe`의 이 버전에서는 `Subscriber`가 아닌 [https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html](https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html)를 받아들입니다. 이 `subscribe` 메서드 변형은 다음 두 번째 예제에 나와 있습니다.

다음 예제는 이전 예제와 동일한 필터 표현식을 사용하는 `scan` 메서드의 비동기 버전을 보여줍니다.

주석 줄 3번 다음에 `DynamoDbAsyncTable.scan`는 `PagePublisher` 객체를 반환합니다. 다음 줄에서 코드는 `org.reactivestreams.Subscriber` 인터페이스의 인스턴스를 만들고, `ProductCatalogSubscriber`는 주석 줄 4 뒤 `PagePublisher`를 구독합니다.

`Subscriber` 객체는 `ProductCatalogSubscriber` 클래스 예제의 주석 줄 8 다음에 있는 `onNext` 메서드의 각 페이지에서 `ProductCatalog` 항목을 수집합니다. 항목은 전용 `List` 변수에 저장되며 `ProductCatalogSubscriber.getSubscribedItems()` 메서드를 사용하여 호출 코드에서 액세스할 수 있습니다. 이는 주석 줄 5 이후에 호출됩니다.

목록이 검색되면 코드는 모든 `ProductCatalog` 항목을 가격별로 정렬하고 각 항목을 기록합니다.

`ProductCatalogSubscriber` 클래스의 [CountDownLatch](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/CountDownLatch.html)는 모든 항목이 목록에 추가될 때까지 호출 스레드를 차단한 다음 주석 줄 5 이후에 계속합니다.

```
    public static void scanAsync(DynamoDbAsyncTable productCatalog) {
        ScanEnhancedRequest request = ScanEnhancedRequest.builder()
                .consistentRead(true)
                .attributesToProject("id", "title", "authors", "price")
                .filterExpression(Expression.builder()
                        // 1. :min_value and :max_value are placeholders for the values provided by the map
                        .expression("price >= :min_value AND price <= :max_value")
                        // 2. Two values are needed for the expression and each is supplied as a map entry.
                        .expressionValues(
                                Map.of( ":min_value", numberValue(8.00),
                                        ":max_value", numberValue(400_000.00)))
                        .build())
                .build();

        // 3. A PagePublisher object is returned by the scan method.
        PagePublisher<ProductCatalog> pagePublisher = productCatalog.scan(request);
        ProductCatalogSubscriber subscriber = new ProductCatalogSubscriber();
        // 4. Subscribe the ProductCatalogSubscriber to the PagePublisher.
        pagePublisher.subscribe(subscriber);
        // 5. Retrieve all collected ProductCatalog items accumulated by the subscriber.
        subscriber.getSubscribedItems().stream()
                .sorted(Comparator.comparing(ProductCatalog::price))
                .forEach(item ->
                        logger.info(item.toString()));
        // 6. Use a Consumer to work through each page.
        pagePublisher.subscribe(page -> page
                        .items().stream()
                        .sorted(Comparator.comparing(ProductCatalog::price))
                        .forEach(item ->
                                logger.info(item.toString())))
                .join(); // If needed, blocks the subscribe() method thread until it is finished processing.
        // 7. Use a Consumer to work through each ProductCatalog item.
        pagePublisher.items()
                .subscribe(product -> logger.info(product.toString()))
                .exceptionally(failure -> {
                    logger.error("ERROR  - ", failure);
                    return null;
                })
                .join(); // If needed, blocks the subscribe() method thread until it is finished processing.
    }
```

```
    private static class ProductCatalogSubscriber implements Subscriber<Page<ProductCatalog>> {
        private CountDownLatch latch = new CountDownLatch(1);
        private Subscription subscription;
        private List<ProductCatalog> itemsFromAllPages = new ArrayList<>();

        @Override
        public void onSubscribe(Subscription sub) {
            subscription = sub;
            subscription.request(1L);
            try {
                latch.await(); // Called by main thread blocking it until latch is released.
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void onNext(Page<ProductCatalog> productCatalogPage) {
            // 8. Collect all the ProductCatalog instances in the page, then ask the publisher for one more page.
            itemsFromAllPages.addAll(productCatalogPage.items());
            subscription.request(1L);
        }

        @Override
        public void onError(Throwable throwable) {
        }

        @Override
        public void onComplete() {
            latch.countDown(); // Call by subscription thread; latch releases.
        }

        List<ProductCatalog> getSubscribedItems() {
            return this.itemsFromAllPages;
        }
    }
```

다음 코드 조각 예제는 주석 줄 6을 다음에 `Consumer`를 받아들이는 `PagePublisher.subscribe` 메서드의 버전을 사용합니다. Java 람다 매개변수는 각 항목을 추가로 처리하는 페이지를 사용합니다. 이 예에서는 각 페이지가 처리되고 각 페이지의 항목이 정렬된 후 기록됩니다.

```
        // 6. Use a Consumer to work through each page.
        pagePublisher.subscribe(page -> page
                        .items().stream()
                        .sorted(Comparator.comparing(ProductCatalog::price))
                        .forEach(item ->
                                logger.info(item.toString())))
                .join(); // If needed, blocks the subscribe() method thread until it is finished processing.
```

`PagePublisher`의 `items` 메서드는 모델 인스턴스를 언래핑하여 코드가 항목을 직접 처리할 수 있도록 합니다. 이 접근 방법은 다음 코드 조각에 나와 있습니다.

```
        // 7. Use a Consumer to work through each ProductCatalog item.
        pagePublisher.items()
                .subscribe(product -> logger.info(product.toString()))
                .exceptionally(failure -> {
                    logger.error("ERROR  - ", failure);
                    return null;
                })
                .join(); // If needed, blocks the subscribe() method thread until it is finished processing.
```

## 테이블 쿼리
<a name="ddb-en-client-use-multirecord-query"></a>

DynamoDB 향상된 클라이언트를 사용하여 테이블을 쿼리하고 특정 기준과 일치하는 여러 항목을 검색할 수 있습니다. [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.html#query(software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest)](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.html#query(software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest)) 메서드는 데이터 클래스에 정의된 `@DynamoDbPartitionKey` 및 `@DynamoDbSortKey` 주석(선택 사항)을 사용하여 프라이머리 키 값을 기반으로 항목을 찾습니다.

`query()` 메서드에는 파티션 키 값이 필요하며 경우에 따라 정렬 키 조건을 수락하여 결과를 추가로 구체화합니다. `scan` API와 마찬가지로 쿼리는 동기식 호출의 `PageIterable`을 반환하고 비동기식 직접 호출의 `PagePublisher`를 반환합니다.

### `Query` 메서드 예제
<a name="ddb-en-client-use-multirecord-query-example"></a>

다음 `query()` 메서드 코드 예제에서는 `MovieActor` 클래스를 사용합니다. 데이터 클래스는 파티션 키의 **`movie`** 속성과 정렬 키의 **`actor`** 속성으로 구성된 복합 프라이머리 키를 정의합니다.

#### `MovieActor` 클래스
<a name="ddb-en-client-use-movieactor-class"></a>

```
package org.example.tests.model;

import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

import java.util.Objects;

@DynamoDbBean
public class MovieActor implements Comparable<MovieActor> {

    private String movieName;
    private String actorName;
    private String actingAward;
    private Integer actingYear;
    private String actingSchoolName;

    @DynamoDbPartitionKey
    @DynamoDbAttribute("movie")
    public String getMovieName() {
        return movieName;
    }

    public void setMovieName(String movieName) {
        this.movieName = movieName;
    }

    @DynamoDbSortKey
    @DynamoDbAttribute("actor")
    public String getActorName() {
        return actorName;
    }

    public void setActorName(String actorName) {
        this.actorName = actorName;
    }

    @DynamoDbSecondaryPartitionKey(indexNames = "acting_award_year")
    @DynamoDbAttribute("actingaward")
    public String getActingAward() {
        return actingAward;
    }

    public void setActingAward(String actingAward) {
        this.actingAward = actingAward;
    }

    @DynamoDbSecondarySortKey(indexNames = {"acting_award_year", "movie_year"})
    @DynamoDbAttribute("actingyear")
    public Integer getActingYear() {
        return actingYear;
    }

    public void setActingYear(Integer actingYear) {
        this.actingYear = actingYear;
    }

    @DynamoDbAttribute("actingschoolname")
    public String getActingSchoolName() {
        return actingSchoolName;
    }

    public void setActingSchoolName(String actingSchoolName) {
        this.actingSchoolName = actingSchoolName;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("MovieActor{");
        sb.append("movieName='").append(movieName).append('\'');
        sb.append(", actorName='").append(actorName).append('\'');
        sb.append(", actingAward='").append(actingAward).append('\'');
        sb.append(", actingYear=").append(actingYear);
        sb.append(", actingSchoolName='").append(actingSchoolName).append('\'');
        sb.append('}');
        return sb.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MovieActor that = (MovieActor) o;
        return Objects.equals(movieName, that.movieName) && Objects.equals(actorName, that.actorName) && Objects.equals(actingAward, that.actingAward) && Objects.equals(actingYear, that.actingYear) && Objects.equals(actingSchoolName, that.actingSchoolName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(movieName, actorName, actingAward, actingYear, actingSchoolName);
    }

    @Override
    public int compareTo(MovieActor o) {
        if (this.movieName.compareTo(o.movieName) != 0){
            return this.movieName.compareTo(o.movieName);
        } else {
            return this.actorName.compareTo(o.actorName);
        }
    }
}
```

다음 항목에 대한 쿼리를 따르는 코드 예제.

#### `MovieActor` 테이블 내 항목
<a name="ddb-en-client-use-movieactor-items"></a>

```
MovieActor{movieName='movie01', actorName='actor0', actingAward='actingaward0', actingYear=2001, actingSchoolName='null'}
MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'}
MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'}
MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'}
MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}
MovieActor{movieName='movie02', actorName='actor0', actingAward='actingaward0', actingYear=2002, actingSchoolName='null'}
MovieActor{movieName='movie02', actorName='actor1', actingAward='actingaward1', actingYear=2002, actingSchoolName='actingschool1'}
MovieActor{movieName='movie02', actorName='actor2', actingAward='actingaward2', actingYear=2002, actingSchoolName='actingschool2'}
MovieActor{movieName='movie02', actorName='actor3', actingAward='actingaward3', actingYear=2002, actingSchoolName='null'}
MovieActor{movieName='movie02', actorName='actor4', actingAward='actingaward4', actingYear=2002, actingSchoolName='actingschool4'}
MovieActor{movieName='movie03', actorName='actor0', actingAward='actingaward0', actingYear=2003, actingSchoolName='null'}
MovieActor{movieName='movie03', actorName='actor1', actingAward='actingaward1', actingYear=2003, actingSchoolName='actingschool1'}
MovieActor{movieName='movie03', actorName='actor2', actingAward='actingaward2', actingYear=2003, actingSchoolName='actingschool2'}
MovieActor{movieName='movie03', actorName='actor3', actingAward='actingaward3', actingYear=2003, actingSchoolName='null'}
MovieActor{movieName='movie03', actorName='actor4', actingAward='actingaward4', actingYear=2003, actingSchoolName='actingschool4'}
```

다음 코드는 `keyEqual`(주석 줄 1 이후) 및 `sortGreaterThanOrEqualTo`(주석 줄 1a 이후)의 2가지 `QueryConditional` 인스턴스를 정의합니다.

#### 파티션 키로 항목 쿼리
<a name="keyEqual-query-conditional-example"></a>

`keyEqual` 인스턴스는 파티션 키 값이 **`movie01`**인 항목과 일치합니다.

또한 이 예제는 주석 줄 2 이후 **`actingschoolname`**이 없는 항목을 필터링하는 필터 표현식을 정의합니다.

`QueryEnhancedRequest`는 쿼리에 대한 키 조건과 필터 표현식을 통합합니다.

```
    public static void query(DynamoDbTable movieActorTable) {

        // 1. Define a QueryConditional instance to return items matching a partition value.
        QueryConditional keyEqual = QueryConditional.keyEqualTo(b -> b.partitionValue("movie01"));
        // 1a. Define a QueryConditional that adds a sort key criteria to the partition value criteria.
        QueryConditional sortGreaterThanOrEqualTo = QueryConditional.sortGreaterThanOrEqualTo(b -> b.partitionValue("movie01").sortValue("actor2"));
        // 2. Define a filter expression that filters out items whose attribute value is null.
        final Expression filterOutNoActingschoolname = Expression.builder().expression("attribute_exists(actingschoolname)").build();

        // 3. Build the query request.
        QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder()
                .queryConditional(keyEqual)
                .filterExpression(filterOutNoActingschoolname)
                .build();
        // 4. Perform the query using the "keyEqual" conditional and filter expression.
        PageIterable<MovieActor> pagedResults = movieActorTable.query(tableQuery);
        logger.info("page count: {}", pagedResults.stream().count()); // Log  number of pages.

        pagedResults.items().stream()
                .sorted()
                .forEach(
                        item -> logger.info(item.toString()) // Log the sorted list of items.
                );
```

**Example - `keyEqual` 쿼리 조건부를 사용한 출력**  
다음은 메서드 실행 결과입니다. 출력에는 **movie01**의 `movieName` 값이 있는 항목이 표시되고 **`null`**과 같은 `actingSchoolName`이 있는 항목은 표시되지 않습니다.  

```
2023-03-05 13:11:05 [main] INFO  org.example.tests.QueryDemo:46 - page count: 1
2023-03-05 13:11:05 [main] INFO  org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'}
2023-03-05 13:11:05 [main] INFO  org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'}
2023-03-05 13:11:05 [main] INFO  org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}
```

#### 파티션 키 및 정렬 키로 항목 쿼리
<a name="sort-type-query-conditional-example"></a>

`sortGreaterThanOrEqualTo` `QueryConditional`은 **actor2**보다 크거나 같은 값에 정렬 키 조건을 추가하여 파티션 키 일치(**movie01**)를 구체화합니다.

`sort`로 시작하는[`QueryConditional` 메서드](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.html)는 정렬 키 값을 기반으로 비교를 통해 쿼리를 일치시키고 더 구체화해야 합니다. 메서드 이름의 `Sort`는 결과가 정렬되었음을 의미하지 않지만 비교 시 정렬 키 값이 사용됩니다.

다음 코드 조각에서는 주석 줄 3 이후, 앞서 표시된 쿼리 요청을 변경합니다. 이 코드 조각은 ‘keyEqual’ 쿼리 조건을 주석 줄 1a 이후 정의된 ‘sortGreaterThanOrEqualTo’ 쿼리 조건으로 대체합니다. 다음 코드는 또한 필터 표현식을 제거합니다.

```
        QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder()
                .queryConditional(sortGreaterThanOrEqualTo).build();
```

**Example - `sortGreaterThanOrEqualTo` 쿼리 조건부를 사용한 출력**  
다음 출력은 쿼리 결과를 표시합니다. 쿼리는 `movieName` 값이 **movie01과** 같은 항목을 반환하고 **actor2**보다 크거나 같은 `actorName` 값을 가진 항목만 반환합니다. 필터가 제거되었으므로 쿼리는 `actingSchoolName` 속성 값이 없는 항목을 반환합니다.  

```
2023-03-05 13:15:00 [main] INFO  org.example.tests.QueryDemo:46 - page count: 1
2023-03-05 13:15:00 [main] INFO  org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'}
2023-03-05 13:15:00 [main] INFO  org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'}
2023-03-05 13:15:00 [main] INFO  org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}
```

# 배치 작업 수행
<a name="ddb-en-client-use-multiop-batch"></a>

DynamoDB 향상된 클라이언트 API는 [`batchGetItem`()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#batchGetItem(java.util.function.Consumer)) 및 [`batchWriteItem`()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#batchWriteItem(java.util.function.Consumer))라는 두 가지 배치 메서드를 제공합니다.

## `batchGetItem()` 예
<a name="ddb-en-client-use-multiop-batch-get"></a>

이 [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#batchGetItem(java.util.function.Consumer)](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#batchGetItem(java.util.function.Consumer)) 메서드를 사용하면 전체 요청 한 번으로 여러 테이블에서 최대 100개의 개별 항목을 검색할 수 있습니다. 다음 예제에서는 이전에 표시된 [`Customer`](ddb-en-client-gs-tableschema.md#ddb-en-client-gs-tableschema-anno-bean-cust) 및 [`MovieActor`](ddb-en-client-use-multirecord.md#ddb-en-client-use-movieactor-class) 데이터 클래스를 사용합니다.

1행과 2행 뒤에 오는 예제에서는 `[ReadBatch](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/ReadBatch.html)` 객체를 빌드하고 나중에 이 객체를 `batchGetItem()` 메서드에 주석 줄 3 다음에 파라미터로 추가합니다.

주석 줄 1 뒤의 코드는 `Customer` 테이블에서 읽을 배치를 빌드합니다. 주석 줄 1a 뒤의 코드는 프라이머리 키 값과 정렬 키 값을 사용하여 읽을 항목을 지정하는 `[GetItemEnhancedRequest](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/GetItemEnhancedRequest.Builder.html)` 빌더를 사용하는 방법을 보여줍니다. 데이터 클래스에 복합 키가 있는 경우 파티션 키 값과 정렬 키 값을 모두 제공해야 합니다.

항목을 요청하기 위해 키 값을 지정하는 것과 달리 주석 줄 1b 다음에 표시된 대로 데이터 클래스를 사용하여 항목을 요청할 수 있습니다. SDK는 요청을 제출하기 전에 백그라운드에서 키 값을 추출합니다.

2a 이후의 두 명령문에서 볼 수 있듯이 키 기반 접근 방식을 사용하여 항목을 지정하는 경우 DynamoDB가 [매우 일관된 읽기](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html)를 수행하도록 지정할 수도 있습니다. `consistentRead()` 메서드를 사용하는 경우 동일한 테이블에 대해 요청된 모든 항목에 이 메서드를 사용해야 합니다.

DynamoDB에서 찾은 항목을 검색하려면 주석 4줄 뒤에 표시된 `[resultsForTable() ](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/BatchGetResultPage.html#resultsForTable(software.amazon.awssdk.enhanced.dynamodb.MappedTableResource))` 메서드를 사용하세요. 요청에서 읽은 각 테이블의 메서드를 호출합니다. `resultsForTable()`는 임의의 `java.util.List` 메서드를 사용하여 처리할 수 있는 검색된 항목 목록을 반환합니다. 이 예제는 각 항목을 기록합니다.

DynamoDB에서 처리하지 않은 항목을 검색하려면 주석 5줄 다음에 있는 접근 방식을 사용하세요. `BatchGetResultPage` 클래스에는 처리되지 않은 각 키에 액세스할 수 있는 `[unprocessedKeysForTable()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/BatchGetResultPage.html#unprocessedKeysForTable(software.amazon.awssdk.enhanced.dynamodb.MappedTableResource))` 메서드가 있습니다. [BatchGetItem API 참조](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html)에는 처리되지 않은 항목이 발생하는 상황에 대한 자세한 정보가 있습니다.

```
    public static void batchGetItemExample(DynamoDbEnhancedClient enhancedClient,
                                           DynamoDbTable<Customer> customerTable,
                                           DynamoDbTable<MovieActor> movieActorTable) {

        Customer customer2 = new Customer();
        customer2.setId("2");
        customer2.setEmail("cust2@example.org");

        // 1. Build a batch to read from the Customer table.
        ReadBatch customerBatch = ReadBatch.builder(Customer.class)
                .mappedTableResource(customerTable)
                // 1a. Specify the primary key value and sort key value for the item.
                .addGetItem(b -> b.key(k -> k.partitionValue("1").sortValue("cust1@orgname.org")))
                // 1b. Alternatively, supply a data class instances to provide the primary key values.
                .addGetItem(customer2)
                .build();

        // 2. Build a batch to read from the MovieActor table.
        ReadBatch moveActorBatch = ReadBatch.builder(MovieActor.class)
                .mappedTableResource(movieActorTable)
                // 2a. Call consistentRead(Boolean.TRUE) for each item for the same table.
                .addGetItem(b -> b.key(k -> k.partitionValue("movie01").sortValue("actor1")).consistentRead(Boolean.TRUE))
                .addGetItem(b -> b.key(k -> k.partitionValue("movie01").sortValue("actor4")).consistentRead(Boolean.TRUE))
                .build();

        // 3. Add ReadBatch objects to the request.
        BatchGetResultPageIterable resultPages = enhancedClient.batchGetItem(b -> b.readBatches(customerBatch, moveActorBatch));

        // 4. Retrieve the successfully requested items from each table.
        resultPages.resultsForTable(customerTable).forEach(item -> logger.info(item.toString()));
        resultPages.resultsForTable(movieActorTable).forEach(item -> logger.info(item.toString()));

        // 5. Retrieve the keys of the items requested but not processed by the service.
        resultPages.forEach((BatchGetResultPage pageResult) -> {
            pageResult.unprocessedKeysForTable(customerTable).forEach(key -> logger.info("Unprocessed item key: " + key.toString()));
            pageResult.unprocessedKeysForTable(movieActorTable).forEach(key -> logger.info("Unprocessed item key: " + key.toString()));
        });
    }
```

예제 코드를 실행하기 전에 두 테이블에 다음 항목이 있다고 가정해 보세요.

### 테이블 내 항목
<a name="ddb-en-client-use-multiop-batch-get-tableitems"></a>

```
Customer [id=1, name=CustName1, email=cust1@example.org, regDate=2023-03-31T15:46:27.688Z]
Customer [id=2, name=CustName2, email=cust2@example.org, regDate=2023-03-31T15:46:28.688Z]
Customer [id=3, name=CustName3, email=cust3@example.org, regDate=2023-03-31T15:46:29.688Z]
Customer [id=4, name=CustName4, email=cust4@example.org, regDate=2023-03-31T15:46:30.688Z]
Customer [id=5, name=CustName5, email=cust5@example.org, regDate=2023-03-31T15:46:31.689Z]
MovieActor{movieName='movie01', actorName='actor0', actingAward='actingaward0', actingYear=2001, actingSchoolName='null'}
MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'}
MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'}
MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'}
MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}
```

다음 출력은 주석 라인 4 이후에 반환되고 기록된 항목을 보여줍니다.

```
Customer [id=1, name=CustName1, email=cust1@example.org, regDate=2023-03-31T15:46:27.688Z]
Customer [id=2, name=CustName2, email=cust2@example.org, regDate=2023-03-31T15:46:28.688Z]
MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}
MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'}
```

## `batchWriteItem()` 예
<a name="ddb-en-client-use-multiop-batch-write"></a>

이 메서드는 하나 이상의 테이블에 여러 항목을 추가하거나 삭제합니다. 요청에 최대 25개의 개별 넣기 또는 삭제 작업을 지정할 수 있습니다. 다음 예제에서는 이전에 표시된 [`ProductCatalog`](ddb-en-client-use.md#ddb-en-client-use-compare-cs3) 및 [`MovieActor`](ddb-en-client-use-multirecord.md#ddb-en-client-use-movieactor-class) 데이터 클래스를 사용합니다.

`WriteBatch` 객체는 주석 라인 1과 2 다음에 빌드됩니다. `ProductCatalog` 테이블의 경우 코드는 항목 하나를 추가하고 항목 하나를 삭제합니다. 주석 줄 2 뒤에 있는 `MovieActor` 테이블의 경우 코드는 항목 두 개를 넣고 한 개를 삭제합니다.

`batchWriteItem` 메서드는 주석 줄 3 이후에 호출됩니다. `[builder](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/BatchWriteItemEnhancedRequest.Builder.html)` 파라미터는 각 테이블에 대한 일괄 요청을 제공합니다.

반환된 `[BatchWriteResult](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/BatchWriteResult.html)` 객체는 처리되지 않은 요청을 볼 수 있는 각 작업에 대해 별도의 메서드를 제공합니다. 주석 줄 4a 뒤의 코드는 처리되지 않은 삭제 요청에 대한 키를 제공하고 주석 줄 4b 뒤의 코드는 처리되지 않은 put 항목을 제공합니다.

```
    public static void batchWriteItemExample(DynamoDbEnhancedClient enhancedClient,
                                             DynamoDbTable<ProductCatalog> catalogTable,
                                             DynamoDbTable<MovieActor> movieActorTable) {

        // 1. Build a batch to write to the ProductCatalog table.
        WriteBatch products = WriteBatch.builder(ProductCatalog.class)
                .mappedTableResource(catalogTable)
                .addPutItem(b -> b.item(getProductCatItem1()))
                .addDeleteItem(b -> b.key(k -> k
                        .partitionValue(getProductCatItem2().id())
                        .sortValue(getProductCatItem2().title())))
                .build();

        // 2. Build a batch to write to the MovieActor table.
        WriteBatch movies = WriteBatch.builder(MovieActor.class)
                .mappedTableResource(movieActorTable)
                .addPutItem(getMovieActorYeoh())
                .addPutItem(getMovieActorBlanchettPartial())
                .addDeleteItem(b -> b.key(k -> k
                        .partitionValue(getMovieActorStreep().getMovieName())
                        .sortValue(getMovieActorStreep().getActorName())))
                .build();

        // 3. Add WriteBatch objects to the request.
        BatchWriteResult batchWriteResult = enhancedClient.batchWriteItem(b -> b.writeBatches(products, movies));
        // 4. Retrieve keys for items the service did not process.
        // 4a. 'unprocessedDeleteItemsForTable()' returns keys for delete requests that did not process.
        if (batchWriteResult.unprocessedDeleteItemsForTable(movieActorTable).size() > 0) {
            batchWriteResult.unprocessedDeleteItemsForTable(movieActorTable).forEach(key ->
                    logger.info(key.toString()));
        }
        // 4b. 'unprocessedPutItemsForTable()' returns keys for put requests that did not process.
        if (batchWriteResult.unprocessedPutItemsForTable(catalogTable).size() > 0) {
            batchWriteResult.unprocessedPutItemsForTable(catalogTable).forEach(key ->
                    logger.info(key.toString()));
        }
    }
```

다음 도우미 메서드는 업로드 및 삭제 작업을 위한 모델 객체를 제공합니다.

### 도우미 메서드
<a name="ddb-en-client-use-multiop-batch-write-helpers"></a>

```
 1.     public static ProductCatalog getProductCatItem1() {
 2.         return ProductCatalog.builder()
 3.                 .id(2)
 4.                 .isbn("1-565-85698")
 5.                 .authors(new HashSet<>(Arrays.asList("a", "b")))
 6.                 .price(BigDecimal.valueOf(30.22))
 7.                 .title("Title 55")
 8.                 .build();
 9.     }
10. 
11.     public static ProductCatalog getProductCatItem2() {
12.         return ProductCatalog.builder()
13.                 .id(4)
14.                 .price(BigDecimal.valueOf(40.00))
15.                 .title("Title 1")
16.                 .build();
17.     }  
18. 
19.     public static MovieActor getMovieActorBlanchettPartial() {
20.         MovieActor movieActor = new MovieActor();
21.         movieActor.setActorName("Cate Blanchett");
22.         movieActor.setMovieName("Blue Jasmine");
23.         movieActor.setActingYear(2023);
24.         movieActor.setActingAward("Best Actress");
25.         return movieActor;
26.     }
27. 
28.     public static MovieActor getMovieActorStreep() {
29.         MovieActor movieActor = new MovieActor();
30.         movieActor.setActorName("Meryl Streep");
31.         movieActor.setMovieName("Sophie's Choice");
32.         movieActor.setActingYear(1982);
33.         movieActor.setActingAward("Best Actress");
34.         movieActor.setActingSchoolName("Yale School of Drama");
35.         return movieActor;
36.     }
37. 
38.     public static MovieActor getMovieActorYeoh(){
39.         MovieActor movieActor = new MovieActor();
40.         movieActor.setActorName("Michelle Yeoh");
41.         movieActor.setMovieName("Everything Everywhere All at Once");
42.         movieActor.setActingYear(2023);
43.         movieActor.setActingAward("Best Actress");
44.         movieActor.setActingSchoolName("Royal Academy of Dance");
45.         return movieActor;
46.     }
```

예제 코드를 실행하기 전에 테이블에 다음 항목이 포함되어 있다고 가정해 보겠습니다.

```
MovieActor{movieName='Blue Jasmine', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2013, actingSchoolName='National Institute of Dramatic Art'}
MovieActor{movieName='Sophie's Choice', actorName='Meryl Streep', actingAward='Best Actress', actingYear=1982, actingSchoolName='Yale School of Drama'}
ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=10}
```

예제 코드가 끝나면 테이블에는 다음 항목이 포함됩니다.

```
MovieActor{movieName='Blue Jasmine', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2013, actingSchoolName='null'}
MovieActor{movieName='Everything Everywhere All at Once', actorName='Michelle Yeoh', actingAward='Best Actress', actingYear=2023, actingSchoolName='Royal Academy of Dance'}
ProductCatalog{id=2, title='Title 55', isbn='1-565-85698', authors=[a, b], price=30.22}
```

`MovieActor` 테이블에서 `Blue Jasmine` 영화 항목이 `getMovieActorBlanchettPartial()` 도우미 메서드를 통해 획득한 put 요청에 사용된 항목으로 대체되었음을 알 수 있습니다. 데이터 bean 속성 값이 제공되지 않은 경우 데이터베이스의 값이 제거됩니다. 이것이 `Blue Jasmine` 영화 항목의 결과 `actingSchoolName`가 null이 되는 이유입니다.

**참고**  
API 설명서에는 조건식을 사용할 수 있고 사용된 용량 및 컬렉션 지표를 개별 [PUT](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/PutItemEnhancedRequest.html) 및 [DELETE](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.html) 요청과 함께 반환할 수 있다고 나와 있지만 일괄 쓰기 시나리오에서는 그렇지 않습니다. 일괄 작업의 성능을 향상시키기 위해 이러한 개별 옵션은 무시됩니다.

# 트랜잭션 작업 수행
<a name="ddb-en-client-use-multiop-trans"></a>

DynamoDB 향상된 클라이언트 API는 `transactGetItems()` 및 `transactWriteItems()` 메서드를 제공합니다. Java용 SDK의 트랜잭션 방법은 DynamoDB 테이블에 ACID(원자성, 일관성, 격리 및 내구성)를 제공하여 애플리케이션에서 데이터 정확성을 유지하는 데 도움이 됩니다.

## `transactGetItems()` 예
<a name="ddb-en-client-use-multiop-trans-getitems"></a>

`[transactGetItems()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#transactGetItems(java.util.function.Consumer))` 메서드는 항목에 대한 개별 요청을 최대 100개까지 수락합니다. 단일 아토믹 트랜잭션으로 모든 항목을 읽습니다. *Amazon DynamoDB 개발자 안내서*에는 [`transactGetItems()` 메서드 실패를 유발하는 조건](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-apis-txgetitems)에 대한 정보와 `[transactGetItem()](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-isolation)` 호출 시 사용되는 격리 수준에 대한 정보가 있습니다.

다음 예제의 주석 줄 1 다음에, 코드는 `[builder](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/TransactGetItemsEnhancedRequest.Builder.html)` 매개변수를 사용하여 `transactGetItems()` 메서드를 호출합니다. SDK가 최종 요청을 생성하는 데 사용할 키 값이 포함된 데이터 객체를 사용하여 빌더 `[addGetItem()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/TransactGetItemsEnhancedRequest.Builder.html#addGetItem(software.amazon.awssdk.enhanced.dynamodb.MappedTableResource,T))`를 세 번 호출합니다.

요청은 주석 줄 2 다음에 `[Document](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/Document.html)` 객체 목록을 반환합니다. 반환되는 문서 목록에는 요청된 순서와 동일한 순서로 항목 데이터의 null이 아닌 [Document](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/Document.html) 인스턴스가 포함되어 있습니다. `[Document.getItem(MappedTableResource<T> mappedTableResource)](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/Document.html#getItem(software.amazon.awssdk.enhanced.dynamodb.MappedTableResource))` 메서드는 항목 데이터가 반환된 경우 형식화되지 않은 `Document` 객체를 형식이 지정된 Java 객체로 변환하고, 그렇지 않으면 null을 반환합니다.

```
    public static void transactGetItemsExample(DynamoDbEnhancedClient enhancedClient,
                                               DynamoDbTable<ProductCatalog> catalogTable,
                                               DynamoDbTable<MovieActor> movieActorTable) {

        // 1. Request three items from two tables using a builder.
        final List<Document> documents = enhancedClient.transactGetItems(b -> b
                .addGetItem(catalogTable, Key.builder().partitionValue(2).sortValue("Title 55").build())
                .addGetItem(movieActorTable, Key.builder().partitionValue("Sophie's Choice").sortValue("Meryl Streep").build())
                .addGetItem(movieActorTable, Key.builder().partitionValue("Blue Jasmine").sortValue("Cate Blanchett").build())
                .build());

        // 2. A list of Document objects is returned in the same order as requested.
        ProductCatalog title55 = documents.get(0).getItem(catalogTable);
        if (title55 != null) {
            logger.info(title55.toString());
        }

        MovieActor sophiesChoice = documents.get(1).getItem(movieActorTable);
        if (sophiesChoice != null) {
            logger.info(sophiesChoice.toString());
        }

        // 3. The getItem() method returns null if the Document object contains no item from DynamoDB.
        MovieActor blueJasmine = documents.get(2).getItem(movieActorTable);
        if (blueJasmine != null) {
            logger.info(blueJasmine.toString());
        }
    }
```

코드 예제가 실행되기 전에 DynamoDB 테이블에는 다음 항목이 포함되어 있습니다.

```
ProductCatalog{id=2, title='Title 55', isbn='orig_isbn', authors=[b, g], price=10}
MovieActor{movieName='Sophie's Choice', actorName='Meryl Streep', actingAward='Best Actress', actingYear=1982, actingSchoolName='Yale School of Drama'}
```

다음 출력이 반환됩니다. 항목을 요청했지만 찾을 수 없는 경우 이름이 `Blue Jasmine`로 지정된 영화에 대한 요청의 경우와 마찬가지로 항목이 반환되지 않습니다.

```
ProductCatalog{id=2, title='Title 55', isbn='orig_isbn', authors=[b, g], price=10}
MovieActor{movieName='Sophie's Choice', actorName='Meryl Streep', actingAward='Best Actress', actingYear=1982, actingSchoolName='Yale School of Drama'}
```

## `transactWriteItems()` 예제
<a name="ddb-en-client-use-multiop-trans-writeitems"></a>

`[transactWriteItems()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html#transactWriteItems(java.util.function.Consumer))`는 여러 테이블에 걸친 단일 아토믹 트랜잭션으로 최대 100개의 추가, 업데이트 또는 삭제 작업을 허용합니다. *Amazon DynamoDB 개발자 안내서*에는 [기본 DynamoDB 서비스 작업](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-apis-txwriteitems)의 제한 및 장애 조건에 대한 세부 정보가 포함되어 있습니다.

### 기본 예제
<a name="ddb-en-client-use-multiop-trans-writeitems-basic"></a>

다음 예제에서는 2개의 테이블에 대해 4개의 작업이 요청됩니다. 해당 모델 클래스 [`ProductCatalog`](ddb-en-client-use.md#ddb-en-client-use-compare-cs3) 및 [`MovieActor`](ddb-en-client-use-multirecord.md#ddb-en-client-use-movieactor-class)는 이전에 표시된 바 있습니다.

세 가지 가능한 작업(put, update, delete)은 각각 전용 요청 매개변수를 사용하여 세부 정보를 지정합니다.

주석 줄 1 뒤의 코드는 `addPutItem()` 메서드의 단순한 변형을 보여줍니다. 메서드는 입력할 `[MappedTableResource](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/MappedTableResource.html)` 개체와 데이터 객체 인스턴스를 받아들입니다. 주석 줄 2 뒤의 명령문은 `[TransactPutItemEnhancedRequest](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/TransactPutItemEnhancedRequest.html)` 인스턴스를 허용하는 변형을 보여줍니다. 이 변형을 사용하면 요청에 조건 표현식과 같은 더 많은 옵션을 추가할 수 있습니다. 다음 [예제](#ddb-en-client-use-multiop-trans-writeitems-opcondition)에서는 개별 작업에 대한 조건식을 보여줍니다.

주석 줄 3 다음에 업데이트 작업이 요청됩니다. `[TransactUpdateItemEnhancedRequest](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/TransactUpdateItemEnhancedRequest.Builder.html)`에는 SDK가 모델 객체의 `null` 값으로 수행하는 작업을 구성할 수 있는 `ignoreNulls()` 메서드가 있습니다. `ignoreNulls()` 메서드가 true를 반환하는 경우 SDK는 `null`인 데이터 객체 속성에 대한 테이블의 속성 값을 제거하지 않습니다. `ignoreNulls()` 메서드가 false를 반환하면 SDK는 DynamoDB 서비스에 테이블의 항목에서 속성을 제거하도록 요청합니다. `ignoreNulls`의 기본값은 false입니다.

주석 줄 4 다음의 명령문은 데이터 객체를 취하는 삭제 요청의 변형을 보여줍니다. 향상된 클라이언트는 최종 요청을 발송하기 전에 키 값을 추출합니다.

```
    public static void transactWriteItems(DynamoDbEnhancedClient enhancedClient,
                                          DynamoDbTable<ProductCatalog> catalogTable,
                                          DynamoDbTable<MovieActor> movieActorTable) {

        enhancedClient.transactWriteItems(b -> b
                // 1. Simplest variation of put item request.
                .addPutItem(catalogTable, getProductCatId2())
                // 2. Put item request variation that accommodates condition expressions.
                .addPutItem(movieActorTable, TransactPutItemEnhancedRequest.builder(MovieActor.class)
                        .item(getMovieActorStreep())
                        .conditionExpression(Expression.builder().expression("attribute_not_exists (movie)").build())
                        .build())
                // 3. Update request that does not remove attribute values on the table if the data object's value is null.
                .addUpdateItem(catalogTable, TransactUpdateItemEnhancedRequest.builder(ProductCatalog.class)
                        .item(getProductCatId4ForUpdate())
                        .ignoreNulls(Boolean.TRUE)
                        .build())
                // 4. Variation of delete request that accepts a data object. The key values are extracted for the request.
                .addDeleteItem(movieActorTable, getMovieActorBlanchett())
        );
    }
```

다음 도우미 메서드는 `add*Item` 매개 변수에 대한 데이터 개체를 제공합니다.

#### 도우미 메서드
<a name="ddb-en-client-use-multiop-trans-writeitems-basic-helpers"></a>

```
    public static ProductCatalog getProductCatId2() {
        return ProductCatalog.builder()
                .id(2)
                .isbn("1-565-85698")
                .authors(new HashSet<>(Arrays.asList("a", "b")))
                .price(BigDecimal.valueOf(30.22))
                .title("Title 55")
                .build();
    }

    public static ProductCatalog getProductCatId4ForUpdate() {
        return ProductCatalog.builder()
                .id(4)
                .price(BigDecimal.valueOf(40.00))
                .title("Title 1")
                .build();
    }

    public static MovieActor getMovieActorBlanchett() {
        MovieActor movieActor = new MovieActor();
        movieActor.setActorName("Cate Blanchett");
        movieActor.setMovieName("Tar");
        movieActor.setActingYear(2022);
        movieActor.setActingAward("Best Actress");
        movieActor.setActingSchoolName("National Institute of Dramatic Art");
        return movieActor;
    }

    public static MovieActor getMovieActorStreep() {
        MovieActor movieActor = new MovieActor();
        movieActor.setActorName("Meryl Streep");
        movieActor.setMovieName("Sophie's Choice");
        movieActor.setActingYear(1982);
        movieActor.setActingAward("Best Actress");
        movieActor.setActingSchoolName("Yale School of Drama");
        return movieActor;
    }
```

코드 예제가 실행되기 전에 DynamoDB 테이블에는 다음 항목이 포함되어 있습니다.

```
1 | ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=10}
2 | MovieActor{movieName='Tar', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2022, actingSchoolName='National Institute of Dramatic Art'}
```

코드 실행이 완료된 후 테이블에 다음 항목이 표시됩니다.

```
3 | ProductCatalog{id=2, title='Title 55', isbn='1-565-85698', authors=[a, b], price=30.22}
4 | ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=40.0}
5 | MovieActor{movieName='Sophie's Choice', actorName='Meryl Streep', actingAward='Best Actress', actingYear=1982, actingSchoolName='Yale School of Drama'}
```

2행 항목은 삭제되었으며, 3행과 5행에는 추가된 항목이 표시됩니다. 4행에는 라인 1의 업데이트 내용이 표시됩니다. `price` 값은 항목에서 변경된 유일한 값입니다. `ignoreNulls()`가 false를 반환했다면 4행은 다음 줄처럼 보일 것입니다.

```
ProductCatalog{id=4, title='Title 1', isbn='null', authors=null, price=40.0}
```

### 조건 검사 예제
<a name="ddb-en-client-use-multiop-trans-writeitems-checkcond"></a>

다음 예에서는 조건 검사의 사용을 보여줍니다. 조건 검사는 항목이 있는지 확인하거나 데이터베이스의 항목의 특정 속성에 대한 조건을 검사하는 데 사용됩니다. 조건 확인에서 확인한 항목은 해당 거래의 다른 작업에 사용할 수 없습니다.

**참고**  
동일한 트랜잭션 내에서 동일한 항목을 여러 작업의 대상으로 지정할 수 없습니다. 예를 들어 동일한 트랜잭션에서 조건 확인과 동일한 항목 업데이트를 동시에 수행할 수 없습니다.

이 예에서는 트랜잭션 쓰기 항목 요청의 각 작업 유형 중 하나를 보여줍니다. `addConditionCheck()` 메서드는 주석 줄 2 다음에 `conditionExpression` 매개 변수가 `false`로 평가될 경우 트랜잭션을 실패시키는 조건을 제공합니다. Helper 메서드 블록에 표시된 메서드에서 반환되는 조건 표현식은 영화 `Sophie's Choice`의 수상 연도가 `1982`과 같지 않은지 확인합니다. 값이 맞으면 표현식이 `false`까지 평가되고 트랜잭션이 실패합니다.

이 가이드에서는 다른 항목에서 [표현식](ddb-en-client-expressions.md)에 대해 자세히 설명합니다.

```
    public static void conditionCheckFailExample(DynamoDbEnhancedClient enhancedClient,
                                                 DynamoDbTable<ProductCatalog> catalogTable,
                                                 DynamoDbTable<MovieActor> movieActorTable) {

        try {
            enhancedClient.transactWriteItems(b -> b
                    // 1. Perform one of each type of operation with the next three methods.
                    .addPutItem(catalogTable, TransactPutItemEnhancedRequest.builder(ProductCatalog.class)
                            .item(getProductCatId2()).build())
                    .addUpdateItem(catalogTable, TransactUpdateItemEnhancedRequest.builder(ProductCatalog.class)
                            .item(getProductCatId4ForUpdate())
                            .ignoreNulls(Boolean.TRUE).build())
                    .addDeleteItem(movieActorTable, TransactDeleteItemEnhancedRequest.builder()
                            .key(b1 -> b1
                                    .partitionValue(getMovieActorBlanchett().getMovieName())
                                    .sortValue(getMovieActorBlanchett().getActorName())).build())
                    // 2. Add a condition check on a table item that is not involved in another operation in this request.
                    .addConditionCheck(movieActorTable, ConditionCheck.builder()
                            .conditionExpression(buildConditionCheckExpression())
                            .key(k -> k
                                    .partitionValue("Sophie's Choice")
                                    .sortValue("Meryl Streep"))
                            // 3. Specify the request to return existing values from the item if the condition evaluates to true.
                            .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
                            .build())
                    .build());
        // 4. Catch the exception if the transaction fails and log the information.
        } catch (TransactionCanceledException ex) {
            ex.cancellationReasons().stream().forEach(cancellationReason -> {
                logger.info(cancellationReason.toString());
            });
        }
    }
```

이전 코드 예제에서는 다음과 같은 도우미 메서드를 사용했습니다.

#### 도우미 메서드
<a name="ddb-en-client-use-multiop-trans-writeitems-checkcond-helpers"></a>

```
    private static Expression buildConditionCheckExpression() {
        Map<String, AttributeValue> expressionValue = Map.of(
                ":year", numberValue(1982));

        return Expression.builder()
                .expression("actingyear <> :year")
                .expressionValues(expressionValue)
                .build();
    }

    public static ProductCatalog getProductCatId2() {
        return ProductCatalog.builder()
                .id(2)
                .isbn("1-565-85698")
                .authors(new HashSet<>(Arrays.asList("a", "b")))
                .price(BigDecimal.valueOf(30.22))
                .title("Title 55")
                .build();
    }

    public static ProductCatalog getProductCatId4ForUpdate() {
        return ProductCatalog.builder()
                .id(4)
                .price(BigDecimal.valueOf(40.00))
                .title("Title 1")
                .build();
    }

    public static MovieActor getMovieActorBlanchett() {
        MovieActor movieActor = new MovieActor();
        movieActor.setActorName("Cate Blanchett");
        movieActor.setMovieName("Blue Jasmine");
        movieActor.setActingYear(2013);
        movieActor.setActingAward("Best Actress");
        movieActor.setActingSchoolName("National Institute of Dramatic Art");
        return movieActor;
    }
```

코드 예제가 실행되기 전에 DynamoDB 테이블에는 다음 항목이 포함되어 있습니다.

```
1 | ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=10}
2 | MovieActor{movieName='Sophie's Choice', actorName='Meryl Streep', actingAward='Best Actress', actingYear=1982, actingSchoolName='Yale School of Drama'}
3 | MovieActor{movieName='Tar', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2022, actingSchoolName='National Institute of Dramatic Art'}
```

코드 실행이 완료된 후 테이블에 다음 항목이 표시됩니다.

```
ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=10}
MovieActor{movieName='Sophie's Choice', actorName='Meryl Streep', actingAward='Best Actress', actingYear=1982, actingSchoolName='Yale School of Drama'}
MovieActor{movieName='Tar', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2022, actingSchoolName='National Institute of Dramatic Art'}
```

트랜잭션이 실패했기 때문에 테이블의 항목은 변경되지 않은 상태로 유지됩니다. 동영상 `Sophie's Choice`의 `actingYear` 값은 `1982`으로, `transactWriteItem()` 메서드가 호출되기 전 테이블 항목의 2행에 표시된 것과 같습니다.

트랜잭션에 대한 취소 정보를 캡처하려면 `transactWriteItems()` 메서드 호출을 `try` 블록으로 묶고 [https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/model/TransactionCanceledException.html](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/model/TransactionCanceledException.html)를 `catch`합니다. 예제의 주석 줄 4 다음에 코드가 각 `[CancellationReason](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/dynamodb/model/CancellationReason.html)` 객체를 기록합니다. 예제의 주석 줄 3 뒤에 오는 코드에서는 트랜잭션 실패를 초래한 항목에 대해 값을 반환하도록 지정하기 때문에 로그에는 `Sophie's Choice` 영화 항목에 대한 원시 데이터베이스 값이 표시됩니다.

```
CancellationReason(Code=None)
CancellationReason(Code=None)
CancellationReason(Code=None)
CancellationReason(Item={actor=AttributeValue(S=Meryl Streep), movie=AttributeValue(S=Sophie's Choice), actingaward=AttributeValue(S=Best Actress), actingyear=AttributeValue(N=1982), actingschoolname=AttributeValue(S=Yale School of Drama)}, ¬
    Code=ConditionalCheckFailed, Message=The conditional request failed.)
```

### 단일 작업 조건 예제
<a name="ddb-en-client-use-multiop-trans-writeitems-opcondition"></a>

다음 예제는 트랜잭션 요청의 단일 작업에 대한 조건 사용을 보여줍니다. 주석 라인 1 이후의 삭제 작업에는 데이터베이스에 대해 작업의 대상 항목 값을 확인하는 조건이 포함되어 있습니다. 이 예제에서 주석 행 2 다음에 도우미 메서드를 사용하여 생성된 조건식은 영화의 개봉 연도가 2013년이 아닌 경우 데이터베이스에서 항목을 삭제해야 함을 지정합니다.

[표현식](ddb-en-client-expressions.md)은 이 가이드의 뒷부분에서 설명합니다.

```
    public static void singleOperationConditionFailExample(DynamoDbEnhancedClient enhancedClient,
                                                           DynamoDbTable<ProductCatalog> catalogTable,
                                                           DynamoDbTable<MovieActor> movieActorTable) {
        try {
            enhancedClient.transactWriteItems(b -> b
                    .addPutItem(catalogTable, TransactPutItemEnhancedRequest.builder(ProductCatalog.class)
                            .item(getProductCatId2())
                            .build())
                    .addUpdateItem(catalogTable, TransactUpdateItemEnhancedRequest.builder(ProductCatalog.class)
                            .item(getProductCatId4ForUpdate())
                            .ignoreNulls(Boolean.TRUE).build())
                    // 1. Delete operation that contains a condition expression
                    .addDeleteItem(movieActorTable, TransactDeleteItemEnhancedRequest.builder()
                            .key((Key.Builder k) -> {
                                MovieActor blanchett = getMovieActorBlanchett();
                                k.partitionValue(blanchett.getMovieName())
                                        .sortValue(blanchett.getActorName());
                            })
                            .conditionExpression(buildDeleteItemExpression())
                            .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
                            .build())
                    .build());
        } catch (TransactionCanceledException ex) {
            ex.cancellationReasons().forEach(cancellationReason -> logger.info(cancellationReason.toString()));
        }
    }

    // 2. Provide condition expression to check if 'actingyear' is not equal to 2013.
    private static Expression buildDeleteItemExpression() {
        Map<String, AttributeValue> expressionValue = Map.of(
                ":year", numberValue(2013));

        return Expression.builder()
                .expression("actingyear <> :year")
                .expressionValues(expressionValue)
                .build();
    }
```

이전 코드 예제에서는 다음과 같은 도우미 메서드를 사용했습니다.

#### 도우미 메서드
<a name="ddb-en-client-use-multiop-trans-writeitems-opcondition-helpers"></a>

```
    public static ProductCatalog getProductCatId2() {
        return ProductCatalog.builder()
                .id(2)
                .isbn("1-565-85698")
                .authors(new HashSet<>(Arrays.asList("a", "b")))
                .price(BigDecimal.valueOf(30.22))
                .title("Title 55")
                .build();
    }

    public static ProductCatalog getProductCatId4ForUpdate() {
        return ProductCatalog.builder()
                .id(4)
                .price(BigDecimal.valueOf(40.00))
                .title("Title 1")
                .build();
    }
    public static MovieActor getMovieActorBlanchett() {
        MovieActor movieActor = new MovieActor();
        movieActor.setActorName("Cate Blanchett");
        movieActor.setMovieName("Blue Jasmine");
        movieActor.setActingYear(2013);
        movieActor.setActingAward("Best Actress");
        movieActor.setActingSchoolName("National Institute of Dramatic Art");
        return movieActor;
    }
```

코드 예제가 실행되기 전에 DynamoDB 테이블에는 다음 항목이 포함되어 있습니다.

```
1 | ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=10}
2 | MovieActor{movieName='Blue Jasmine', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2013, actingSchoolName='National Institute of Dramatic Art'}
```

코드 실행이 완료된 후 테이블에 다음 항목이 표시됩니다.

```
ProductCatalog{id=4, title='Title 1', isbn='orig_isbn', authors=[b, g], price=10}
2023-03-15 11:29:07 [main] INFO  org.example.tests.TransactDemoTest:168 - MovieActor{movieName='Blue Jasmine', actorName='Cate Blanchett', actingAward='Best Actress', actingYear=2013, actingSchoolName='National Institute of Dramatic Art'}
```

트랜잭션이 실패했기 때문에 테이블의 항목은 변경되지 않은 상태로 유지됩니다. 영화 `Blue Jasmine`의 `actingYear` 값은 코드 예제가 실행되기 전 항목 목록의 2행에 표시된 것과 같은 `2013`입니다.

다음 줄이 콘솔에 기록됩니다.

```
CancellationReason(Code=None)
CancellationReason(Code=None)
CancellationReason(Item={actor=AttributeValue(S=Cate Blanchett), movie=AttributeValue(S=Blue Jasmine), actingaward=AttributeValue(S=Best Actress), actingyear=AttributeValue(N=2013), actingschoolname=AttributeValue(S=National Institute of Dramatic Art)}, 
    Code=ConditionalCheckFailed, Message=The conditional request failed)
```

# 보조 인덱스 사용
<a name="ddb-en-client-use-secindex"></a>

보조 인덱스는 쿼리 및 스캔 작업에 사용하는 대체 키를 정의하여 데이터 액세스를 향상시킵니다. GSI(글로벌 보조 인덱스)에는 기본 테이블의 파티션 키와 정렬 키가 다를 수 있습니다. 반대로 LSI(로컬 보조 인덱스)는 기본 인덱스의 파티션 키를 사용합니다.

## 보조 인덱스 주석으로 데이터 클래스에 주석 달기
<a name="ddb-en-client-use-secindex-annomodel"></a>

보조 인덱스에 속하는 속성에는 `@DynamoDbSecondaryPartitionKey` 또는 `@DynamoDbSecondarySortKey` 주석이 필요합니다.

다음 클래스는 두 인덱스에 대한 주석을 보여줍니다. *SubjectLastPostedDateIndex*라는 GSI는 파티션 키에 `Subject` 속성을 사용하고 정렬 키에는 `LastPostedDateTime` 속성을 사용합니다. *ForumLastPostedDateIndex라는* 이름의 LSI는 `ForumName`를 파티션 키로, `LastPostedDateTime`를 정렬 키로 사용합니다.

`Subject` 속성은 이중 역할을 한다는 점에 유의하세요. 기본 키의 정렬 키이자 *SubjectLastPostedDateIndex*라는 GSI의 파티션 키입니다.

### `MessageThread` 클래스
<a name="ddb-en-client-use-secindex-class"></a>

이 `MessageThread` 클래스는 *Amazon DynamoDB 개발자 안내서*의 [예제 스레드 테이블](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AppendixSampleTables.html)의 데이터 클래스로 사용하기에 적합합니다.

#### 가져오기
<a name="ddb-en-client-use-secindex-classimports"></a>

```
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

import java.util.List;
```

```
@DynamoDbBean
public class MessageThread {
    private String ForumName;
    private String Subject;
    private String Message;
    private String LastPostedBy;
    private String LastPostedDateTime;
    private Integer Views;
    private Integer Replies;
    private Integer Answered;
    private List<String> Tags;

    @DynamoDbPartitionKey
    public String getForumName() {
        return ForumName;
    }

    public void setForumName(String forumName) {
        ForumName = forumName;
    }

    // Sort key for primary index and partition key for GSI "SubjectLastPostedDateIndex".
    @DynamoDbSortKey
    @DynamoDbSecondaryPartitionKey(indexNames = "SubjectLastPostedDateIndex")
    public String getSubject() {
        return Subject;
    }

    public void setSubject(String subject) {
        Subject = subject;
    }

    // Sort key for GSI "SubjectLastPostedDateIndex" and sort key for LSI "ForumLastPostedDateIndex".
    @DynamoDbSecondarySortKey(indexNames = {"SubjectLastPostedDateIndex", "ForumLastPostedDateIndex"})
    public String getLastPostedDateTime() {
        return LastPostedDateTime;
    }

    public void setLastPostedDateTime(String lastPostedDateTime) {
        LastPostedDateTime = lastPostedDateTime;
    }
    public String getMessage() {
        return Message;
    }

    public void setMessage(String message) {
        Message = message;
    }

    public String getLastPostedBy() {
        return LastPostedBy;
    }

    public void setLastPostedBy(String lastPostedBy) {
        LastPostedBy = lastPostedBy;
    }

    public Integer getViews() {
        return Views;
    }

    public void setViews(Integer views) {
        Views = views;
    }

    @DynamoDbSecondaryPartitionKey(indexNames = "ForumRepliesIndex")
    public Integer getReplies() {
        return Replies;
    }

    public void setReplies(Integer replies) {
        Replies = replies;
    }

    public Integer getAnswered() {
        return Answered;
    }

    public void setAnswered(Integer answered) {
        Answered = answered;
    }

    public List<String> getTags() {
        return Tags;
    }

    public void setTags(List<String> tags) {
        Tags = tags;
    }

    public MessageThread() {
        this.Answered = 0;
        this.LastPostedBy = "";
        this.ForumName = "";
        this.Message = "";
        this.LastPostedDateTime = "";
        this.Replies = 0;
        this.Views = 0;
        this.Subject = "";
    }

    @Override
    public String toString() {
        return "MessageThread{" +
                "ForumName='" + ForumName + '\'' +
                ", Subject='" + Subject + '\'' +
                ", Message='" + Message + '\'' +
                ", LastPostedBy='" + LastPostedBy + '\'' +
                ", LastPostedDateTime='" + LastPostedDateTime + '\'' +
                ", Views=" + Views +
                ", Replies=" + Replies +
                ", Answered=" + Answered +
                ", Tags=" + Tags +
                '}';
    }
}
```

## 인덱스 만들기
<a name="ddb-en-client-use-secindex-confindex"></a>

Java용 SDK 버전 2.20.86부터 이 `createTable()` 메서드는 데이터 클래스 주석에서 보조 인덱스를 자동으로 생성합니다. 기본적으로 기본 테이블의 모든 속성이 인덱스에 복사되며 프로비저닝된 처리량 값은 20 읽기 용량 단위 및 20 쓰기 용량 단위입니다.

단, SDK 2.20.86 이전 버전을 사용하는 경우에는 다음 예와 같이 테이블과 함께 인덱스를 빌드해야 합니다. 이 예제에서는 `Thread` 테이블의 인덱스 두 개를 빌드합니다. [빌더](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/CreateTableEnhancedRequest.Builder.html) 매개 변수에는 주석 줄 1과 2 다음에 표시된 것처럼 두 가지 유형의 인덱스를 모두 구성하는 메서드가 있습니다. 인덱스 빌더의 `indexName()` 메서드를 사용하여 데이터 클래스 주석에 지정된 인덱스 이름을 의도한 인덱스 유형과 연결할 수 있습니다.

이 코드는 모든 테이블 속성이 주석 라인 3과 4 다음에 있는 두 인덱스 모두에 포함되도록 구성합니다. [속성 프로젝션](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.Projections)에 대한 자세한 내용은 *Amazon DynamoDB 개발자 안내서*에서 확인할 수 있습니다.

```
    public static void createMessageThreadTable(DynamoDbTable<MessageThread> messageThreadDynamoDbTable, DynamoDbClient dynamoDbClient) {
        messageThreadDynamoDbTable.createTable(b -> b
                // 1. Generate the GSI.
                .globalSecondaryIndices(gsi -> gsi.indexName("SubjectLastPostedDateIndex")
                        // 3. Populate the GSI with all attributes.
                        .projection(p -> p
                                .projectionType(ProjectionType.ALL))
                )
                // 2. Generate the LSI.
                .localSecondaryIndices(lsi -> lsi.indexName("ForumLastPostedDateIndex")
                        // 4. Populate the LSI with all attributes.
                        .projection(p -> p
                                .projectionType(ProjectionType.ALL))
                )
        );
```

## 인덱스를 사용하여 쿼리
<a name="ddb-en-client-use-secindex-query"></a>

다음 예시에서는 로컬 보조 인덱스 *ForumLastPostedDateIndex*를 쿼리합니다.

주석 줄 2에 이어 [DynamoDbIndex.query()](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbIndex.html#query(java.util.function.Consumer)) 메서드를 호출할 때 필요한 [QueryConditional](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.html) 객체를 생성합니다.

인덱스 이름을 전달하여 주석 줄 3 다음에 쿼리하려는 인덱스에 대한 참조를 얻습니다. 주석 줄 4에 이어 `QueryConditional` 객체를 전달하는 인덱스에서 `query()` 메서드를 호출합니다.

또한 주석 줄 5 다음에 표시된 대로 세 가지 속성 값을 반환하도록 쿼리를 구성합니다. `attributesToProject()`가 호출되지 않은 경우 쿼리는 모든 속성 값을 반환합니다. 지정된 속성 이름은 소문자로 시작하는 것을 알 수 있습니다. 이러한 속성 이름은 테이블에 사용된 이름과 일치하지만 반드시 데이터 클래스의 속성 이름과 일치할 필요는 없습니다.

주석 줄 6 다음에 결과를 반복하고 쿼리에서 반환된 각 항목을 기록하고 목록에 저장하여 호출자에게 반환합니다.

```
public class IndexScanExamples {
    private static Logger logger = LoggerFactory.getLogger(IndexScanExamples.class);

    public static List<MessageThread> queryUsingSecondaryIndices(String lastPostedDate,
                                                                 DynamoDbTable<MessageThread> threadTable) {
        // 1. Log the parameter value.
        logger.info("lastPostedDate value: {}", lastPostedDate);

        // 2. Create a QueryConditional whose sort key value must be greater than or equal to the parameter value.
        QueryConditional queryConditional = QueryConditional.sortGreaterThanOrEqualTo(qc ->
                qc.partitionValue("Forum02").sortValue(lastPostedDate));

        // 3. Specify the index name to query.
        final DynamoDbIndex<MessageThread> forumLastPostedDateIndex = threadTable.index("ForumLastPostedDateIndex");

        // 4. Perform the query using the QueryConditional object.
        final SdkIterable<Page<MessageThread>> pagedResult = forumLastPostedDateIndex.query(q -> q
                .queryConditional(queryConditional)
                // 5. Request three attribute in the results.
                .attributesToProject("forumName", "subject", "lastPostedDateTime"));

        List<MessageThread> collectedItems = new ArrayList<>();
        // 6. Iterate through pages response and sort the items.
        pagedResult.stream().forEach(page -> page.items().stream()
                .sorted(Comparator.comparing(MessageThread::getLastPostedDateTime))
                .forEach(mt -> {
                    // 7. Log the returned items and add the collection to return to the caller.
                    logger.info(mt.toString());
                    collectedItems.add(mt);
                }));
        return collectedItems;
    }
```

쿼리가 실행되기 전의 데이터베이스에는 다음과 같은 항목이 있습니다.

```
MessageThread{ForumName='Forum01', Subject='Subject01', Message='Message01', LastPostedBy='', LastPostedDateTime='2023.03.28', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum02', Subject='Subject02', Message='Message02', LastPostedBy='', LastPostedDateTime='2023.03.29', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum02', Subject='Subject04', Message='Message04', LastPostedBy='', LastPostedDateTime='2023.03.31', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum02', Subject='Subject08', Message='Message08', LastPostedBy='', LastPostedDateTime='2023.04.04', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum02', Subject='Subject10', Message='Message10', LastPostedBy='', LastPostedDateTime='2023.04.06', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum03', Subject='Subject03', Message='Message03', LastPostedBy='', LastPostedDateTime='2023.03.30', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum03', Subject='Subject06', Message='Message06', LastPostedBy='', LastPostedDateTime='2023.04.02', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum03', Subject='Subject09', Message='Message09', LastPostedBy='', LastPostedDateTime='2023.04.05', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum05', Subject='Subject05', Message='Message05', LastPostedBy='', LastPostedDateTime='2023.04.01', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum07', Subject='Subject07', Message='Message07', LastPostedBy='', LastPostedDateTime='2023.04.03', Views=0, Replies=0, Answered=0, Tags=null}
```

1행과 6행의 로깅 명령문을 실행하면 다음과 같은 콘솔 출력이 나타납니다.

```
lastPostedDate value: 2023.03.31
MessageThread{ForumName='Forum02', Subject='Subject04', Message='', LastPostedBy='', LastPostedDateTime='2023.03.31', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum02', Subject='Subject08', Message='', LastPostedBy='', LastPostedDateTime='2023.04.04', Views=0, Replies=0, Answered=0, Tags=null}
MessageThread{ForumName='Forum02', Subject='Subject10', Message='', LastPostedBy='', LastPostedDateTime='2023.04.06', Views=0, Replies=0, Answered=0, Tags=null}
```

쿼리가 *Forum02*의 `forumName` 값과 *2023.03.31*보다 크거나 같은 `lastPostedDateTime`을 반환했습니다. 인덱스에 `message` 속성 값이 있더라도 결과에는 빈 문자열이 있는 `message` 값이 표시됩니다. 이는 주석 줄 5 이후에 코드로 메시지 속성이 프로젝션되지 않았기 때문입니다.