

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

# Amazon QLDB 드라이버 시작하기
<a name="getting-started-driver"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 장에는 QLDB 드라이버를 사용하여 Amazon QLDB로 개발하는 방법을 배우는 데 도움이 되는 실습용 자습서가 포함되어 있습니다. 드라이버는 [QLDB API](api-reference.md)와의 상호 작용을 지원하는 AWS SDK를 기반으로 구축됩니다.

**QLDB 세션 추상화**

드라이버는 트랜잭션 데이터 API(*QLDB 세션*) 위에 높은 수준의 추상화 계층을 제공합니다. [SendCommand](https://docs.aws.amazon.com/qldb/latest/developerguide/API_QLDB-Session_SendCommand.html) API 호출을 관리하여 원장 데이터에서 [PartiQL](ql-reference.md) 문을 실행하는 프로세스를 간소화합니다. 이러한 API 호출에는 세션 관리, 트랜잭션, 오류 발생 시 재시도 정책 등 드라이버가 대신 처리하는 여러 파라미터가 필요합니다. 또한 드라이버에는 성능 최적화 기능이 있으며 QLDB와의 상호 작용을 위한 모범 사례를 적용합니다.

**참고**  
[Amazon QLDB API 참조에 나열된 리소스 관리 API](https://docs.aws.amazon.com/qldb/latest/developerguide/API_Operations_Amazon_QLDB.html) 작업과 상호 작용하려면 드라이버 대신 AWS SDK를 직접 사용합니다. 관리 API는 원장 리소스 관리와 내보내기, 스트리밍, 데이터 확인과 같은 비트랜잭션 데이터 작업에만 사용합니다.

**Amazon Ion 지원**

또한 드라이버는 [Amazon Ion](ion.md) 라이브러리를 사용하여 트랜잭션 실행 시 Ion 데이터 처리를 지원합니다. 또한 이러한 라이브러리는 Ion 값의 해시 계산을 담당합니다. QLDB는 데이터 트랜잭션 요청의 무결성을 확인하기 위해 이러한 Ion 해시를 필요로 합니다.

**드라이버 용어**

이 도구는 개발자에게 친숙한 인터페이스를 제공하는 다른 데이터베이스 드라이버와 비슷하기 때문에 *드라이버*라고 불립니다. 이러한 드라이버는 표준 명령 및 기능 세트를 서비스의 하위 수준 API에 필요한 특정 호출로 변환하는 로직을 유사하게 캡슐화합니다.

드라이버는 GitHub의 오픈 소스이며 다음 프로그래밍 언어에서 사용할 수 있습니다.
+ [Java 드라이버](getting-started.java.md)
+ [.NET 드라이버](getting-started.dotnet.md)
+ [Go 드라이버](getting-started.golang.md)
+ [Node.js 드라이버](getting-started.nodejs.md)
+ [Python 드라이버](getting-started.python.md)

지원되는 모든 프로그래밍 언어에 대한 일반 드라이버 정보와 추가 자습서는 다음 항목을 참조하세요.
+ [드라이버를 사용한 세션 관리](driver-session-management.md)
+ [드라이버 권장 사항](driver.best-practices.md)
+ [드라이버 재시도 정책](driver-retry-policy.md)
+ [일반적인 오류](driver-errors.md)
+ [샘플 애플리케이션 자습서](getting-started-sample-app.md)
+ [Amazon Ion 작업](driver-working-with-ion.md)
+ [PartiQL 문 통계 가져오기](working.statement-stats.md)

# Java용 Amazon QLDB 드라이버
<a name="getting-started.java"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

원장의 데이터를 사용하려면 AWS 제공된 드라이버를 사용하여 Java 애플리케이션에서 Amazon QLDB에 연결할 수 있습니다. 다음 주제에서는 Java용 QLDB 드라이버를 시작하는 방법에 대해 설명합니다.

**Topics**
+ [

## 드라이버 리소스
](#getting-started.java.resources)
+ [

## 사전 조건
](#getting-started.java.prereqs)
+ [

## 기본 AWS 보안 인증 정보 및 리전 설정
](#getting-started.java.credentials)
+ [

## 설치
](#getting-started.java.install)
+ [빠른 시작 자습서](driver-quickstart-java.md)
+ [Cookbook 참조](driver-cookbook-java.md)

## 드라이버 리소스
<a name="getting-started.java.resources"></a>

Java 드라이버가 지원하는 기능에 대한 자세한 정보는 다음 리소스를 참조하세요.
+ API 참조: [2.x](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/), [1.x](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/1.1.0/index.html)
+ [드라이버 소스 코드(GitHub)](https://github.com/awslabs/amazon-qldb-driver-java)
+ [샘플 애플리케이션 소스 코드(GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-java)
+ [원장 로더 프레임워크(GitHub)](https://github.com/awslabs/amazon-qldb-ledger-load-java)
+ [Amazon Ion 코드 예제](ion.code-examples.md)

## 사전 조건
<a name="getting-started.java.prereqs"></a>

Java용 QLDB 드라이버를 시작하기 전에 다음을 수행해야 합니다.

1. 의 AWS 설정 지침을 따릅니다[Amazon QLDB 액세스](accessing.md). 다음 내용이 포함됩니다:

   1. 가입합니다 AWS.

   1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

   1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

1. 다음을 다운로드하고 설치하여 Java 개발 환경을 설정합니다.

   1. Java SE 개발 키트 8(예: [Amazon Corretto 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html))

   1. (선택 사항) [Eclipse](http://www.eclipse.org) 또는 [IntelliJ](https://www.jetbrains.com/idea/)와 같은 Java 통합 개발 환경(IDE)을 선택할 수 있습니다.

1. 를 AWS SDK for Java 사용하여에 대한 개발 환경을 구성합니다[기본 AWS 보안 인증 정보 및 리전 설정](#getting-started.java.credentials).

그런 다음 전체 자습서 샘플 애플리케이션을 다운로드하거나 Java 프로젝트에 드라이버만 설치하고 단축 코드 예제를 실행할 수 있습니다.
+ 기존 프로젝트에 QLDB 드라이버와 AWS SDK for Java 를 설치하려면 로 이동합니다[설치](#getting-started.java.install).
+ 프로젝트를 설정하고 원장에 대한 기본 데이터 트랜잭션을 보여주는 단축 코드 예제를 실행하려면 [빠른 시작 자습서](driver-quickstart-java.md)를 참조하세요.
+ 전체 자습서 샘플 애플리케이션에서 데이터 및 관리 API 작업에 대한 보다 심층적인 예제를 실행하려면 [Java 자습서](getting-started.java.tutorial.md)를 참조하세요.

## 기본 AWS 보안 인증 정보 및 리전 설정
<a name="getting-started.java.credentials"></a>

QLDB 드라이버와 기본를 [AWS SDK for Java](https://aws.amazon.com/sdk-for-java) 사용하려면 런타임에 애플리케이션에 AWS 자격 증명을 제공해야 합니다. 이 설명서의 코드 예제에서는 *AWS SDK for Java 2.x 개발자 안내서*의 [보안 인증 정보 및 리전 설정](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/setup.html#setup-credentials)에 설명된 대로 AWS 보안 인증 정보 파일을 사용한다고 가정합니다.

이러한 단계의 일부로 기본 QLDB 엔드포인트를 결정 AWS 리전 하도록 기본값을 설정해야 합니다. 코드 예제는 기본 AWS 리전의 QLDB에 연결됩니다. QLDB를 사용할 수 있는 리전의 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

다음은 이름이 인 AWS 자격 증명 파일의 예입니다. `~/.aws/credentials`여기서 타일 문자(`~`)는 홈 디렉터리를 나타냅니다.

```
[default] 
aws_access_key_id = your_access_key_id
aws_secret_access_key = your_secret_access_key
```

자체 AWS 자격 증명 값을 *your\$1access\$1key\$1id* 및 *your\$1secret\$1access\$1key* 값으로 바꿉니다.

## 설치
<a name="getting-started.java.install"></a>

QLDB는 다음 Java 드라이버 버전과 해당 AWS SDK 종속성을 지원합니다.


****  

| 드라이버 버전 | AWS SDK | 상태 표시기 | 최근 릴리스 날짜 | 
| --- | --- | --- | --- | 
| [1.x](https://search.maven.org/artifact/software.amazon.qldb/amazon-qldb-driver-java/1.1.0/jar) | AWS SDK for Java 1.x | 프로덕션 릴리스 | 2020년 3월 20일 | 
| [2.x](https://search.maven.org/artifact/software.amazon.qldb/amazon-qldb-driver-java/2.3.1/jar) | AWS SDK for Java 2.x | 프로덕션 릴리스 | 2021년 6월 4일 | 

QLDB 드라이버를 설치하려면 Gradle 또는 Maven과 같은 종속성 관리 시스템을 사용할 것을 권장합니다. 예를 들어, Java 프로젝트에 다음 아티팩트를 종속성 항목으로 추가합니다.

------
#### [ 2.x ]

**Gradle**

`build.gradle` 구성 파일에 이 종속성을 추가합니다.

```
dependencies {
    compile group: 'software.amazon.qldb', name: 'amazon-qldb-driver-java', version: '2.3.1'
}
```

**Maven**

`pom.xml` 구성 파일에 이 종속성을 추가합니다.

```
<dependencies>
  <dependency>
    <groupId>software.amazon.qldb</groupId>
    <artifactId>amazon-qldb-driver-java</artifactId>
    <version>2.3.1</version>
  </dependency>
</dependencies>
```

이 아티팩트에는 AWS SDK for Java 2.x 코어 모듈, [Amazon Ion](ion.md) 라이브러리 및 기타 필수 종속성이 자동으로 포함됩니다.

------
#### [ 1.x ]

**Gradle**

`build.gradle` 구성 파일에 이 종속성을 추가합니다.

```
dependencies {
    compile group: 'software.amazon.qldb', name: 'amazon-qldb-driver-java', version: '1.1.0'
}
```

**Maven**

`pom.xml` 구성 파일에 이 종속성을 추가합니다.

```
<dependencies>
  <dependency>
    <groupId>software.amazon.qldb</groupId>
    <artifactId>amazon-qldb-driver-java</artifactId>
    <version>1.1.0</version>
  </dependency>
</dependencies>
```

이 아티팩트에는 AWS SDK for Java 코어 모듈, [Amazon Ion](ion.md) 라이브러리 및 기타 필수 종속성이 자동으로 포함됩니다.

**중요**  
**Amazon Ion 네임스페이스** - 애플리케이션에서 Amazon Ion 클래스를 가져올 때는 네임스페이스 `com.amazon.ion` 아래에 있는 패키지를 사용해야 합니다. AWS SDK for Java 은 네임스페이스 `software.amazon.ion` 아래의 다른 Ion 패키지에 따라 다르지만 이 패키지는 QLDB 드라이버와 호환되지 않는 레거시 패키지입니다.

------

원장에서 기본 데이터 트랜잭션을 실행하는 방법에 대한 단축 코드 예제는 [Cookbook 참조](driver-cookbook-java.md)를 참조하세요.

### 기타 옵션 라이브러리
<a name="getting-started.java.install-optional"></a>

선택 사항으로 프로젝트에 다음과 같은 유용한 라이브러리를 추가할 수도 있습니다. 이러한 아티팩트는 [Java 자습서](getting-started.java.tutorial.md) 샘플 애플리케이션의 필수 종속성입니다.

1. [aws-java-sdk-qldb](https://search.maven.org/artifact/com.amazonaws/aws-java-sdk-qldb/1.11.785/jar) - AWS SDK for Java의 QLDB 모듈입니다. 현재 지원되는 QLDB 최소 버전은 `1.11.785`입니다.

   애플리케이션에서 이 모듈을 사용하여 [Amazon QLDB API 참조](api-reference.md)에 나열된 관리 API 작업과 직접 상호 작용할 수 있습니다.

1. [jackson-dataformat-ion](https://search.maven.org/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-ion/2.10.0/bundle) - FasterXML의 Ion용 Jackson 데이터 형식 모듈입니다. 샘플 애플리케이션에는 `2.10.0` 버전 이상이 필요합니다.

------
#### [ Gradle ]

`build.gradle` 구성 파일에 이 종속성을 추가합니다.

```
dependencies {
    compile group: 'com.amazonaws', name: 'aws-java-sdk-qldb', version: '1.11.785'
    compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-ion', version: '2.10.0'
}
```

------
#### [ Maven ]

`pom.xml` 구성 파일에 이 종속성을 추가합니다.

```
<dependencies>
  <dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-qldb</artifactId>
    <version>1.11.785</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-ion</artifactId>
    <version>2.10.0</version>
  </dependency>
</dependencies>
```

------

# Java용 Amazon QLDB 드라이버 - 빠른 시작 자습서
<a name="driver-quickstart-java"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 Java용 Amazon QLDB 드라이버의 최신 버전을 사용하여 간단한 애플리케이션을 설정하는 방법을 알아봅니다. 이 안내서에는 드라이버 설치 단계 및 기본적인 CRUD(*생성, 읽기, 업데이트 및 삭제*) 작업에 대한 단축 코드 예제가 포함되어 있습니다. 전체 샘플 애플리케이션에서 이러한 작업을 보여 주는 자세한 예를 보려면 [Java 자습서](getting-started.java.tutorial.md) 섹션을 참조하세요.

**Topics**
+ [

## 사전 조건
](#driver-quickstart-java.prereqs)
+ [

## 1단계: 프로젝트 설정
](#driver-quickstart-java.step-1)
+ [

## 2단계: 드라이버 초기화
](#driver-quickstart-java.step-2)
+ [

## 3단계: 테이블 및 인덱스 생성
](#driver-quickstart-java.step-3)
+ [

## 4단계: 문서 삽입
](#driver-quickstart-java.step-4)
+ [

## 5단계: 문서 쿼리
](#driver-quickstart-java.step-5)
+ [

## 6단계: 문서 업데이트
](#driver-quickstart-java.step-6)
+ [

## 전체 애플리케이션 실행
](#driver-quickstart-java.complete)

## 사전 조건
<a name="driver-quickstart-java.prereqs"></a>

시작하기 전에 다음을 수행해야 합니다.

1. Java 드라이버를 위한 [사전 조건](getting-started.java.md#getting-started.java.prereqs)을 아직 완료하지 않은 경우, 완료하세요. 여기에는 가입 AWS, 개발을 위한 프로그래밍 방식 액세스 권한 부여, Java 통합 개발 환경(IDE) 설치가 포함됩니다.

1. `quick-start`라는 명칭의 원장을 생성합니다.

   원장 생성 방법을 알아보려면 *콘솔 시작하기*의 [Amazon QLDB 원장의 기본 작업](ledger-management.basics.md) 또는 [1단계: 새 원장 생성](getting-started-step-1.md) 섹션을 참조하세요.

## 1단계: 프로젝트 설정
<a name="driver-quickstart-java.step-1"></a>

먼저 Java 프로젝트를 설정합니다. 이 자습서에서는 [Maven](https://maven.apache.org/index.html) 종속성 관리 시스템을 사용하는 것이 좋습니다.

**참고**  
이러한 설정 단계를 자동화하는 기능이 있는 IDE를 사용하는 경우 [2단계: 드라이버 초기화](#driver-quickstart-java.step-2)로 넘어가도 됩니다.

1. 애플리케이션을 위한 폴더를 생성합니다.

   ```
   $ mkdir myproject
   $ cd myproject
   ```

1. 다음 명령을 입력하여 Maven 템플릿에서 프로젝트를 초기화합니다. *project-package*, *project-name*, *maven-template*을 원하는 값으로 적절하게 바꿉니다.

   ```
   $ mvn archetype:generate
     -DgroupId=project-package \
     -DartifactId=project-name \
     -DarchetypeArtifactId=maven-template \
     -DinteractiveMode=false
   ```

   *maven-template*의 경우 다음 기본 Maven 템플릿을 사용할 수 있습니다. `maven-archetype-quickstart` 

1. [Java용 QLDB 드라이버](https://search.maven.org/artifact/software.amazon.qldb/amazon-qldb-driver-java/2.3.1/jar)를 프로젝트 종속 항목으로 추가하려면 새로 만든 `pom.xml` 파일로 이동하여 다음 아티팩트를 추가합니다.

   ```
   <dependency>
       <groupId>software.amazon.qldb</groupId>
       <artifactId>amazon-qldb-driver-java</artifactId>
       <version>2.3.1</version>
   </dependency>
   ```

   이 아티팩트에는 [AWS SDK for Java 2.x](https://aws.amazon.com/sdk-for-java) 코어 모듈, [Amazon Ion](ion.md) 라이브러리 및 기타 필수 종속성이 자동으로 포함됩니다. 이제 `pom.xml` 파일은 다음과 같아야 합니다.

   ```
   <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>software.amazon.qldb</groupId>
     <artifactId>qldb-quickstart</artifactId>
     <packaging>jar</packaging>
     <version>1.0-SNAPSHOT</version>
     <name>qldb-quickstart</name>
     <url>http://maven.apache.org</url>
     <dependencies>
       <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>3.8.1</version>
         <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>software.amazon.qldb</groupId>
           <artifactId>amazon-qldb-driver-java</artifactId>
           <version>2.3.1</version>
       </dependency>
     </dependencies>
   </project>
   ```

1. `App.java` 파일을 엽니다.

   그런 다음, 다음 단계의 코드 예를 점진적으로 추가하여 몇 가지 기본 CRUD 작업을 시도해 보세요. 또는 단계별 자습서를 건너뛰고 [전체 애플리케이션](#driver-quickstart-java.complete)을 실행할 수도 있습니다.

## 2단계: 드라이버 초기화
<a name="driver-quickstart-java.step-2"></a>

`quick-start`라는 명칭의 원장에 연결되는 드라이버의 인스턴스를 초기화합니다. 다음 코드를 `App.java` 파일에 추가합니다.

```
import java.util.*;
import com.amazon.ion.*;
import com.amazon.ion.system.*;
import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
import software.amazon.qldb.*;

public final class App {
    public static IonSystem ionSys = IonSystemBuilder.standard().build();
    public static QldbDriver qldbDriver;

    public static void main(final String... args) {
        System.out.println("Initializing the driver");
        qldbDriver = QldbDriver.builder()
            .ledger("quick-start")
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(QldbSessionClient.builder())
            .build();
    }
}
```

## 3단계: 테이블 및 인덱스 생성
<a name="driver-quickstart-java.step-3"></a>

다음 코드 예에서는 `CREATE TABLE` 및 `CREATE INDEX` 문을 실행하는 방법을 보여줍니다.

`main` 메서드에서 `People`라는 명칭의 표와 해당 표의 `lastName` 필드를 위한 인덱스를 만드는 다음 코드를 추가합니다. [인덱스](ql-reference.create-index.md)는 쿼리 성능을 최적화하고 [OCC(낙관적 동시성 제어)](concurrency.md) 충돌 예외를 제한하는 데 필요합니다.

```
// Create a table and an index in the same transaction
qldbDriver.execute(txn -> {
    System.out.println("Creating a table and an index");
    txn.execute("CREATE TABLE People");
    txn.execute("CREATE INDEX ON People(lastName)");
});
```

## 4단계: 문서 삽입
<a name="driver-quickstart-java.step-4"></a>

다음 코드 예에서는 `INSERT` 문을 실행하는 방법을 보여줍니다. QLDB는 [PartiQL](ql-reference.md) 쿼리 언어(SQL 호환) 및 [Amazon Ion](ion.md) 데이터 형식(JSON의 상위 집합)을 지원합니다.

`People` 테이블에 문서를 삽입하는 다음 코드를 추가합니다.

```
// Insert a document
qldbDriver.execute(txn -> {
    System.out.println("Inserting a document");
    IonStruct person = ionSys.newEmptyStruct();
    person.put("firstName").newString("John");
    person.put("lastName").newString("Doe");
    person.put("age").newInt(32);
    txn.execute("INSERT INTO People ?", person);
});
```

이 예에서는 물음표(`?`)를 변수 자리 표시자로 사용하여 문서 정보를 해당 문에 전달합니다. 자리 표시자를 사용할 때는 `IonValue` 타입의 값을 전달해야 합니다.

**작은 정보**  
단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [IonList](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터(명시적으로 `IonValue`로 캐스팅됨)를 해당 문에 전달할 수 있습니다.  

```
// people is an IonList explicitly cast as an IonValue
txn.execute("INSERT INTO People ?", (IonValue) people);
```
`IonList`를 전달할 때는 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

## 5단계: 문서 쿼리
<a name="driver-quickstart-java.step-5"></a>

다음 코드 예에서는 `SELECT` 문을 실행하는 방법을 보여줍니다.

`People` 테이블에서 문서를 쿼리하는 다음 코드를 추가합니다.

```
// Query the document
qldbDriver.execute(txn -> {
    System.out.println("Querying the table");
    Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
            ionSys.newString("Doe"));
    IonStruct person = (IonStruct) result.iterator().next();
    System.out.println(person.get("firstName")); // prints John
    System.out.println(person.get("lastName")); // prints Doe
    System.out.println(person.get("age")); // prints 32
});
```

## 6단계: 문서 업데이트
<a name="driver-quickstart-java.step-6"></a>

다음 코드 예에서는 `UPDATE` 문을 실행하는 방법을 보여줍니다.

1. `age`를 `42`로 업데이트하여 `People` 표의 문서를 업데이트하는 다음 코드를 추가합니다.

   ```
   // Update the document
   qldbDriver.execute(txn -> {
       System.out.println("Updating the document");
       final List<IonValue> parameters = new ArrayList<>();
       parameters.add(ionSys.newInt(42));
       parameters.add(ionSys.newString("Doe"));
       txn.execute("UPDATE People SET age = ? WHERE lastName = ?", parameters);
   });
   ```

1. 문서를 다시 쿼리하여 업데이트된 값을 확인합니다.

   ```
   // Query the updated document
   qldbDriver.execute(txn -> {
       System.out.println("Querying the table for the updated document");
       Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
               ionSys.newString("Doe"));
       IonStruct person = (IonStruct) result.iterator().next();
       System.out.println(person.get("firstName")); // prints John
       System.out.println(person.get("lastName")); // prints Doe
       System.out.println(person.get("age")); // prints 32
   });
   ```

1. Maven 또는 IDE를 사용하여 `App.java` 파일을 컴파일하고 실행합니다.

## 전체 애플리케이션 실행
<a name="driver-quickstart-java.complete"></a>

다음 코드 예는 `App.java` 애플리케이션의 전체 버전입니다. 이전 단계를 개별적으로 수행하는 대신 이 코드 예를 처음부터 끝까지 복사하여 실행할 수도 있습니다. 이 애플리케이션은 `quick-start`이라는 명칭의 원장에 대한 몇 가지 기본 CRUD 작업을 보여줍니다.

**참고**  
이 코드를 실행하기 전에 `quick-start` 원장에 `People`이라는 명칭의 활성 테이블이 아직 없는지 확인하세요.  
첫 번째 행에서 *project-package*를 [1단계: 프로젝트 설정](#driver-quickstart-java.step-1)의 Maven 명령에 사용한 `groupId` 값으로 바꾸세요.

```
package project-package;

import java.util.*;
import com.amazon.ion.*;
import com.amazon.ion.system.*;
import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
import software.amazon.qldb.*;

public class App {
    public static IonSystem ionSys = IonSystemBuilder.standard().build();
    public static QldbDriver qldbDriver;

    public static void main(final String... args) {
        System.out.println("Initializing the driver");
        qldbDriver = QldbDriver.builder()
            .ledger("quick-start")
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(QldbSessionClient.builder())
            .build();

        // Create a table and an index in the same transaction
        qldbDriver.execute(txn -> {
            System.out.println("Creating a table and an index");
            txn.execute("CREATE TABLE People");
            txn.execute("CREATE INDEX ON People(lastName)");
        });
        
        // Insert a document
        qldbDriver.execute(txn -> {
            System.out.println("Inserting a document");
            IonStruct person = ionSys.newEmptyStruct();
            person.put("firstName").newString("John");
            person.put("lastName").newString("Doe");
            person.put("age").newInt(32);
            txn.execute("INSERT INTO People ?", person);
        });
        
        // Query the document
        qldbDriver.execute(txn -> {
            System.out.println("Querying the table");
            Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
                    ionSys.newString("Doe"));
            IonStruct person = (IonStruct) result.iterator().next();
            System.out.println(person.get("firstName")); // prints John
            System.out.println(person.get("lastName")); // prints Doe
            System.out.println(person.get("age")); // prints 32
        });
        
        // Update the document
        qldbDriver.execute(txn -> {
            System.out.println("Updating the document");
            final List<IonValue> parameters = new ArrayList<>();
            parameters.add(ionSys.newInt(42));
            parameters.add(ionSys.newString("Doe"));
            txn.execute("UPDATE People SET age = ? WHERE lastName = ?", parameters);
        });
        
        // Query the updated document
        qldbDriver.execute(txn -> {
            System.out.println("Querying the table for the updated document");
            Result result = txn.execute("SELECT * FROM People WHERE lastName = ?",
                    ionSys.newString("Doe"));
            IonStruct person = (IonStruct) result.iterator().next();
            System.out.println(person.get("firstName")); // prints John
            System.out.println(person.get("lastName")); // prints Doe
            System.out.println(person.get("age")); // prints 42
        });
    }
}
```

Maven 또는 IDE를 사용하여 `App.java` 파일을 컴파일하고 실행합니다.

# Java용 Amazon QLDB 드라이버 - Cookbook 참조
<a name="driver-cookbook-java"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 참조 가이드는 Java용 Amazon QLDB 드라이버의 일반적인 사용 사례를 보여줍니다. 이 Java 코드 예제는 드라이버를 사용하여 기본 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 수행하는 방법을 보여줍니다. 또한 Amazon Ion 데이터를 처리하기 위한 코드 예제도 포함되어 있습니다. 또한 이 가이드에서는 트랜잭션에 멱등성을 부여하고 고유성 제약하는 모범 사례를 중점적으로 설명합니다.

**참고**  
해당하는 경우, 일부 사용 사례에서는 Java용 QLDB 드라이버의 지원되는 각 주요 버전마다 다른 코드 예제가 있습니다.

**Contents**
+ [

## 드라이버 가져오기
](#cookbook-java.importing)
+ [

## 드라이버 인스턴스화
](#cookbook-java.instantiating)
+ [

## CRUD 작업
](#cookbook-java.crud)
  + [

### 테이블 생성
](#cookbook-java.crud.creating-tables)
  + [

### 인덱스 생성
](#cookbook-java.crud.creating-indexes)
  + [

### 문서 읽기
](#cookbook-java.crud.reading)
  + [

### 문서 삽입하기
](#cookbook-java.crud.inserting)
    + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-java.crud.inserting.multiple)
  + [

### 문서 업데이트
](#cookbook-java.crud.updating)
  + [

### 문서 삭제
](#cookbook-java.crud.deleting)
  + [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-java.crud.multi-statement)
  + [

### 재시도 로직
](#cookbook-java.crud.retry-logic)
  + [

### 고유성 제약 조건 구현
](#cookbook-java.crud.uniqueness-constraints)
+ [

## Amazon Ion 작업
](#cookbook-java.ion)
  + [

### Ion 패키지 가져오기
](#cookbook-java.ion.importing)
  + [

### Ion 초기화
](#cookbook-java.ion.initializing)
  + [

### Ion 객체 생성
](#cookbook-java.ion.creating)
  + [

### Ion 객체 읽기
](#cookbook-java.ion.reading)

## 드라이버 가져오기
<a name="cookbook-java.importing"></a>

다음 코드 예제는 드라이버, QLDB 세션 클라이언트, Amazon Ion 패키지 및 기타 관련 종속 항목을 가져옵니다.

------
#### [ 2.x ]

```
import com.amazon.ion.IonStruct;
import com.amazon.ion.IonSystem;
import com.amazon.ion.IonValue;
import com.amazon.ion.system.IonSystemBuilder;

import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
import software.amazon.qldb.QldbDriver;
import software.amazon.qldb.Result;
```

------
#### [ 1.x ]

```
import com.amazon.ion.IonStruct;
import com.amazon.ion.IonSystem;
import com.amazon.ion.IonValue;
import com.amazon.ion.system.IonSystemBuilder;

import com.amazonaws.services.qldbsession.AmazonQLDBSessionClientBuilder;
import software.amazon.qldb.PooledQldbDriver;
import software.amazon.qldb.Result;
```

------

## 드라이버 인스턴스화
<a name="cookbook-java.instantiating"></a>

다음 코드 예제는 지정된 원장 이름에 연결되는 드라이버 인스턴스를 만들고 사용자 지정 재시도 제한이 있는 지정된 [재시도 로직](#cookbook-java.crud.retry-logic)을 사용합니다.

**참고**  
또한 이 예제는 Amazon Ion 시스템 객체(`IonSystem`)를 인스턴스화합니다. 이 참조에서 일부 데이터 작업을 실행할 때 Ion 데이터를 처리하려면 이 객체가 필요합니다. 자세한 내용은 [Amazon Ion 작업](#cookbook-java.ion) 섹션을 참조하세요.

------
#### [ 2.x ]

```
QldbDriver qldbDriver = QldbDriver.builder()
    .ledger("vehicle-registration")
    .transactionRetryPolicy(RetryPolicy
        .builder()
        .maxRetries(3)
        .build())
    .sessionClientBuilder(QldbSessionClient.builder())
    .build();

IonSystem SYSTEM = IonSystemBuilder.standard().build();
```

------
#### [ 1.x ]

```
PooledQldbDriver qldbDriver = PooledQldbDriver.builder()
    .withLedger("vehicle-registration")
    .withRetryLimit(3)
    .withSessionClientBuilder(AmazonQLDBSessionClientBuilder.standard())
    .build();

IonSystem SYSTEM = IonSystemBuilder.standard().build();
```

------

## CRUD 작업
<a name="cookbook-java.crud"></a>

QLDB는 트랜잭션의 일부로 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행합니다.

**주의**  
가장 좋은 방법은 쓰기 트랜잭션이 완전한 멱등성을 부여하는 것입니다.

**트랜잭션에 멱등성 부여하기**

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 좋습니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 *멱등성*을 가집니다.

이름이 `Person`인 테이블에 문서를 삽입하는 트랜잭션을 예로 들어 보겠습니다. 트랜잭션은 먼저 문서가 테이블에 이미 존재하는지 여부를 확인해야 합니다. 이렇게 확인하지 않으면 테이블에 문서가 중복될 수 있습니다.

QLDB가 서버 측에서 트랜잭션을 성공적으로 커밋했지만 응답을 기다리는 동안 클라이언트 제한 시간이 초과되었다고 가정해 보겠습니다. 트랜잭션이 멱등성을 가지지 않는 경우 재시도 시 동일한 문서가 두 번 이상 삽입될 수 있습니다.

**인덱스를 사용하여 전체 테이블 스캔 방지**

인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 이 인덱싱된 조회가 없으면 QLDB는 테이블 스캔을 수행해야 하며, 이로 인해 트랜잭션 제한 시간이 초과되거나 *OCC(낙관적 동시성 제어)* 충돌이 발생할 수 있습니다.

OCC에 대한 자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md) 단원을 참조하세요.

**암시적으로 생성된 트랜잭션**

[QldbDriver.execute](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/QldbDriver.html) 메서드는 [Executor](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/Executor.html) 인스턴스를 수신하는 Lambda 함수를 받아들이며, 이 함수를 사용하여 명령문을 실행할 수 있습니다. `Executor` 인스턴스는 암시적으로 생성된 트랜잭션을 래핑합니다.

`Executor.execute` 메서드를 사용하여 Lambda 함수 내에서 명령문을 실행할 수 있습니다. 드라이버는 Lambda 함수가 반환될 때 트랜잭션을 암시적으로 커밋합니다.

다음 섹션에서는 기본 CRUD 작업을 실행하고, 사용자 지정 재시도 로직을 지정하고, 고유성 제약 조건을 구현하는 방법을 보여줍니다.

**참고**  
해당하는 경우 이 섹션에서는 내장된 Ion 라이브러리와 Jackson Ion 매퍼 라이브러리를 모두 사용하여 Amazon Ion 데이터를 처리하는 코드 예제를 제공합니다. 자세한 내용은 [Amazon Ion 작업](#cookbook-java.ion) 섹션을 참조하세요.

**Contents**
+ [

### 테이블 생성
](#cookbook-java.crud.creating-tables)
+ [

### 인덱스 생성
](#cookbook-java.crud.creating-indexes)
+ [

### 문서 읽기
](#cookbook-java.crud.reading)
+ [

### 문서 삽입하기
](#cookbook-java.crud.inserting)
  + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-java.crud.inserting.multiple)
+ [

### 문서 업데이트
](#cookbook-java.crud.updating)
+ [

### 문서 삭제
](#cookbook-java.crud.deleting)
+ [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-java.crud.multi-statement)
+ [

### 재시도 로직
](#cookbook-java.crud.retry-logic)
+ [

### 고유성 제약 조건 구현
](#cookbook-java.crud.uniqueness-constraints)

### 테이블 생성
<a name="cookbook-java.crud.creating-tables"></a>

```
qldbDriver.execute(txn -> {
    txn.execute("CREATE TABLE Person");
});
```

### 인덱스 생성
<a name="cookbook-java.crud.creating-indexes"></a>

```
qldbDriver.execute(txn -> {
    txn.execute("CREATE INDEX ON Person(GovId)");
});
```

### 문서 읽기
<a name="cookbook-java.crud.reading"></a>

```
// Assumes that Person table has documents as follows:
// { GovId: "TOYENC486FH", FirstName: "Brent" }

qldbDriver.execute(txn -> {
    Result result = txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'");
    IonStruct person = (IonStruct) result.iterator().next();
    System.out.println(person.get("GovId")); // prints TOYENC486FH
    System.out.println(person.get("FirstName")); // prints Brent
});
```

**쿼리 파라미터 사용**

다음 코드 예제는 Ion 유형 쿼리 파라미터를 사용합니다.

```
qldbDriver.execute(txn -> {
    Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?",
            SYSTEM.newString("TOYENC486FH"));
    IonStruct person = (IonStruct) result.iterator().next();
    System.out.println(person.get("GovId")); // prints TOYENC486FH
    System.out.println(person.get("FirstName")); // prints Brent
});
```

다음 코드 예제는 여러 쿼리 파라미터를 사용합니다.

```
qldbDriver.execute(txn -> {
    Result result = txn.execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?",
            SYSTEM.newString("TOYENC486FH"),
            SYSTEM.newString("Brent"));
    IonStruct person = (IonStruct) result.iterator().next();
    System.out.println(person.get("GovId")); // prints TOYENC486FH
    System.out.println(person.get("FirstName")); // prints Brent
});
```

다음 코드 예제는 쿼리 파라미터 목록을 사용합니다.

```
qldbDriver.execute(txn -> {
    final List<IonValue> parameters = new ArrayList<>();
    parameters.add(SYSTEM.newString("TOYENC486FH"));
    parameters.add(SYSTEM.newString("ROEE1"));
    parameters.add(SYSTEM.newString("YH844"));
    Result result = txn.execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", parameters);
    IonStruct person = (IonStruct) result.iterator().next();
    System.out.println(person.get("GovId")); // prints TOYENC486FH
    System.out.println(person.get("FirstName")); // prints Brent
});
```

#### Jackson 매퍼 사용
<a name="cookbook-java.crud.reading.jackson"></a>

```
// Assumes that Person table has documents as follows:
// {GovId: "TOYENC486FH", FirstName: "Brent" }

qldbDriver.execute(txn -> {
    try {
        Result result = txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'");
        Person person = MAPPER.readValue(result.iterator().next(), Person.class);
        System.out.println(person.getFirstName()); // prints Brent
        System.out.println(person.getGovId()); // prints TOYENC486FH
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

**쿼리 파라미터 사용**

다음 코드 예제는 Ion 유형 쿼리 파라미터를 사용합니다.

```
qldbDriver.execute(txn -> {
    try {
        Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?",
                MAPPER.writeValueAsIonValue("TOYENC486FH"));
        Person person = MAPPER.readValue(result.iterator().next(), Person.class);
        System.out.println(person.getFirstName()); // prints Brent
        System.out.println(person.getGovId()); // prints TOYENC486FH
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

다음 코드 예제는 여러 쿼리 파라미터를 사용합니다.

```
qldbDriver.execute(txn -> {
    try {
        Result result = txn.execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?",
                MAPPER.writeValueAsIonValue("TOYENC486FH"),
                MAPPER.writeValueAsIonValue("Brent"));
        Person person = MAPPER.readValue(result.iterator().next(), Person.class);
        System.out.println(person.getFirstName()); // prints Brent
        System.out.println(person.getGovId()); // prints TOYENC486FH
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

다음 코드 예제는 쿼리 파라미터 목록을 사용합니다.

```
qldbDriver.execute(txn -> {
    try {
        final List<IonValue> parameters = new ArrayList<>();
        parameters.add(MAPPER.writeValueAsIonValue("TOYENC486FH"));
        parameters.add(MAPPER.writeValueAsIonValue("ROEE1"));
        parameters.add(MAPPER.writeValueAsIonValue("YH844"));
        Result result = txn.execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", parameters);
        Person person = MAPPER.readValue(result.iterator().next(), Person.class);
        System.out.println(person.getFirstName()); // prints Brent
        System.out.println(person.getGovId()); // prints TOYENC486FH
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

**참고**  
인덱싱된 조회 없이 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. 이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 [인덱스](ql-reference.create-index.md)를 사용하는 것이 좋습니다. `GovId`에 인덱스를 사용하지 않으면 쿼리에 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삽입하기
<a name="cookbook-java.crud.inserting"></a>

다음 코드 예제는 Ion 데이터 유형을 삽입합니다.

```
qldbDriver.execute(txn -> {
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?",
            SYSTEM.newString("TOYENC486FH"));
    // Check if there is a result
    if (!result.iterator().hasNext()) {
        IonStruct person = SYSTEM.newEmptyStruct();
        person.put("GovId").newString("TOYENC486FH");
        person.put("FirstName").newString("Brent");
        // Insert the document
        txn.execute("INSERT INTO Person ?", person);
    }
});
```

#### Jackson 매퍼 사용
<a name="cookbook-java.crud.inserting.jackson"></a>

다음 코드 예제는 Ion 데이터 유형을 삽입합니다.

```
qldbDriver.execute(txn -> {
    try {
        // Check if a document with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?",
                MAPPER.writeValueAsIonValue("TOYENC486FH"));
        // Check if there is a result
        if (!result.iterator().hasNext()) {
            // Insert the document
            txn.execute("INSERT INTO Person ?",
                    MAPPER.writeValueAsIonValue(new Person("Brent", "TOYENC486FH")));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

이 트랜잭션은 문서를 `Person` 테이블에 삽입합니다. 삽입하기 전에 먼저 문서가 테이블에 이미 있는지 확인합니다. **이 검사를 통해 트랜잭션은 본질적으로 멱등성을 가지게 됩니다.** 이 트랜잭션을 여러 번 실행하더라도 의도하지 않은 부작용이 발생하지는 않습니다.

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

#### 하나의 명령문에 여러 문서 삽입
<a name="cookbook-java.crud.inserting.multiple"></a>

단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [IonList](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터(명시적으로 `IonValue`로 캐스팅됨)를 해당 문에 전달할 수 있습니다.

```
// people is an IonList explicitly cast as an IonValue
txn.execute("INSERT INTO People ?", (IonValue) people);
```

`IonList`를 전달할 때는 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

##### 명시적 캐스팅이 필요한 이유는 무엇인가요?
<a name="cookbook-java.crud.inserting.multiple.cast"></a>

[TransactionExecutor.execute](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/TransactionExecutor.html) 메서드가 오버로드되었습니다. 이는 다양한 수의 `IonValue` 인수(*varargs*) 또는 단일 `List<IonValue>` 인수를 받아들입니다. [ion-java](https://www.javadoc.io/doc/com.amazon.ion/ion-java/latest/index.html)에서는 `IonList`는 `List<IonValue>`로 구현됩니다.

Java는 오버로드된 메서드를 호출할 때 기본적으로 가장 구체적인 메서드를 구현합니다. 이 경우 `IonList` 파라미터를 전달하면 `List<IonValue>`를 받는 메서드가 기본값으로 사용됩니다. 호출 시 이 메서드 구현은 목록의 `IonValue` 요소를 고유한 값으로 전달합니다. 따라서 varargs 메서드를 대신 호출하려면 `IonList` 파라미터를 `IonValue`으로 명시적으로 캐스팅해야 합니다.

### 문서 업데이트
<a name="cookbook-java.crud.updating"></a>

```
qldbDriver.execute(txn -> {
    final List<IonValue> parameters = new ArrayList<>();
    parameters.add(SYSTEM.newString("John"));
    parameters.add(SYSTEM.newString("TOYENC486FH"));
    txn.execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", parameters);
});
```

#### Jackson 매퍼 사용
<a name="cookbook-java.crud.updating.jackson"></a>

```
qldbDriver.execute(txn -> {
    try {
        final List<IonValue> parameters = new ArrayList<>();
        parameters.add(MAPPER.writeValueAsIonValue("John"));
        parameters.add(MAPPER.writeValueAsIonValue("TOYENC486FH"));
        txn.execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", parameters);
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삭제
<a name="cookbook-java.crud.deleting"></a>

```
qldbDriver.execute(txn -> {
    txn.execute("DELETE FROM Person WHERE GovId = ?",
            SYSTEM.newString("TOYENC486FH"));
});
```

#### Jackson 매퍼 사용
<a name="cookbook-java.crud.deleting.jackson"></a>

```
qldbDriver.execute(txn -> {
    try {
        txn.execute("DELETE FROM Person WHERE GovId = ?",
                MAPPER.writeValueAsIonValue("TOYENC486FH"));
    } catch (IOException e) {
        e.printStackTrace();
    }
});
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 하나의 트랜잭션에서 여러 명령문 실행
<a name="cookbook-java.crud.multi-statement"></a>

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static boolean InsureCar(QldbDriver qldbDriver, final String vin) {
    final IonSystem ionSystem = IonSystemBuilder.standard().build();
    final IonString ionVin = ionSystem.newString(vin);

    return qldbDriver.execute(txn -> {
        Result result = txn.execute(
                "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE",
                ionVin);
        if (!result.isEmpty()) {
            txn.execute("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

### 재시도 로직
<a name="cookbook-java.crud.retry-logic"></a>

드라이버의 `execute` 메서드에는 재시도 가능한 예외(예: 시간 초과 또는 OCC 충돌)가 발생할 경우 트랜잭션을 재시도하는 재시도 메커니즘이 내장되어 있습니다.

------
#### [ 2.x ]

최대 재시도 횟수와 백오프 전략을 구성할 수 있습니다.

기본 재시도 제한은 `4`이고 기본 백오프 전략은 [DefaultQldbTransactionBackoffStrategy](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/DefaultQldbTransactionBackoffStrategy.html)입니다. [RetryPolicy](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/software/amazon/qldb/RetryPolicy.html) 인스턴스를 사용하여 드라이버 인스턴스별 및 트랜잭션별 재시도 구성을 설정할 수 있습니다.

다음 코드 예제는 드라이버 인스턴스에 대한 사용자 지정 재시도 제한 및 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다.

```
public void retry() {
    QldbDriver qldbDriver = QldbDriver.builder()
            .ledger("vehicle-registration")
            .transactionRetryPolicy(RetryPolicy.builder()
                    .maxRetries(2)
                    .backoffStrategy(new CustomBackOffStrategy()).build())
            .sessionClientBuilder(QldbSessionClient.builder())
            .build();
}

private class CustomBackOffStrategy implements BackoffStrategy {

    @Override
    public Duration calculateDelay(RetryPolicyContext retryPolicyContext) {
        return Duration.ofMillis(1000 * retryPolicyContext.retriesAttempted());
    }
}
```

다음 코드 예제는 특정 함수에 대한 사용자 지정 재시도 제한 및 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다. `execute`에 대한 이 구성은 드라이버 인스턴스에 설정된 재시도 로직을 재정의합니다.

```
public void retry() {
    Result result = qldbDriver.execute(txn -> { txn.execute("SELECT * FROM Person WHERE GovId = ?",
            SYSTEM.newString("TOYENC486FH")); },
            RetryPolicy.builder()
                    .maxRetries(2)
                    .backoffStrategy(new CustomBackOffStrategy())
                    .build());
}

private class CustomBackOffStrategy implements BackoffStrategy {

    // Configuring a custom backoff which increases delay by 1s for each attempt.
    @Override
    public Duration calculateDelay(RetryPolicyContext retryPolicyContext) {
        return Duration.ofMillis(1000 * retryPolicyContext.retriesAttempted());
    }
}
```

------
#### [ 1.x ]

최대 재시도 횟수는 구성 가능합니다. `PooledQldbDriver`를 초기화할 때 `retryLimit` 속성을 설정하여 재시도 제한을 구성할 수 있습니다.

기본 재시도 한도는 `4`입니다.

------

### 고유성 제약 조건 구현
<a name="cookbook-java.crud.uniqueness-constraints"></a>

QLDB는 고유 인덱스를 지원하지 않지만 애플리케이션에서 이 동작을 구현할 수 있습니다.

`Person` 테이블의 `GovId` 필드에 고유성 제약 조건을 구현하려고 한다고 가정해 보겠습니다. 이렇게 하면 다음 작업을 수행하는 트랜잭션을 작성합니다.

1. 테이블에 지정된 `GovId`가 있는 기존 문서가 없는지 확인합니다.

1. 어설션이 통과하면 문서를 삽입합니다.

경쟁 트랜잭션이 어설션을 동시에 통과하면 트랜잭션 중 하나만 성공적으로 커밋됩니다. 다른 트랜잭션은 OCC 충돌 예외가 발생하여 실패합니다.

다음 코드 예제는 이 고유성 제약 조건 구현 방법을 보여줍니다.

```
qldbDriver.execute(txn -> {
    Result result = txn.execute("SELECT * FROM Person WHERE GovId = ?",
            SYSTEM.newString("TOYENC486FH"));
    // Check if there is a result
    if (!result.iterator().hasNext()) {
        IonStruct person = SYSTEM.newEmptyStruct();
        person.put("GovId").newString("TOYENC486FH");
        person.put("FirstName").newString("Brent");
        // Insert the document
        txn.execute("INSERT INTO Person ?", person);
    }
});
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

## Amazon Ion 작업
<a name="cookbook-java.ion"></a>

QLDB에서 Amazon Ion 데이터를 처리하는 방법은 여러 가지가 있습니다. [Ion 라이브러리](https://github.com/amzn/ion-java)의 기본 제공 메서드를 사용하여 필요에 따라 유연하게 문서를 만들고 수정할 수 있습니다. 또는 FasterXML의 [Ion용 Jackson 데이터 형식 모듈](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/ion)을 사용하여 Ion 문서를 *POJO(Plain Old Java Object)* 모델에 매핑할 수 있습니다.

다음 섹션에서는 두 기술을 모두 사용하여 Ion 데이터를 처리하는 코드 예제를 제공합니다.

**Contents**
+ [

### Ion 패키지 가져오기
](#cookbook-java.ion.importing)
+ [

### Ion 초기화
](#cookbook-java.ion.initializing)
+ [

### Ion 객체 생성
](#cookbook-java.ion.creating)
+ [

### Ion 객체 읽기
](#cookbook-java.ion.reading)

### Ion 패키지 가져오기
<a name="cookbook-java.ion.importing"></a>

아티팩트 [ion-java](https://search.maven.org/artifact/com.amazon.ion/ion-java/1.6.1/bundle)를 Java 프로젝트에 종속 항목으로 추가합니다.

------
#### [ Gradle ]

```
dependencies {
    compile group: 'com.amazon.ion', name: 'ion-java', version: '1.6.1'
}
```

------
#### [ Maven ]

```
<dependencies>
  <dependency>
    <groupId>com.amazon.ion</groupId>
    <artifactId>ion-java</artifactId>
    <version>1.6.1</version>
  </dependency>
</dependencies>
```

------

다음 Ion 패키지를 가져옵니다.

```
import com.amazon.ion.IonStruct;
import com.amazon.ion.IonSystem;
import com.amazon.ion.system.IonSystemBuilder;
```

#### Jackson 매퍼 사용
<a name="cookbook-java.ion.importing.jackson"></a>

Java 프로젝트에 아티팩트 [jackson-dataformat-ion](https://search.maven.org/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-ion/2.10.0/bundle)을 종속 항목으로 추가합니다. QLDB에는 버전 `2.10.0` 이상이 필요합니다.

------
#### [ Gradle ]

```
dependencies {
    compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-ion', version: '2.10.0'
}
```

------
#### [ Maven ]

```
<dependencies>
  <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-ion</artifactId>
    <version>2.10.0</version>
  </dependency>
</dependencies>
```

------

다음 Ion 패키지를 가져옵니다.

```
import com.amazon.ion.IonReader;
import com.amazon.ion.IonStruct;
import com.amazon.ion.system.IonReaderBuilder;
import com.amazon.ion.system.IonSystemBuilder;

import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
import com.fasterxml.jackson.dataformat.ion.ionvalue.IonValueMapper;
```

### Ion 초기화
<a name="cookbook-java.ion.initializing"></a>

```
IonSystem SYSTEM = IonSystemBuilder.standard().build();
```

#### Jackson 매퍼 사용
<a name="cookbook-java.ion.initializing.jackson"></a>

```
IonObjectMapper MAPPER = new IonValueMapper(IonSystemBuilder.standard().build());
```

### Ion 객체 생성
<a name="cookbook-java.ion.creating"></a>

다음 코드 예제는 `IonStruct` 인터페이스와 내장 메서드를 사용하여 Ion 객체를 만듭니다.

```
IonStruct ionStruct = SYSTEM.newEmptyStruct();

ionStruct.put("GovId").newString("TOYENC486FH");
ionStruct.put("FirstName").newString("Brent");

System.out.println(ionStruct.toPrettyString()); // prints a nicely formatted copy of ionStruct
```

#### Jackson 매퍼 사용
<a name="cookbook-java.ion.creating.jackson"></a>

다음과 같이 `Person`라는 명칭의 JSON 매핑된 모델 클래스가 있다고 가정하겠습니다.

```
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public static class Person {
    private final String firstName;
    private final String govId;

    @JsonCreator
    public Person(@JsonProperty("FirstName") final String firstName,
                  @JsonProperty("GovId") final String govId) {
        this.firstName = firstName;
        this.govId = govId;
    }

    @JsonProperty("FirstName")
    public String getFirstName() {
        return firstName;
    }

    @JsonProperty("GovId")
    public String getGovId() {
        return govId;
    }
}
```

다음 코드 예제는 `Person`의 인스턴스에서 `IonStruct` 객체를 만듭니다.

```
IonStruct ionStruct = (IonStruct) MAPPER.writeValueAsIonValue(new Person("Brent", "TOYENC486FH"));
```

### Ion 객체 읽기
<a name="cookbook-java.ion.reading"></a>

다음 코드 예제는 `ionStruct` 인스턴스의 각 필드를 인쇄합니다.

```
// ionStruct is an instance of IonStruct
System.out.println(ionStruct.get("GovId")); // prints TOYENC486FH
System.out.println(ionStruct.get("FirstName")); // prints Brent
```

#### Jackson 매퍼 사용
<a name="cookbook-java.ion.reading.jackson"></a>

다음 코드 예제는 `IonStruct` 객체를 읽고 이를 `Person`의 인스턴스에 매핑합니다.

```
// ionStruct is an instance of IonStruct
IonReader reader = IonReaderBuilder.standard().build(ionStruct);
Person person = MAPPER.readValue(reader, Person.class);
System.out.println(person.getFirstName()); // prints Brent
System.out.println(person.getGovId()); // prints TOYENC486FH
```

Ion 작업에 대한 자세한 정보는 GitHub의 [Amazon Ion 설명서](http://amzn.github.io/ion-docs/)를 참조하세요. QLDB에서 Ion을 사용하는 방법에 대한 추가 코드 예제는 [Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업](driver-working-with-ion.md) 섹션을 참조하세요.

# .NET용 Amazon QLDB 드라이버
<a name="getting-started.dotnet"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

원장의 데이터를 사용하려면 AWS 제공된 드라이버를 사용하여 Microsoft .NET 애플리케이션에서 Amazon QLDB에 연결할 수 있습니다. 드라이버는 **.NET Standard 2.0**을 대상으로 합니다. 구체적으로, **.NET Core (LTS) 2.1\$1** 및 **.NET Framework 4.5.2\$1**를 지원합니다. 호환성에 대한 자세한 내용은 *Microsoft Docs* 사이트의 [.NET Standard](https://docs.microsoft.com/en-us/dotnet/standard/net-standard)을 참조하세요.

Amazon Ion 유형과 네이티브 C\$1 유형 간에 수동으로 변환할 필요가 없도록 하려면 *Ion 객체 매퍼*를 사용하는 것이 좋습니다.

다음 주제에서는 .NET용 QLDB 드라이버를 시작하는 방법에 대해 설명합니다.

**Topics**
+ [

## 드라이버 리소스
](#getting-started.dotnet.resources)
+ [

## 사전 조건
](#getting-started.dotnet.prereqs)
+ [

## 설치
](#getting-started.dotnet.install)
+ [빠른 시작 자습서](driver-quickstart-dotnet.md)
+ [Cookbook 참조](driver-cookbook-dotnet.md)

## 드라이버 리소스
<a name="getting-started.dotnet.resources"></a>

.NET 드라이버에서 지원하는 기능에 대한 자세한 정보는 다음 리소스를 참조하세요.
+ [API 참조](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.html)
+ [드라이버 소스 코드(GitHub)](https://github.com/awslabs/amazon-qldb-driver-dotnet)
+ [샘플 애플리케이션 소스 코드(GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-dotnet)
+ [Amazon Ion Cookbook](http://amzn.github.io/ion-docs/guides/cookbook.html)
+ [Ion 객체 매퍼(GitHub)](https://github.com/amzn/ion-object-mapper-dotnet)

## 사전 조건
<a name="getting-started.dotnet.prereqs"></a>

.NET용 QLDB 드라이버를 시작하기 전에 다음을 수행해야 합니다.

1. 의 AWS 설정 지침을 따릅니다[Amazon QLDB 액세스](accessing.md). 다음 내용이 포함됩니다:

   1. 가입합니다 AWS.

   1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

   1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

1. [Microsoft .NET 다운로드](https://dotnet.microsoft.com/download) 사이트에서 .NET Core SDK 버전 2.1 이상을 다운로드하여 설치합니다.

1. (선택 사항) Visual Studio, Mac용 Visual Studio 또는 Visual Studio Code와 같은 통합 개발 환경(IDE)을 원하는 대로 설치합니다. 이러한 프로그램은 [Microsoft Visual Studio](https://visualstudio.microsoft.com/) 사이트에서 다운로드할 수 있습니다.

1. [AWS SDK for .NET](https://aws.amazon.com/sdk-for-net)를 위한 개발 환경 구성:

   1.  AWS 자격 증명을 설정합니다. 공유 보안 인증 파일을 생성할 것을 권장합니다.

      지침은 *AWS SDK for .NET 개발자 안내서*의 [보안 인증 파일을 사용하여 AWS 보안 인증 구성](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-config-creds.html#creds-file)을 참조하세요.

   1. 기본 AWS 리전을 설정하세요. 방법을 알아보려면 [AWS 리전 선택](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-region-selection.html)을 참조하세요.

      사용 가능한 리전의 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

그런 다음 기본 샘플 애플리케이션을 설정하고 단축 코드 예제를 실행하거나 기존 .NET 프로젝트에 드라이버를 설치할 수 있습니다.
+ 기존 프로젝트에 QLDB 드라이버와 AWS SDK for .NET 를 설치하려면 로 이동합니다[설치](#getting-started.dotnet.install).
+ 프로젝트를 설정하고 원장에 대한 기본 데이터 트랜잭션을 보여주는 단축 코드 예제를 실행하려면 [빠른 시작 자습서](driver-quickstart-dotnet.md)를 참조하세요.

## 설치
<a name="getting-started.dotnet.install"></a>

NuGet 패키지 관리자를 사용하여 .NET용 QLDB 드라이버를 설치합니다. 원하는 대로 Visual Studio 또는 IDE를 사용하여 프로젝트 종속 항목을 추가하는 것이 좋습니다. 드라이버 패키지 이름은 [Amazon.QLDB.Driver](https://www.nuget.org/packages/amazon.qldb.driver)입니다.

예를 들어 Visual Studio의 경우, **도구** 메뉴에서 **NuGet 패키지 관리자 콘솔**을 엽니다. 그런 다음 `PM>` 프롬프트에 다음 명령을 입력합니다.

```
PM> Install-Package Amazon.QLDB.Driver
```

드라이버를 설치하면 AWS SDK for .NET 및 [Amazon Ion](ion.md) 패키지를 비롯한 종속성도 설치됩니다.

### Ion 객체 매퍼 설치
<a name="getting-started.dotnet.install.mapper"></a>

.NET용 QLDB 드라이버 버전 1.3.0에는 Amazon Ion을 사용할 필요 없이 네이티브 C\$1 데이터 유형을 수락하고 반환할 수 있는 지원이 도입되었습니다. 이 기능을 사용하려면 프로젝트에 다음 패키지를 추가하세요.
+ [Amazon.QLDB.Driver.Serialization](https://www.nuget.org/packages/Amazon.QLDB.Driver.Serialization/) - Ion 값을 C\$1 *POCO(Plain Old CLR Object)*에 매핑하거나 그 반대로도 매핑할 수 있는 라이브러리입니다. 이 Ion 객체 매퍼를 사용하면 애플리케이션이 Ion을 사용할 필요 없이 네이티브 C\$1 데이터 유형과 직접 상호 작용할 수 있습니다. 이 라이브러리를 사용하는 방법에 대한 간단한 가이드는 GitHub 리포지토리 `awslabs/amazon-qldb-driver-dotnet`의 [SERIALIZATION.md](https://github.com/awslabs/amazon-qldb-driver-dotnet/blob/master/SERIALIZATION.md) 파일을 참조하세요.

이 패키지를 설치하려면 다음 명령을 입력합니다.

```
PM> Install-Package Amazon.QLDB.Driver.Serialization
```

원장에서 기본 데이터 트랜잭션을 실행하는 방법에 대한 단축 코드 예제는 [Cookbook 참조](driver-cookbook-dotnet.md)를 참조하세요.

# .NET용 Amazon QLDB 드라이버 - 빠른 시작 자습서
<a name="driver-quickstart-dotnet"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 .NET용 Amazon QLDB 드라이버를 사용하여 간단한 애플리케이션을 설정하는 방법을 알아봅니다. 이 안내서에는 드라이버 설치 단계 및 기본적인 CRUD(*생성, 읽기, 업데이트 및 삭제*) 작업에 대한 단축 코드 예제가 포함되어 있습니다.

**Topics**
+ [

## 사전 조건
](#driver-quickstart-dotnet.prereqs)
+ [

## 1단계: 프로젝트 설정
](#driver-quickstart-dotnet.step-1)
+ [

## 2단계: 드라이버 초기화
](#driver-quickstart-dotnet.step-2)
+ [

## 3단계: 테이블 및 인덱스 생성
](#driver-quickstart-dotnet.step-3)
+ [

## 4단계: 문서 삽입
](#driver-quickstart-dotnet.step-4)
+ [

## 5단계: 문서 쿼리
](#driver-quickstart-dotnet.step-5)
+ [

## 6단계: 문서 업데이트
](#driver-quickstart-dotnet.step-6)
+ [

## 전체 애플리케이션 실행
](#driver-quickstart-dotnet.complete)

## 사전 조건
<a name="driver-quickstart-dotnet.prereqs"></a>

시작하기 전에 다음을 수행해야 합니다.

1. 아직 완료하지 않은 경우 .NET 드라이버에 대해 [사전 조건](getting-started.dotnet.md#getting-started.dotnet.prereqs)를 완료합니다. 여기에는 가입 AWS, 개발을 위한 프로그래밍 방식 액세스 권한 부여, .NET Core SDK 설치가 포함됩니다.

1. `quick-start`라는 명칭의 원장을 생성합니다.

   원장 생성 방법을 알아보려면 *콘솔 시작하기*의 [Amazon QLDB 원장의 기본 작업](ledger-management.basics.md) 또는 [1단계: 새 원장 생성](getting-started-step-1.md) 섹션을 참조하세요.

## 1단계: 프로젝트 설정
<a name="driver-quickstart-dotnet.step-1"></a>

먼저 .NET 프로젝트를 설정합니다.

1. 템플릿 애플리케이션을 생성하고 실행하려면 *bash*, *PowerShell* 또는 *명령 프롬프트*와 같은 터미널에 `dotnet` 명령을 입력합니다.

   ```
   $ dotnet new console --output Amazon.QLDB.QuickStartGuide
   $ dotnet run --project Amazon.QLDB.QuickStartGuide
   ```

   이 템플릿은 `Amazon.QLDB.QuickStartGuide`이라는 폴더를 생성합니다. 해당 폴더에 같은 이름의 프로젝트와 `Program.cs`이라는 이름의 파일이 생성됩니다. 프로그램에는 `Hello World!` 출력을 표시하는 코드가 포함되어 있습니다.

1. NuGet 패키지 관리자를 사용하여 .NET용 QLDB 드라이버를 설치합니다. 프로젝트에 종속성을 추가하려면 Visual Studio 또는 선택한 IDE를 사용하는 것이 좋습니다. 드라이버 패키지 이름은 [Amazon.QLDB.Driver](https://www.nuget.org/packages/amazon.qldb.driver)입니다.
   + 예를 들어 Visual Studio의 경우, **도구** 메뉴에서 **NuGet 패키지 관리자 콘솔**을 엽니다. 그런 다음 `PM>` 프롬프트에 다음 명령을 입력합니다.

     ```
     PM> Install-Package Amazon.QLDB.Driver
     ```
   + 또는 터미널에서 다음 명령을 입력할 수 있습니다.

     ```
     $ cd Amazon.QLDB.QuickStartGuide
     $ dotnet add package Amazon.QLDB.Driver
     ```

   드라이버를 설치하면 [AWS SDK for .NET](https://aws.amazon.com/sdk-for-net)및 [Amazon Ion](ion.md) 라이브러리를 비롯한 해당 종속 항목도 설치됩니다.

1. 드라이버의 직렬화 라이브러리를 설치합니다.

   ```
   PM> Install-Package Amazon.QLDB.Driver.Serialization
   ```

1. `Program.cs` 파일을 엽니다.

   그런 다음, 다음 단계의 코드 예를 점진적으로 추가하여 몇 가지 기본 CRUD 작업을 시도해 보세요. 또는 단계별 자습서를 건너뛰고 [전체 애플리케이션](#driver-quickstart-dotnet.complete)을 실행할 수도 있습니다.

**참고**  
**동기식 API와 비동기식 API 선택** - 드라이버는 동기식 및 비동기식 API를 제공합니다. 여러 요청을 차단하지 않고 처리하는 수요가 많은 애플리케이션의 경우, 성능 향상을 위해 비동기식 API를 사용하는 것이 좋습니다. 드라이버는 동기식으로 작성된 기존 코드 베이스의 편의성을 높이기 위해 동기식 API를 제공합니다.  
이 자습서에는 동기식 및 비동기식 코드 예제가 모두 포함되어 있습니다. API에 대한 자세한 내용은 API 설명서의 [IQldbDriver](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.IQldbDriver.html) 및 [IAsyncQldbDriver](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.IAsyncQldbDriver.html) 인터페이스를 참조하세요.
**Amazon Ion 데이터 처리** - 이 자습서에서는 기본적으로 [Ion 객체 매퍼](https://github.com/amzn/ion-object-mapper-dotnet)를 사용하여 Amazon Ion 데이터를 처리하는 코드 예제를 제공합니다. QLDB는 .NET 드라이버 버전 1.3.0에서 Ion 객체 매퍼를 도입했습니다. 해당하는 경우, 이 자습서에서는 표준 [Ion 라이브러리](https://github.com/amzn/ion-dotnet)를 대안으로 사용하는 코드 예제도 제공합니다. 자세한 내용은 [Amazon Ion 작업](driver-cookbook-dotnet.md#cookbook-dotnet.ion) 섹션을 참조하세요.

## 2단계: 드라이버 초기화
<a name="driver-quickstart-dotnet.step-2"></a>

`quick-start`라는 명칭의 원장에 연결되는 드라이버의 인스턴스를 초기화합니다. 다음 코드를 `Program.cs` 파일에 추가합니다.

------
#### [ Async ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();
        }
    }
}
```

------
#### [ Sync ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();
        }
    }
}
```

------

### Ion 라이브러리 사용
<a name="driver-quickstart-dotnet.step-2.ion-library"></a>

------
#### [ Async ]

```
using System;
using System.Threading.Tasks;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();
        }
    }
}
```

------
#### [ Sync ]

```
using System;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB Driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();
        }
    }
}
```

------

## 3단계: 테이블 및 인덱스 생성
<a name="driver-quickstart-dotnet.step-3"></a>

이 자습서의 나머지 부분부터 *6단계*까지는 이전 코드 예제에 다음 코드 예제를 추가해야 합니다.

이 단계에서 다음 코드는 `CREATE TABLE` 및 `CREATE INDEX` 문을 실행하는 방법을 보여줍니다. `Person`라는 이름의 테이블과 해당 테이블의 `firstName` 필드에 대한 인덱스를 생성합니다. [인덱스](ql-reference.create-index.md)는 쿼리 성능을 최적화하고 [OCC(낙관적 동시성 제어)](concurrency.md) 충돌 예외를 제한하는 데 필요합니다.

------
#### [ Async ]

```
Console.WriteLine("Creating the table and index");

// Creates the table and the index in the same transaction.
// Note: Any code within the lambda can potentially execute multiple times due to retries.
// For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
await driver.Execute(async txn =>
{
    await txn.Execute("CREATE TABLE Person");
    await txn.Execute("CREATE INDEX ON Person(firstName)");
});
```

------
#### [ Sync ]

```
Console.WriteLine("Creating the tables and index");

// Creates the table and the index in the same transaction.
// Note: Any code within the lambda can potentially execute multiple times due to retries.
// For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
driver.Execute(txn =>
{
    txn.Execute("CREATE TABLE Person");
    txn.Execute("CREATE INDEX ON Person(firstName)");
});
```

------

## 4단계: 문서 삽입
<a name="driver-quickstart-dotnet.step-4"></a>

다음 코드 예에서는 `INSERT` 문을 실행하는 방법을 보여줍니다. QLDB는 [PartiQL](ql-reference.md) 쿼리 언어(SQL 호환) 및 [Amazon Ion](ion.md) 데이터 형식(JSON의 상위 집합)을 지원합니다.

`Person` 테이블에 문서를 삽입하는 다음 코드를 추가합니다.

------
#### [ Async ]

```
Console.WriteLine("Inserting a document");

Person myPerson = new Person {
    FirstName = "John",
    LastName = "Doe",
    Age = 32
};

await driver.Execute(async txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
    await txn.Execute(myQuery);
});
```

------
#### [ Sync ]

```
Console.WriteLine("Inserting a document");

Person myPerson = new Person {
    FirstName = "John",
    LastName = "Doe",
    Age = 32
};

driver.Execute(txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
    txn.Execute(myQuery);
});
```

------

### Ion 라이브러리 사용
<a name="driver-quickstart-dotnet.step-4.ion-library"></a>

------
#### [ Async ]

```
Console.WriteLine("Inserting a document");

// This is one way of creating Ion values. We can also use an IonLoader.
// For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("firstName", valueFactory.NewString("John"));
ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
ionPerson.SetField("age", valueFactory.NewInt(32));

await driver.Execute(async txn =>
{
    await txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------
#### [ Sync ]

```
Console.WriteLine("Inserting a document");

// This is one way of creating Ion values, we can also use an IonLoader.
// For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("firstName", valueFactory.NewString("John"));
ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
ionPerson.SetField("age", valueFactory.NewInt(32));

driver.Execute(txn =>
{
    txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------

**작은 정보**  
단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [Ion 목록](driver-working-with-ion.md#driver-ion-list) 타입 파라미터를 해당 에 전달할 수 있습니다.  

```
// people is an Ion list
txn.Execute("INSERT INTO Person ?", people);
```
Ion 목록을 전달할 때는 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호(`<<...>>`)로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

## 5단계: 문서 쿼리
<a name="driver-quickstart-dotnet.step-5"></a>

다음 코드 예에서는 `SELECT` 문을 실행하는 방법을 보여줍니다.

`Person` 테이블에서 문서를 쿼리하는 다음 코드를 추가합니다.

------
#### [ Async ]

```
Console.WriteLine("Querying the table");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IAsyncResult<Person> selectResult = await driver.Execute(async txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
    return await txn.Execute(myQuery);
});

await foreach (Person person in selectResult)
{
    Console.WriteLine(person);
    // John, Doe, 32
}
```

------
#### [ Sync ]

```
Console.WriteLine("Querying the table");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IResult<Person> selectResult = driver.Execute(txn =>
{
    IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
    return txn.Execute(myQuery);
});

foreach (Person person in selectResult)
{
    Console.WriteLine(person);
    // John, Doe, 32
}
```

------

### Ion 라이브러리 사용
<a name="driver-quickstart-dotnet.step-5.ion-library"></a>

------
#### [ Async ]

```
Console.WriteLine("Querying the table");

IIonValue ionFirstName = valueFactory.NewString("John");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IAsyncResult selectResult = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
});

await foreach (IIonValue row in selectResult)
{
    Console.WriteLine(row.GetField("firstName").StringValue);
    Console.WriteLine(row.GetField("lastName").StringValue);
    Console.WriteLine(row.GetField("age").IntValue);
}
```

------
#### [ Sync ]

```
Console.WriteLine("Querying the table");

IIonValue ionFirstName = valueFactory.NewString("John");

// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IResult selectResult = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
});

foreach (IIonValue row in selectResult)
{
    Console.WriteLine(row.GetField("firstName").StringValue);
    Console.WriteLine(row.GetField("lastName").StringValue);
    Console.WriteLine(row.GetField("age").IntValue);
}
```

------

이 예에서는 물음표(`?`)를 변수 자리 표시자로 사용하여 문서 정보를 해당 문에 전달합니다. 자리 표시자를 사용할 때는 `IonValue` 타입의 값을 전달해야 합니다.

## 6단계: 문서 업데이트
<a name="driver-quickstart-dotnet.step-6"></a>

다음 코드 예에서는 `UPDATE` 문을 실행하는 방법을 보여줍니다.

1. `age`를 42로 업데이트하여 `Person` 테이블의 문서를 업데이트하는 다음 코드를 추가합니다.

------
#### [ Async ]

   ```
   Console.WriteLine("Updating the document");
   
   await driver.Execute(async txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
       await txn.Execute(myQuery);
   });
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Updating the document");
   
   driver.Execute(txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
       txn.Execute(myQuery);
   });
   ```

------

1. 문서를 다시 쿼리하여 업데이트된 값을 확인합니다.

------
#### [ Async ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IAsyncResult<Person> updateResult = await driver.Execute(async txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
       return await txn.Execute(myQuery);
   });
   
   await foreach (Person person in updateResult)
   {
       Console.WriteLine(person);
       // John, Doe, 42
   }
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IResult<Person> updateResult = driver.Execute(txn =>
   {
       IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
       return txn.Execute(myQuery);
   });
   
   foreach (Person person in updateResult)
   {
       Console.WriteLine(person);
       // John, Doe, 42
   }
   ```

------

1. 애플리케이션을 실행하려면 `Amazon.QLDB.QuickStartGuide` 프로젝트 디렉터리의 상위 디렉터리에서 다음 명령을 입력합니다.

   ```
   $ dotnet run --project Amazon.QLDB.QuickStartGuide
   ```

### Ion 라이브러리 사용
<a name="driver-quickstart-dotnet.step-6.ion-library"></a>

1. `age`를 42로 업데이트하여 `Person` 테이블의 문서를 업데이트하는 다음 코드를 추가합니다.

------
#### [ Async ]

   ```
   Console.WriteLine("Updating the document");
   
   IIonValue ionIntAge = valueFactory.NewInt(42);
   IIonValue ionFirstName2 = valueFactory.NewString("John");
   
   await driver.Execute(async txn =>
   {
       await txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
   });
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Updating a document");
   
   IIonValue ionIntAge = valueFactory.NewInt(42);
   IIonValue ionFirstName2 = valueFactory.NewString("John");
   
   driver.Execute(txn =>
   {
       txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
   });
   ```

------

1. 문서를 다시 쿼리하여 업데이트된 값을 확인합니다.

------
#### [ Async ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IIonValue ionFirstName3 = valueFactory.NewString("John");
   
   IAsyncResult updateResult = await driver.Execute(async txn =>
   {
       return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3 );
   });
   
   await foreach (IIonValue row in updateResult)
   {
       Console.WriteLine(row.GetField("firstName").StringValue);
       Console.WriteLine(row.GetField("lastName").StringValue);
       Console.WriteLine(row.GetField("age").IntValue);
   }
   ```

------
#### [ Sync ]

   ```
   Console.WriteLine("Querying the table for the updated document");
   
   IIonValue ionFirstName3 = valueFactory.NewString("John");
   
   IResult updateResult = driver.Execute(txn =>
   {
       return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3);
   });
   
   foreach (IIonValue row in updateResult)
   {
       Console.WriteLine(row.GetField("firstName").StringValue);
       Console.WriteLine(row.GetField("lastName").StringValue);
       Console.WriteLine(row.GetField("age").IntValue);
   }
   ```

------

1. 애플리케이션을 실행하려면 `Amazon.QLDB.QuickStartGuide` 프로젝트 디렉터리의 상위 디렉터리에서 다음 명령을 입력합니다.

   ```
   $ dotnet run --project Amazon.QLDB.QuickStartGuide
   ```

## 전체 애플리케이션 실행
<a name="driver-quickstart-dotnet.complete"></a>

다음 코드 예는 `Program.cs` 애플리케이션의 전체 버전입니다. 이전 단계를 개별적으로 수행하는 대신 이 코드 예를 처음부터 끝까지 복사하여 실행할 수도 있습니다. 이 애플리케이션은 `quick-start`이라는 명칭의 원장에 대한 몇 가지 기본 CRUD 작업을 보여줍니다.

**참고**  
이 코드를 실행하기 전에 `quick-start` 원장에 `Person`이라는 명칭의 활성 테이블이 아직 없는지 확인하세요.

------
#### [ Async ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();

            Console.WriteLine("Creating the table and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            await driver.Execute(async txn =>
            {
                await txn.Execute("CREATE TABLE Person");
                await txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            Person myPerson = new Person {
                FirstName = "John",
                LastName = "Doe",
                Age = 32
            };

            await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
                await txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IAsyncResult<Person> selectResult = await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return await txn.Execute(myQuery);
            });

            await foreach (Person person in selectResult)
            {
                Console.WriteLine(person);
                // John, Doe, 32
            }

            Console.WriteLine("Updating the document");

            await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
                await txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table for the updated document");

            IAsyncResult<Person> updateResult = await driver.Execute(async txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return await txn.Execute(myQuery);
            });

            await foreach (Person person in updateResult)
            {
                Console.WriteLine(person);
                // John, Doe, 42
            }
        }
    }
}
```

------
#### [ Sync ]

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        public class Person
        {
            public string FirstName { get; set; }

            public string LastName { get; set; }

            public int Age { get; set; }

            public override string ToString()
            {
                return FirstName + ", " + LastName + ", " + Age.ToString();
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .WithSerializer(new ObjectSerializer())
                .Build();

            Console.WriteLine("Creating the table and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            driver.Execute(txn =>
            {
                txn.Execute("CREATE TABLE Person");
                txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            Person myPerson = new Person {
                FirstName = "John",
                LastName = "Doe",
                Age = 32
            };

            driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("INSERT INTO Person ?", myPerson);
                txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IResult<Person> selectResult = driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return txn.Execute(myQuery);
            });

            foreach (Person person in selectResult)
            {
                Console.WriteLine(person);
                // John, Doe, 32
            }

            Console.WriteLine("Updating the document");

            driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("UPDATE Person SET Age = ? WHERE FirstName = ?", 42, "John");
                txn.Execute(myQuery);
            });

            Console.WriteLine("Querying the table for the updated document");

            IResult<Person> updateResult = driver.Execute(txn =>
            {
                IQuery<Person> myQuery = txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "John");
                return txn.Execute(myQuery);
            });

            foreach (Person person in updateResult)
            {
                Console.WriteLine(person);
                // John, Doe, 42
            }

        }
    }
}
```

------

### Ion 라이브러리 사용
<a name="driver-quickstart-dotnet.complete.ion-library"></a>

------
#### [ Async ]

```
using System;
using System.Threading.Tasks;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static async Task Main(string[] args)
        {
            Console.WriteLine("Create the async QLDB driver");
            IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();

            Console.WriteLine("Creating the table and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            await driver.Execute(async txn =>
            {
                await txn.Execute("CREATE TABLE Person");
                await txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            // This is one way of creating Ion values. We can also use an IonLoader.
            // For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
            IIonValue ionPerson = valueFactory.NewEmptyStruct();
            ionPerson.SetField("firstName", valueFactory.NewString("John"));
            ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
            ionPerson.SetField("age", valueFactory.NewInt(32));

            await driver.Execute(async txn =>
            {
                await txn.Execute("INSERT INTO Person ?", ionPerson);
            });

            Console.WriteLine("Querying the table");

            IIonValue ionFirstName = valueFactory.NewString("John");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IAsyncResult selectResult = await driver.Execute(async txn =>
            {
                return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
            });

            await foreach (IIonValue row in selectResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }

            Console.WriteLine("Updating the document");

            IIonValue ionIntAge = valueFactory.NewInt(42);
            IIonValue ionFirstName2 = valueFactory.NewString("John");

            await driver.Execute(async txn =>
            {
                await txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
            });

            Console.WriteLine("Querying the table for the updated document");

            IIonValue ionFirstName3 = valueFactory.NewString("John");

            IAsyncResult updateResult = await driver.Execute(async txn =>
            {
                return await txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3);
            });

            await foreach (IIonValue row in updateResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }
        }
    }
}
```

------
#### [ Sync ]

```
using System;
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;
using Amazon.QLDB.Driver;

namespace Amazon.QLDB.QuickStartGuide
{
    class Program
    {
        static IValueFactory valueFactory = new ValueFactory();

        static void Main(string[] args)
        {
            Console.WriteLine("Create the sync QLDB Driver");
            IQldbDriver driver = QldbDriver.Builder()
                .WithLedger("quick-start")
                .Build();

            Console.WriteLine("Creating the tables and index");

            // Creates the table and the index in the same transaction.
            // Note: Any code within the lambda can potentially execute multiple times due to retries.
            // For more information, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-retry-policy
            driver.Execute(txn =>
            {
                txn.Execute("CREATE TABLE Person");
                txn.Execute("CREATE INDEX ON Person(firstName)");
            });

            Console.WriteLine("Inserting a document");

            // This is one way of creating Ion values. We can also use an IonLoader.
            // For more details, see: https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-dotnet.html#cookbook-dotnet.ion
            IIonValue ionPerson = valueFactory.NewEmptyStruct();
            ionPerson.SetField("firstName", valueFactory.NewString("John"));
            ionPerson.SetField("lastName", valueFactory.NewString("Doe"));
            ionPerson.SetField("age", valueFactory.NewInt(32));

            driver.Execute(txn =>
            {
                txn.Execute("INSERT INTO Person ?", ionPerson);
            });

            Console.WriteLine("Querying the table");

            IIonValue ionFirstName = valueFactory.NewString("John");

            // The result from driver.Execute() is buffered into memory because once the
            // transaction is committed, streaming the result is no longer possible.
            IResult selectResult = driver.Execute(txn =>
            {
                return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName);
            });

            foreach (IIonValue row in selectResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }

            Console.WriteLine("Updating a document");

            IIonValue ionIntAge = valueFactory.NewInt(42);
            IIonValue ionFirstName2 = valueFactory.NewString("John");

            driver.Execute(txn =>
            {
                txn.Execute("UPDATE Person SET age = ? WHERE firstName = ?", ionIntAge, ionFirstName2);
            });

            Console.WriteLine("Querying the table for the updated document");

            IIonValue ionFirstName3 = valueFactory.NewString("John");

            IResult updateResult = driver.Execute(txn =>
            {
                return txn.Execute("SELECT * FROM Person WHERE firstName = ?", ionFirstName3);
            });

            foreach (IIonValue row in updateResult)
            {
                Console.WriteLine(row.GetField("firstName").StringValue);
                Console.WriteLine(row.GetField("lastName").StringValue);
                Console.WriteLine(row.GetField("age").IntValue);
            }
        }
    }
}
```

------

전체 애플리케이션을 실행하려면 `Amazon.QLDB.QuickStartGuide` 프로젝트 디렉터리의 상위 디렉터리에서 다음 명령을 입력합니다.

```
$ dotnet run --project Amazon.QLDB.QuickStartGuide
```

# .NET용 Amazon QLDB 드라이버 - Cookbook 참조
<a name="driver-cookbook-dotnet"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 참조 가이드는 .NET용 Amazon QLDB 드라이버의 일반적인 사용 사례를 보여줍니다. 이 C\$1 코드 예제는 드라이버를 사용하여 기본 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행하는 방법을 보여줍니다. 또한 Amazon Ion 데이터를 처리하기 위한 코드 예제도 포함되어 있습니다. 또한 이 가이드에서는 트랜잭션에 멱등성을 부여하고 고유성 제약하는 모범 사례를 중점적으로 설명합니다.

**참고**  
이 주제는 기본적으로 [Ion 객체 매퍼](https://github.com/amzn/ion-object-mapper-dotnet)를 사용하여 Amazon Ion 데이터를 처리하는 코드 예제를 제공합니다. QLDB는 .NET 드라이버 버전 1.3.0에서 Ion 객체 매퍼를 도입했습니다. 해당하는 경우, 이 주제에서는 표준 [Ion 라이브러리](https://github.com/amzn/ion-dotnet)를 대안으로 사용하는 코드 예제도 제공합니다. 자세한 내용은 [Amazon Ion 작업](#cookbook-dotnet.ion) 섹션을 참조하세요.

**Contents**
+ [

## 드라이버 가져오기
](#cookbook-dotnet.importing)
+ [

## 드라이버 인스턴스화
](#cookbook-dotnet.instantiating)
+ [

## CRUD 작업
](#cookbook-dotnet.crud)
  + [

### 테이블 생성
](#cookbook-dotnet.crud.creating-tables)
  + [

### 인덱스 생성
](#cookbook-dotnet.crud.creating-indexes)
  + [

### 문서 읽기
](#cookbook-dotnet.crud.reading)
    + [

#### 쿼리 파라미터 사용
](#cookbook-dotnet.reading-using-params)
  + [

### 문서 삽입하기
](#cookbook-dotnet.crud.inserting)
    + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-dotnet.crud.inserting.multiple)
  + [

### 문서 업데이트
](#cookbook-dotnet.crud.updating)
  + [

### 문서 삭제
](#cookbook-dotnet.crud.deleting)
  + [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-dotnet.crud.multi-statement)
  + [

### 재시도 로직
](#cookbook-dotnet.crud.retry-logic)
  + [

### 고유성 제약 조건 구현
](#cookbook-dotnet.crud.uniqueness-constraints)
+ [

## Amazon Ion 작업
](#cookbook-dotnet.ion)
  + [

### Ion 모듈 가져오기
](#cookbook-dotnet.ion.import)
  + [

### Ion 유형 생성
](#cookbook-dotnet.ion.creating-types)
  + [

### Ion 이진 덤프 가져오기
](#cookbook-dotnet.ion.getting-binary)
  + [

### Ion 텍스트 덤프 가져오기
](#cookbook-dotnet.ion.getting-text)

## 드라이버 가져오기
<a name="cookbook-dotnet.importing"></a>

다음 코드 예제에서는 드라이브를 가져옵니다.

```
using Amazon.QLDB.Driver;
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;
```

### Ion 라이브러리 사용
<a name="cookbook-dotnet.importing.ion-library"></a>

```
using Amazon.QLDB.Driver;
using Amazon.IonDotnet.Builders;
```

## 드라이버 인스턴스화
<a name="cookbook-dotnet.instantiating"></a>

다음 코드 예제는 기본 설정을 사용하여 지정된 원장 이름에 연결하는 드라이버 인스턴스를 만듭니다.

------
#### [ Async ]

```
IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
    .WithLedger("vehicle-registration")
    // Add Serialization library
    .WithSerializer(new ObjectSerializer())
    .Build();
```

------
#### [ Sync ]

```
IQldbDriver driver = QldbDriver.Builder()
    .WithLedger("vehicle-registration")
    // Add Serialization library
    .WithSerializer(new ObjectSerializer())
    .Build();
```

------

### Ion 라이브러리 사용
<a name="cookbook-dotnet.instantiating.ion-library"></a>

------
#### [ Async ]

```
IAsyncQldbDriver driver = AsyncQldbDriver.Builder().WithLedger("vehicle-registration").Build();
```

------
#### [ Sync ]

```
IQldbDriver driver = QldbDriver.Builder().WithLedger("vehicle-registration").Build();
```

------

## CRUD 작업
<a name="cookbook-dotnet.crud"></a>

QLDB는 트랜잭션의 일부로 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행합니다.

**주의**  
가장 좋은 방법은 쓰기 트랜잭션이 완전한 멱등성을 부여하는 것입니다.

**트랜잭션에 멱등성 부여하기**

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 좋습니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 *멱등성*을 가집니다.

이름이 `Person`인 테이블에 문서를 삽입하는 트랜잭션을 예로 들어 보겠습니다. 트랜잭션은 먼저 문서가 테이블에 이미 존재하는지 여부를 확인해야 합니다. 이렇게 확인하지 않으면 테이블에 문서가 중복될 수 있습니다.

QLDB가 서버 측에서 트랜잭션을 성공적으로 커밋했지만 응답을 기다리는 동안 클라이언트 제한 시간이 초과되었다고 가정해 보겠습니다. 트랜잭션이 멱등성을 가지지 않는 경우 재시도 시 동일한 문서가 두 번 이상 삽입될 수 있습니다.

**인덱스를 사용하여 전체 테이블 스캔 방지**

인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 이 인덱싱된 조회가 없으면 QLDB는 테이블 스캔을 수행해야 하며, 이로 인해 트랜잭션 제한 시간이 초과되거나 *OCC(낙관적 동시성 제어)* 충돌이 발생할 수 있습니다.

OCC에 대한 자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md) 단원을 참조하세요.

**암시적으로 생성된 트랜잭션**

[Amazon.QLDB.Driver.IQldbDriver.Execute](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.IQldbDriver.html) 메서드는 [Amazon.QLDB.Driver.TransactionExecutor](https://amazon-qldb-docs.s3.amazonaws.com/drivers/dotnet/1.4.1/api/Amazon.QLDB.Driver.TransactionExecutor.html)의 인스턴스를 수신하는 Lambda 함수를 허용하며, 이 인스턴스를 사용하여 명령문을 실행할 수 있습니다. `TransactionExecutor`의 인스턴스는 암시적으로 생성된 트랜잭션을 래핑합니다.

트랜잭션 실행자의 `Execute` 메서드를 사용하여 Lambda 함수 내에서 명령문을 실행할 수 있습니다. 드라이버는 Lambda 함수가 반환될 때 트랜잭션을 암시적으로 커밋합니다.

다음 섹션에서는 기본 CRUD 작업을 실행하고, 사용자 지정 재시도 로직을 지정하고, 고유성 제약 조건을 구현하는 방법을 보여줍니다.

**Contents**
+ [

### 테이블 생성
](#cookbook-dotnet.crud.creating-tables)
+ [

### 인덱스 생성
](#cookbook-dotnet.crud.creating-indexes)
+ [

### 문서 읽기
](#cookbook-dotnet.crud.reading)
  + [

#### 쿼리 파라미터 사용
](#cookbook-dotnet.reading-using-params)
+ [

### 문서 삽입하기
](#cookbook-dotnet.crud.inserting)
  + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-dotnet.crud.inserting.multiple)
+ [

### 문서 업데이트
](#cookbook-dotnet.crud.updating)
+ [

### 문서 삭제
](#cookbook-dotnet.crud.deleting)
+ [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-dotnet.crud.multi-statement)
+ [

### 재시도 로직
](#cookbook-dotnet.crud.retry-logic)
+ [

### 고유성 제약 조건 구현
](#cookbook-dotnet.crud.uniqueness-constraints)

### 테이블 생성
<a name="cookbook-dotnet.crud.creating-tables"></a>

------
#### [ Async ]

```
IAsyncResult<Table> createResult = await driver.Execute(async txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE TABLE Person");
    return await txn.Execute(query);
});

await foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the created table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------
#### [ Sync ]

```
IResult<Table> createResult = driver.Execute( txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE TABLE Person");
    return txn.Execute(query);
});

foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the created table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.creating-tables.ion-library"></a>

------
#### [ Async ]

```
// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("CREATE TABLE Person");
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------
#### [ Sync ]

```
// The result from driver.Execute() is buffered into memory because once the
// transaction is committed, streaming the result is no longer possible.
IResult result = driver.Execute(txn =>
{
    return txn.Execute("CREATE TABLE Person");
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------

### 인덱스 생성
<a name="cookbook-dotnet.crud.creating-indexes"></a>

------
#### [ Async ]

```
IAsyncResult<Table> createResult = await driver.Execute(async txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE INDEX ON Person(firstName)");
    return await txn.Execute(query);
});

await foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the updated table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------
#### [ Sync ]

```
IResult<Table> createResult = driver.Execute(txn =>
{
    IQuery<Table> query = txn.Query<Table>("CREATE INDEX ON Person(firstName)");
    return txn.Execute(query);
});

foreach (var result in createResult)
{
    Console.WriteLine("{ tableId: " + result.TableId + " }");
    // The statement returns the updated table ID:
    // { tableId: 4o5Uk09OcjC6PpJpLahceE }
}
```

------

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.creating-indexes.ion-library"></a>

------
#### [ Async ]

```
IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("CREATE INDEX ON Person(GovId)");
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------
#### [ Sync ]

```
IResult result = driver.Execute(txn =>
{
    return txn.Execute("CREATE INDEX ON Person(GovId)");
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated table ID:
    // {
    //    tableId: "4o5Uk09OcjC6PpJpLahceE"
    // }
}
```

------

### 문서 읽기
<a name="cookbook-dotnet.crud.reading"></a>

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// Person class is defined as follows:
// public class Person
// {
//     public string GovId { get; set; }
//     public string FirstName { get; set; }
//  }

IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'"));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.GovId); // Prints TOYENC486FH.
    Console.WriteLine(person.FirstName); // Prints Brent.
}
```

**참고**  
인덱싱된 조회 없이 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. 이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 [인덱스](ql-reference.create-index.md)를 사용하는 것이 좋습니다. `GovId`에 인덱스를 사용하지 않으면 쿼리에 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

#### 쿼리 파라미터 사용
<a name="cookbook-dotnet.reading-using-params"></a>

다음 코드 예제에서는 C\$1 형식 쿼리 파라미터를 사용합니다.

```
IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE FirstName = ?", "Brent"));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.GovId); // Prints TOYENC486FH.
    Console.WriteLine(person.FirstName); // Prints Brent.
}
```

다음 코드 예제에서는 여러 C\$1 형식 쿼리 파라미터를 사용합니다.

```
IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", "TOYENC486FH", "Brent"));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.GovId); // Prints TOYENC486FH.
    Console.WriteLine(person.FirstName); // Prints Brent.
}
```

다음 코드 예제에서는 C\$1 형식 쿼리 파라미터의 배열을 사용합니다.

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" }
// { "GovId": "YH844DA7LDB", "FirstName" : "Mary" }

string[] ids = {
    "TOYENC486FH",
    "ROEE1C1AABH",
    "YH844DA7LDB"
};

IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId IN (?,?,?)", ids));
});

await foreach (Person person in result)
{
    Console.WriteLine(person.FirstName); // Prints Brent on first iteration.
    // Prints Jim on second iteration.
    // Prints Mary on third iteration.
}
```

다음 코드 예제에서는 C\$1 목록을 값으로 사용합니다.

```
// Assumes that Person table has document as follows:
// { "GovId": "TOYENC486FH",
//   "FirstName" : "Brent",
//   "Vehicles": [
//      { "Make": "Volkswagen",
//        "Model": "Golf"},
//      { "Make": "Honda",
//        "Model": "Civic"}
//   ]
// }
// Person class is defined as follows:
// public class Person
// {
//     public string GovId { get; set; }
//     public string FirstName { get; set; }
//     public List<Vehicle> Vehicles { get; set; }
// }
// Vehicle class is defined as follows:
// public class Vehicle
// {
//     public string Make { get; set; }
//     public string Model { get; set; }
// }

List<Vehicle> vehicles = new List<Vehicle>
{
    new Vehicle
    {
        Make = "Volkswagen",
        Model = "Golf"
    },
    new Vehicle
    {
        Make = "Honda",
        Model = "Civic"
    }
};

IAsyncResult<Person> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE Vehicles = ?", vehicles));
});

await foreach (Person person in result)
{
    Console.WriteLine("{");
    Console.WriteLine($"  GovId: {person.GovId},");
    Console.WriteLine($"  FirstName: {person.FirstName},");
    Console.WriteLine("  Vehicles: [");
    foreach (Vehicle vehicle in person.Vehicles)
    {
        Console.WriteLine("  {");
        Console.WriteLine($"    Make: {vehicle.Make},");
        Console.WriteLine($"    Model: {vehicle.Model},");
        Console.WriteLine("  },");
    }
    Console.WriteLine("  ]");
    Console.WriteLine("}");
    // Prints:
    // {
    //     GovId: TOYENC486FH,
    //     FirstName: Brent,
    //     Vehicles: [
    //     {
    //         Make: Volkswagen,
    //         Model: Golf
    //     },
    //     {
    //         Make: Honda,
    //         Model: Civic
    //     },
    //     ]
    // }
}
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.reading.ion-library"></a>

------
#### [ Async ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'");
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------
#### [ Sync ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'");
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------

**참고**  
인덱싱된 조회 없이 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. 이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 [인덱스](ql-reference.create-index.md)를 사용하는 것이 좋습니다. `GovId`에 인덱스를 사용하지 않으면 쿼리에 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

다음 코드 예제는 Ion 유형 쿼리 파라미터를 사용합니다.

------
#### [ Async ]

```
IValueFactory valueFactory = new ValueFactory();
IIonValue ionFirstName = valueFactory.NewString("Brent");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE FirstName = ?", ionFirstName);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------
#### [ Sync ]

```
IValueFactory valueFactory = new ValueFactory();
IIonValue ionFirstName = valueFactory.NewString("Brent");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE FirstName = ?", ionFirstName);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------

다음 코드 예제는 여러 쿼리 파라미터를 사용합니다.

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("Brent");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", ionGovId, ionFirstName);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("Brent");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", ionGovId, ionFirstName);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("GovId").StringValue); // Prints TOYENC486FH.
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent.
}
```

------

다음 코드 예제는 쿼리 파라미터 목록을 사용합니다.

------
#### [ Async ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" }
// { "GovId": "YH844DA7LDB", "FirstName" : "Mary" }

IIonValue[] ionIds = {
    valueFactory.NewString("TOYENC486FH"),
    valueFactory.NewString("ROEE1C1AABH"),
    valueFactory.NewString("YH844DA7LDB")
};

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", ionIds);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent on first iteration.
                                                              // Prints Jim on second iteration.
                                                              // Prints Mary on third iteration.
}
```

------
#### [ Sync ]

```
// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName" : "Brent" }
// { "GovId": "ROEE1C1AABH", "FirstName" : "Jim" }
// { "GovId": "YH844DA7LDB", "FirstName" : "Mary" }

IIonValue[] ionIds = {
    valueFactory.NewString("TOYENC486FH"),
    valueFactory.NewString("ROEE1C1AABH"),
    valueFactory.NewString("YH844DA7LDB")
};

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", ionIds);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.GetField("FirstName").StringValue); // Prints Brent on first iteration.
                                                              // Prints Jim on second iteration.
                                                              // Prints Mary on third iteration.
}
```

------

다음 코드 예제에서는 Ion 목록을 값으로 사용합니다. 다른 Ion 유형 사용에 더 알아보려면 [Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업](driver-working-with-ion.md) 단원을 참조하세요.

------
#### [ Async ]

```
// Assumes that Person table has document as follows:
// { "GovId": "TOYENC486FH",
//   "FirstName" : "Brent",
//   "Vehicles": [
//      { "Make": "Volkswagen",
//        "Model": "Golf"},
//      { "Make": "Honda",
//        "Model": "Civic"}
//   ]
// }

IIonValue ionVehicle1 = valueFactory.NewEmptyStruct();
ionVehicle1.SetField("Make", valueFactory.NewString("Volkswagen"));
ionVehicle1.SetField("Model", valueFactory.NewString("Golf"));

IIonValue ionVehicle2 = valueFactory.NewEmptyStruct();
ionVehicle2.SetField("Make", valueFactory.NewString("Honda"));
ionVehicle2.SetField("Model", valueFactory.NewString("Civic"));

IIonValue ionVehicles =  valueFactory.NewEmptyList();
ionVehicles.Add(ionVehicle1);
ionVehicles.Add(ionVehicle2);

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("SELECT * FROM Person WHERE Vehicles = ?", ionVehicles);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // Prints:
    // {
    //     GovId: "TOYENC486FN",
    //     FirstName: "Brent",
    //     Vehicles: [
    //     {
    //         Make: "Volkswagen",
    //         Model: "Golf"
    //     },
    //     {
    //         Make: "Honda",
    //         Model: "Civic"
    //     }
    //     ]
    // }
}
```

------
#### [ Sync ]

```
// Assumes that Person table has document as follows:
// { "GovId": "TOYENC486FH",
//   "FirstName" : "Brent",
//   "Vehicles": [
//      { "Make": "Volkswagen",
//        "Model": "Golf"},
//      { "Make": "Honda",
//        "Model": "Civic"}
//   ]
// }

IIonValue ionVehicle1 = valueFactory.NewEmptyStruct();
ionVehicle1.SetField("Make", valueFactory.NewString("Volkswagen"));
ionVehicle1.SetField("Model", valueFactory.NewString("Golf"));

IIonValue ionVehicle2 = valueFactory.NewEmptyStruct();
ionVehicle2.SetField("Make", valueFactory.NewString("Honda"));
ionVehicle2.SetField("Model", valueFactory.NewString("Civic"));

IIonValue ionVehicles =  valueFactory.NewEmptyList();
ionVehicles.Add(ionVehicle1);
ionVehicles.Add(ionVehicle2);

IResult result = driver.Execute(txn =>
{
    return txn.Execute("SELECT * FROM Person WHERE Vehicles = ?", ionVehicles);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // Prints:
    // {
    //     GovId: "TOYENC486FN",
    //     FirstName: "Brent",
    //     Vehicles: [
    //     {
    //         Make: "Volkswagen",
    //         Model: "Golf"
    //     },
    //     {
    //         Make: "Honda",
    //         Model: "Civic"
    //     }
    //     ]
    // }
}
```

------

### 문서 삽입하기
<a name="cookbook-dotnet.crud.inserting"></a>

다음 코드 예제는 Ion 데이터 유형을 삽입합니다.

```
string govId = "TOYENC486FH";

Person person = new Person
{
    GovId = "TOYENC486FH",
    FirstName = "Brent"
};

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult<Person> result = await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ?", govId));

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", person));
});
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.inserting.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult result = await txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

driver.Execute(txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IResult result = txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = result.Count();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------

이 트랜잭션은 문서를 `Person` 테이블에 삽입합니다. 삽입하기 전에 먼저 문서가 테이블에 이미 있는지 확인합니다. **이 검사를 통해 트랜잭션은 본질적으로 멱등성을 가지게 됩니다.** 이 트랜잭션을 여러 번 실행하더라도 의도하지 않은 부작용이 발생하지는 않습니다.

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

#### 하나의 명령문에 여러 문서 삽입
<a name="cookbook-dotnet.crud.inserting.multiple"></a>

단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 문에 C\$1 `List` 파라미터를 전달할 수 있습니다.

```
Person person1 = new Person
{
    FirstName = "Brent",
    GovId = "TOYENC486FH"
};

Person person2 = new Person
{
    FirstName = "Jim",
    GovId = "ROEE1C1AABH"
};

List<Person> people = new List<Person>();
people.Add(person1);
people.Add(person2);

IAsyncResult<Document> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", people));
});

await foreach (Document row in result)
{
    Console.WriteLine("{ documentId: " + row.DocumentId + " }");
    // The statement returns the created documents' ID:
    // { documentId: 6BFt5eJQDFLBW2aR8LPw42 }
    // { documentId: K5Zrcb6N3gmIEHgGhwoyKF }
}
```

##### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.inserting.multiple.ion-library"></a>

단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [Ion 목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.

------
#### [ Async ]

```
IIonValue ionPerson1 = valueFactory.NewEmptyStruct();
ionPerson1.SetField("FirstName", valueFactory.NewString("Brent"));
ionPerson1.SetField("GovId", valueFactory.NewString("TOYENC486FH"));

IIonValue ionPerson2 = valueFactory.NewEmptyStruct();
ionPerson2.SetField("FirstName", valueFactory.NewString("Jim"));
ionPerson2.SetField("GovId", valueFactory.NewString("ROEE1C1AABH"));

IIonValue ionPeople = valueFactory.NewEmptyList();
ionPeople.Add(ionPerson1);
ionPeople.Add(ionPerson2);

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("INSERT INTO Person ?", ionPeople);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created documents' ID:
    // {
    //     documentId: "6BFt5eJQDFLBW2aR8LPw42"
    // }
    //
    // {
    //     documentId: "K5Zrcb6N3gmIEHgGhwoyKF"
    // }
}
```

------
#### [ Sync ]

```
IIonValue ionPerson1 = valueFactory.NewEmptyStruct();
ionPerson1.SetField("FirstName", valueFactory.NewString("Brent"));
ionPerson1.SetField("GovId", valueFactory.NewString("TOYENC486FH"));

IIonValue ionPerson2 = valueFactory.NewEmptyStruct();
ionPerson2.SetField("FirstName", valueFactory.NewString("Jim"));
ionPerson2.SetField("GovId", valueFactory.NewString("ROEE1C1AABH"));

IIonValue ionPeople = valueFactory.NewEmptyList();
ionPeople.Add(ionPerson1);
ionPeople.Add(ionPerson2);

IResult result = driver.Execute(txn =>
{
    return txn.Execute("INSERT INTO Person ?", ionPeople);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the created documents' ID:
    // {
    //     documentId: "6BFt5eJQDFLBW2aR8LPw42"
    // }
    //
    // {
    //     documentId: "K5Zrcb6N3gmIEHgGhwoyKF"
    // }
}
```

------

Ion 목록을 전달할 때는 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호(`<<...>>`)로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

### 문서 업데이트
<a name="cookbook-dotnet.crud.updating"></a>

```
string govId = "TOYENC486FH";
string firstName = "John";

IAsyncResult<Document> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Document>("UPDATE Person SET FirstName = ? WHERE GovId = ?", firstName , govId));
});

await foreach (Document row in result)
{
    Console.WriteLine("{ documentId: " + row.DocumentId + " }");
    // The statement returns the updated document ID:
    // { documentId: Djg30Zoltqy5M4BFsA2jSJ }
}
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.updating.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("John");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", ionFirstName , ionGovId);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");
IIonValue ionFirstName = valueFactory.NewString("John");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", ionFirstName , ionGovId);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the updated document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삭제
<a name="cookbook-dotnet.crud.deleting"></a>

```
string govId = "TOYENC486FH";

IAsyncResult<Document> result = await driver.Execute(async txn =>
{
    return await txn.Execute(txn.Query<Document>("DELETE FROM Person WHERE GovId = ?", govId));
});

await foreach (Document row in result)
{
    Console.WriteLine("{ documentId: " + row.DocumentId + " }");
    // The statement returns the updated document ID:
    // { documentId: Djg30Zoltqy5M4BFsA2jSJ }
}
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.deleting.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IAsyncResult result = await driver.Execute(async txn =>
{
    return await txn.Execute("DELETE FROM Person WHERE GovId = ?", ionGovId);
});

await foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the deleted document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IResult result = driver.Execute(txn =>
{
    return txn.Execute("DELETE FROM Person WHERE GovId = ?", ionGovId);
});

foreach (IIonValue row in result)
{
    Console.WriteLine(row.ToPrettyString());
    // The statement returns the deleted document ID:
    // {
    //     documentId: "Djg30Zoltqy5M4BFsA2jSJ"
    // }
}
```

------

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 하나의 트랜잭션에서 여러 명령문 실행
<a name="cookbook-dotnet.crud.multi-statement"></a>

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.Generic.IAsyncResult<Vehicle> result = await txn.Execute(
            txn.Query<Vehicle>("SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin));

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                txn.Query<Document>("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin));
            return true;
        }
        return false;
    });
}
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.multi-statement.ion-library"></a>

------
#### [ Async ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    ValueFactory valueFactory = new ValueFactory();
    IIonValue ionVin = valueFactory.NewString(vin);

    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.IAsyncResult result = await txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin);

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------

### 재시도 로직
<a name="cookbook-dotnet.crud.retry-logic"></a>

드라이버의 내장 재시도 로직에 대한 자세한 내용은 [Amazon QLDB의 드라이버를 사용한 재시도 정책에 대한 이해](driver-retry-policy.md) 섹션을 참조하세요.

### 고유성 제약 조건 구현
<a name="cookbook-dotnet.crud.uniqueness-constraints"></a>

QLDB는 고유 인덱스를 지원하지 않지만 애플리케이션에서 이 동작을 구현할 수 있습니다.

`Person` 테이블의 `GovId` 필드에 고유성 제약 조건을 구현하려고 한다고 가정해 보겠습니다. 이렇게 하면 다음 작업을 수행하는 트랜잭션을 작성합니다.

1. 테이블에 지정된 `GovId`가 있는 기존 문서가 없는지 확인합니다.

1. 어설션이 통과하면 문서를 삽입합니다.

경쟁 트랜잭션이 어설션을 동시에 통과하면 트랜잭션 중 하나만 성공적으로 커밋됩니다. 다른 트랜잭션은 OCC 충돌 예외가 발생하여 실패합니다.

다음 코드 예제는 이 고유성 제약 조건 구현 방법을 보여줍니다.

```
string govId = "TOYENC486FH";

Person person = new Person
{
    GovId = "TOYENC486FH",
    FirstName = "Brent"
};

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult<Person> result = await txn.Execute(txn.Query<Person>("SELECT * FROM Person WHERE GovId = ?", govId));

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute(txn.Query<Document>("INSERT INTO Person ?", person));
});
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.crud.uniqueness.ion-library"></a>

------
#### [ Async ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

await driver.Execute(async txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IAsyncResult result = await txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = await result.CountAsync();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    await txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------
#### [ Sync ]

```
IIonValue ionGovId = valueFactory.NewString("TOYENC486FH");

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("GovId", valueFactory.NewString("TOYENC486FH"));
ionPerson.SetField("FirstName", valueFactory.NewString("Brent"));

driver.Execute(txn =>
{
    // Check if a document with GovId:TOYENC486FH exists
    // This is critical to make this transaction idempotent
    IResult result = txn.Execute("SELECT * FROM Person WHERE GovId = ?", ionGovId);

    // Check if there is a record in the cursor.
    int count = result.Count();
    if (count > 0)
    {
        // Document already exists, no need to insert
        return;
    }

    // Insert the document.
    txn.Execute("INSERT INTO Person ?", ionPerson);
});
```

------

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

## Amazon Ion 작업
<a name="cookbook-dotnet.ion"></a>

QLDB에서 Amazon Ion 데이터를 처리하는 방법은 여러 가지가 있습니다. [Ion 라이브러리](https://github.com/amzn/ion-dotnet)를 사용하여 Ion 값을 생성하고 수정할 수 있습니다. 또는 [Ion 객체 매퍼](https://github.com/amzn/ion-object-mapper-dotnet)를 사용하여 C\$1 *POCO(Plain Old CLR Object)*를 Ion 값에 매핑하거나 그 반대로 매핑할 수 있습니다. .NET용 QLDB 드라이버 버전 1.3.0에서는 Ion 객체 매퍼에 대한 지원이 도입되었습니다.

다음 섹션에서는 두 기술을 모두 사용하여 Ion 데이터를 처리하는 코드 예제를 제공합니다.

**Contents**
+ [

### Ion 모듈 가져오기
](#cookbook-dotnet.ion.import)
+ [

### Ion 유형 생성
](#cookbook-dotnet.ion.creating-types)
+ [

### Ion 이진 덤프 가져오기
](#cookbook-dotnet.ion.getting-binary)
+ [

### Ion 텍스트 덤프 가져오기
](#cookbook-dotnet.ion.getting-text)

### Ion 모듈 가져오기
<a name="cookbook-dotnet.ion.import"></a>

```
using Amazon.IonObjectMapper;
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.ion.import.ion-library"></a>

```
using Amazon.IonDotnet.Builders;
```

### Ion 유형 생성
<a name="cookbook-dotnet.ion.creating-types"></a>

다음 코드 예제는 Ion 객체 매퍼를 사용하여 C\$1 객체에서 Ion 값을 만드는 방법을 보여줍니다.

```
// Assumes that Person class is defined as follows:
// public class Person
// {
//     public string FirstName { get; set; }
//     public int Age { get; set; }
// }

// Initialize the Ion Object Mapper
IonSerializer ionSerializer = new IonSerializer();

// The C# object to be serialized
Person person = new Person
{
    FirstName = "John",
    Age = 13
};

// Serialize the C# object into stream using the Ion Object Mapper
Stream stream = ionSerializer.Serialize(person);

// Load will take in stream and return a datagram; a top level container of Ion values.
IIonValue ionDatagram = IonLoader.Default.Load(stream);

// To get the Ion value within the datagram, we call GetElementAt(0).
IIonValue ionPerson = ionDatagram.GetElementAt(0);

Console.WriteLine(ionPerson.GetField("firstName").StringValue);
Console.WriteLine(ionPerson.GetField("age").IntValue);
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.ion.creating-types.ion-library"></a>

다음 코드 예제는 Ion 라이브러리를 사용하여 Ion 값을 생성하는 두 가지 방법을 보여줍니다.

**`ValueFactory` 사용**

```
using Amazon.IonDotnet.Tree;
using Amazon.IonDotnet.Tree.Impl;

IValueFactory valueFactory = new ValueFactory();

IIonValue ionPerson = valueFactory.NewEmptyStruct();
ionPerson.SetField("firstName", valueFactory.NewString("John"));
ionPerson.SetField("age", valueFactory.NewInt(13));

Console.WriteLine(ionPerson.GetField("firstName").StringValue);
Console.WriteLine(ionPerson.GetField("age").IntValue);
```

**`IonLoader` 사용**

```
using Amazon.IonDotnet.Builders;
using Amazon.IonDotnet.Tree;

// Load will take in Ion text and return a datagram; a top level container of Ion values.
IIonValue ionDatagram = IonLoader.Default.Load("{firstName: \"John\", age: 13}");

// To get the Ion value within the datagram, we call GetElementAt(0).
IIonValue ionPerson = ionDatagram.GetElementAt(0);

Console.WriteLine(ionPerson.GetField("firstName").StringValue);
Console.WriteLine(ionPerson.GetField("age").IntValue);
```

### Ion 이진 덤프 가져오기
<a name="cookbook-dotnet.ion.getting-binary"></a>

```
// Initialize the Ion Object Mapper with Ion binary serialization format
IonSerializer ionSerializer = new IonSerializer(new IonSerializationOptions
{
    Format = IonSerializationFormat.BINARY
});

// The C# object to be serialized
Person person = new Person
{
    FirstName = "John",
    Age = 13
};

MemoryStream stream = (MemoryStream) ionSerializer.Serialize(person);
Console.WriteLine(BitConverter.ToString(stream.ToArray()));
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.ion.getting-binary.ion-library"></a>

```
// ionObject is an Ion struct
MemoryStream stream = new MemoryStream();
using (var writer = IonBinaryWriterBuilder.Build(stream))
{
    ionObject.WriteTo(writer);
    writer.Finish();
}

Console.WriteLine(BitConverter.ToString(stream.ToArray()));
```

### Ion 텍스트 덤프 가져오기
<a name="cookbook-dotnet.ion.getting-text"></a>

```
// Initialize the Ion Object Mapper
IonSerializer ionSerializer = new IonSerializer(new IonSerializationOptions
{
    Format = IonSerializationFormat.TEXT
});

// The C# object to be serialized
Person person = new Person
{
    FirstName = "John",
    Age = 13
};

MemoryStream stream = (MemoryStream) ionSerializer.Serialize(person);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(stream.ToArray()));
```

#### Ion 라이브러리 사용
<a name="cookbook-dotnet.ion.getting-text.ion-library"></a>

```
// ionObject is an Ion struct
StringWriter sw = new StringWriter();
using (var writer = IonTextWriterBuilder.Build(sw))
{
    ionObject.WriteTo(writer);
    writer.Finish();
}

Console.WriteLine(sw.ToString());
```

Ion 작업에 대한 자세한 정보는 GitHub의 [Amazon Ion 설명서](http://amzn.github.io/ion-docs/)를 참조하세요. QLDB에서 Ion을 사용하는 방법에 대한 추가 코드 예제는 [Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업](driver-working-with-ion.md) 섹션을 참조하세요.

# Go용 Amazon QLDB 드라이버
<a name="getting-started.golang"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

원장의 데이터를 사용하려면 AWS 제공된 드라이버를 사용하여 Go 애플리케이션에서 Amazon QLDB에 연결할 수 있습니다. 다음 주제에서는 Go용 QLDB 드라이버를 시작하는 방법에 대해 설명합니다.

**Topics**
+ [

## 드라이버 리소스
](#getting-started.golang.resources)
+ [

## 사전 조건
](#getting-started.golang.prereqs)
+ [

## 설치
](#getting-started.golang.install)
+ [빠른 시작 자습서](driver-quickstart-golang.md)
+ [Cookbook 참조](driver-cookbook-golang.md)

## 드라이버 리소스
<a name="getting-started.golang.resources"></a>

Go 드라이버에서 지원하는 기능에 대한 자세한 설명은 다음 리소스를 참조하세요.
+ API 참조: [3.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver), [2.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver), [1.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/qldbdriver)
+ [드라이버 소스 코드(GitHub)](https://github.com/awslabs/amazon-qldb-driver-go)
+ [Amazon Ion Cookbook](http://amzn.github.io/ion-docs/guides/cookbook.html)

## 사전 조건
<a name="getting-started.golang.prereqs"></a>

Go용 QLDB 드라이버를 시작하기 전에 다음을 수행해야 합니다:

1. 의 AWS 설정 지침을 따릅니다[Amazon QLDB 액세스](accessing.md). 다음 내용이 포함됩니다:

   1. 가입합니다 AWS.

   1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

   1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

1. (선택 사항 )원하는 통합 개발 환경(IDE)을 설치합니다. 일반적으로 Go에 사용되는 IDE 목록은 Go 웹 사이트의 [편집기 플러그인 및 IDE](https://golang.org/doc/editors.html)를 참조하세요.

1. [Go 다운로드](https://golang.org/dl/) 사이트에서 다음 버전의 Go를 다운로드하여 설치하세요.
   + **1.15 이상** - Go v3용 QLDB 드라이버
   + **1.14** - Go v1 또는 v2용 QLDB 드라이버

1. [AWS SDK for Go](https://aws.amazon.com/sdk-for-go)를 위한 개발 환경 구성:

   1.  AWS 자격 증명을 설정합니다. 공유 보안 인증 파일을 생성할 것을 권장합니다.

      지침은 *AWS SDK for Go 개발자 가이드*의 [자격 증명 지정](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials)을 참조하세요.

   1. 기본 AWS 리전을 설정하세요. 방법을 알아보려면 [AWS 리전지정하기](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-the-aws-region)를 참조하세요.

      사용 가능한 리전의 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

그런 다음 기본 샘플 애플리케이션을 설정하고 예의 단축 코드를 실행하거나 해당 드라이버를 기존 Go 프로젝트에 설치할 수 있습니다.
+ 기존 프로젝트에 QLDB 드라이버와 AWS SDK for Go 를 설치하려면 로 이동합니다[설치](#getting-started.golang.install).
+ 프로젝트를 설정하고 원장에 대한 기본 데이터 트랜잭션을 보여주는 단축 코드 예제를 실행하려면 [빠른 시작 자습서](driver-quickstart-golang.md)를 참조하세요.

## 설치
<a name="getting-started.golang.install"></a>

Go용 QLDB 드라이버는 GitHub 리포지토리 [awslabs/amazon-qldb-driver-go](http://github.com/awslabs/amazon-qldb-driver-go)의 오픈 소스입니다. QLDB는 다음 드라이버 버전과 해당 Go 종속성을 지원합니다.


****  

| 드라이버 버전 | Go 버전 | 상태 표시기 | 최근 릴리스 날짜 | 
| --- | --- | --- | --- | 
| [1.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/qldbdriver) | 1.14 이상 | 프로덕션 릴리스 | 2021년 6월 16일 | 
| [2.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver) | 1.14 이상 | 프로덕션 릴리스 | 2021년 7월 21일 | 
| [3.x](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver) | 1.15 이상 | 프로덕션 릴리스 | 2022년 11월 10일 | 

**드라이버를 설치하려면**

1. 프로젝트에서 [Go 모듈](https://blog.golang.org/using-go-modules)을 사용하여 프로젝트 종속성을 설치하는지 확인하세요.

1. 프로젝트 디렉터리에 다음 `go get` 명령을 입력합니다.

------
#### [ 3.x ]

   ```
   $ go get -u github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver
   ```

------
#### [ 2.x ]

   ```
   $ go get -u github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver
   ```

------

드라이버를 설치하면 [AWS SDK for Go](https://github.com/aws/aws-sdk-go) 또는 [AWS SDK for Go v2](https://github.com/aws/aws-sdk-go-v2) 및 [Amazon Ion](https://github.com/amzn/ion-go) 패키지를 포함한 종속 항목도 설치됩니다.

원장에서 기본 데이터 트랜잭션을 실행하는 방법에 대한 단축 코드 예제는 [Cookbook 참조](driver-cookbook-golang.md)를 참조하세요.

# Go용 Amazon QLDB 드라이버 - 빠른 시작 자습서
<a name="driver-quickstart-golang"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 Go용 Amazon QLDB 드라이버의 최신 버전을 사용하여 간단한 애플리케이션을 설정하는 방법을 알아봅니다. 이 안내서에는 드라이버 설치 단계 및 기본적인 CRUD(*생성, 읽기, 업데이트 및 삭제*) 작업에 대한 단축 코드 예제가 포함되어 있습니다.

**Topics**
+ [

## 사전 조건
](#driver-quickstart-golang.prereqs)
+ [

## 1단계: 드라이버 설치
](#driver-quickstart-golang.install)
+ [

## 2단계: 패키지 가져오기
](#driver-quickstart-golang.import)
+ [

## 3단계: 드라이버 초기화
](#driver-quickstart-golang.initialize)
+ [

## 4단계: 테이블 및 인덱스 생성
](#driver-quickstart-golang.create-table-index)
+ [

## 5단계: 문서 삽입
](#driver-quickstart-golang.insert)
+ [

## 6단계: 문서 쿼리
](#driver-quickstart-golang.query)
+ [

## 7단계: 문서 업데이트
](#driver-quickstart-golang.update)
+ [

## 8단계: 업데이트된 문서 쿼리
](#driver-quickstart-golang.query-2)
+ [

## 9단계: 테이블 삭제
](#driver-quickstart-golang.drop-table)
+ [

## 전체 애플리케이션 실행
](#driver-quickstart-golang.complete)

## 사전 조건
<a name="driver-quickstart-golang.prereqs"></a>

시작하기 전에 다음을 수행해야 합니다.

1. 아직 Go 드라이버에 대한 [사전 조건](getting-started.golang.md#getting-started.golang.prereqs)을 완료하지 않은 경우, 완료하세요. 여기에는 가입 AWS, 개발을 위한 프로그래밍 방식 액세스 권한 부여, Go 설치가 포함됩니다.

1. `quick-start`라는 명칭의 원장을 생성합니다.

   원장 생성 방법을 알아보려면 *콘솔 시작하기*의 [Amazon QLDB 원장의 기본 작업](ledger-management.basics.md) 또는 [1단계: 새 원장 생성](getting-started-step-1.md) 섹션을 참조하세요.

## 1단계: 드라이버 설치
<a name="driver-quickstart-golang.install"></a>

프로젝트에서 [Go 모듈](https://blog.golang.org/using-go-modules)을 사용하여 프로젝트 종속성을 설치하는지 확인하세요.

프로젝트 디렉터리에 다음 `go get` 명령을 입력합니다.

```
$ go get -u github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver
```

드라이버를 설치하면 [AWS SDK for Go v2](https://github.com/aws/aws-sdk-go-v2) 및 [Amazon Ion](https://github.com/amzn/ion-go) 패키지를 포함한 종속 항목도 설치됩니다.

## 2단계: 패키지 가져오기
<a name="driver-quickstart-golang.import"></a>

다음 AWS 패키지를 가져옵니다.

```
import (
    "context"
    "fmt"

    "github.com/amzn/ion-go/ion"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/qldbSession"
    "github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver"
)
```

## 3단계: 드라이버 초기화
<a name="driver-quickstart-golang.initialize"></a>

`quick-start`라는 명칭의 원장에 연결되는 드라이버의 인스턴스를 초기화합니다.

```
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
  panic(err)
}

qldbSession := qldbsession.NewFromConfig(cfg, func(options *qldbsession.Options) {
    options.Region = "us-east-1"
})
driver, err := qldbdriver.New(
    "quick-start",
    qldbSession,
    func(options *qldbdriver.DriverOptions) {
        options.LoggerVerbosity = qldbdriver.LogInfo
    })
if err != nil {
    panic(err)
}

defer driver.Shutdown(context.Background())
```

**참고**  
이 코드 예에서는 *us-east-1*을 원장을 생성한 AWS 리전 으로 바꿉니다.

## 4단계: 테이블 및 인덱스 생성
<a name="driver-quickstart-golang.create-table-index"></a>

다음 코드 예에서는 `CREATE TABLE` 및 `CREATE INDEX` 문을 실행하는 방법을 보여줍니다.

```
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    _, err := txn.Execute("CREATE TABLE People")
    if err != nil {
        return nil, err
    }

    // When working with QLDB, it's recommended to create an index on fields we're filtering on.
    // This reduces the chance of OCC conflict exceptions with large datasets.
    _, err = txn.Execute("CREATE INDEX ON People (firstName)")
    if err != nil {
        return nil, err
    }

    _, err = txn.Execute("CREATE INDEX ON People (age)")
    if err != nil {
        return nil, err
    }

    return nil, nil
})
if err != nil {
    panic(err)
}
```

이 코드는 `People`이라는 테이블을 만들고 해당 테이블의 `age` 및 `firstName` 필드를 인덱싱합니다. [인덱스](ql-reference.create-index.md)는 쿼리 성능을 최적화하고 [OCC(낙관적 동시성 제어)](concurrency.md) 충돌 예외를 제한하는 데 필요합니다.

## 5단계: 문서 삽입
<a name="driver-quickstart-golang.insert"></a>

다음 코드 예에서는 `INSERT` 문을 실행하는 방법을 보여줍니다. QLDB는 [PartiQL](ql-reference.md) 쿼리 언어(SQL 호환) 및 [Amazon Ion](ion.md) 데이터 형식(JSON의 상위 집합)을 지원합니다.

### 리터럴 PartiQL 사용
<a name="driver-quickstart-golang.insert.partiql"></a>

다음 코드는 문자열 리터럴 PartiQL 명령문을 사용하여 `People` 테이블에 문서를 삽입합니다.

```
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    return txn.Execute("INSERT INTO People {'firstName': 'Jane', 'lastName': 'Doe', 'age': 77}")
})
if err != nil {
    panic(err)
}
```

### Ion 데이터 유형 사용
<a name="driver-quickstart-golang.insert.ion"></a>

Go의 내장 [JSON 패키지](https://golang.org/pkg/encoding/json/)와 마찬가지로 Ion과 Go 데이터 유형을 마샬링하거나 Ion에서 마샬링 취소할 수 있습니다.

1. 다음과 같이 `Person`이라는 이름의 Go 구조가 있는 경우.

   ```
   type Person struct {
       FirstName string `ion:"firstName"`
       LastName  string `ion:"lastName"`
       Age       int    `ion:"age"`
   }
   ```

1. `Person`의 인스턴스를 만듭니다.

   ```
   person := Person{"John", "Doe", 54}
   ```

   드라이버는 Ion 인코딩된 `person`의 텍스트 표현을 자동으로 마샬링합니다.
**중요**  
마샬링과 마샬링 취소가 제대로 작동하려면 Go 데이터 구조의 필드 이름을 내보내야 합니다(첫 글자를 대문자로 표시).

1. `person` 인스턴스를 트랜잭션의 `Execute` 메서드로 전달합니다.

   ```
   _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
       return txn.Execute("INSERT INTO People ?", person)
   })
   if err != nil {
       panic(err)
   }
   ```

   이 예에서는 물음표(`?`)를 변수 자리 표시자로 사용하여 문서 정보를 해당 문에 전달합니다. 자리 표시자를 사용할 때는 Ion 인코딩된 텍스트 값을 전달해야 합니다.
**작은 정보**  
단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.  

   ```
   // people is a list
   txn.Execute("INSERT INTO People ?", people)
   ```
목록을 전달할 때 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

## 6단계: 문서 쿼리
<a name="driver-quickstart-golang.query"></a>

다음 코드 예에서는 `SELECT` 문을 실행하는 방법을 보여줍니다.

```
p, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE age = 54")
    if err != nil {
        return nil, err
    }

    // Assume the result is not empty
    hasNext := result.Next(txn)
    if !hasNext && result.Err() != nil {
        return nil, result.Err()
    }

    ionBinary := result.GetCurrentData()

    temp := new(Person)
    err = ion.Unmarshal(ionBinary, temp)
    if err != nil {
        return nil, err
    }

    return *temp, nil
})
if err != nil {
    panic(err)
}

var returnedPerson Person
returnedPerson = p.(Person)

if returnedPerson != person {
    fmt.Print("Queried result does not match inserted struct")
}
```

이 예제에서는 `People` 테이블에서 문서를 쿼리하고 결과 세트가 비어 있지 않다고 가정하고 결과에서 문서를 반환합니다.

## 7단계: 문서 업데이트
<a name="driver-quickstart-golang.update"></a>

다음 코드 예에서는 `UPDATE` 문을 실행하는 방법을 보여줍니다.

```
person.Age += 10

_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    return txn.Execute("UPDATE People SET age = ? WHERE firstName = ?", person.Age, person.FirstName)
})
if err != nil {
    panic(err)
}
```

## 8단계: 업데이트된 문서 쿼리
<a name="driver-quickstart-golang.query-2"></a>

다음 코드 예제는 `firstName`으로 `People` 테이블을 쿼리하고 결과 세트의 모든 문서를 반환합니다.

```
p, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE firstName = ?", person.FirstName)
    if err != nil {
        return nil, err
    }

    var people []Person
    for result.Next(txn) {
        ionBinary := result.GetCurrentData()

        temp := new(Person)
        err = ion.Unmarshal(ionBinary, temp)
        if err != nil {
            return nil, err
        }

        people = append(people, *temp)
    }
    if result.Err() != nil {
        return nil, result.Err()
    }

    return people, nil
})
if err != nil {
    panic(err)
}

var people []Person
people = p.([]Person)

updatedPerson := Person{"John", "Doe", 64}
if people[0] != updatedPerson {
    fmt.Print("Queried result does not match updated struct")
}
```

## 9단계: 테이블 삭제
<a name="driver-quickstart-golang.drop-table"></a>

다음 코드 예에서는 `DROP TABLE` 문을 실행하는 방법을 보여줍니다.

```
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    return txn.Execute("DROP TABLE People")
})
if err != nil {
    panic(err)
}
```

## 전체 애플리케이션 실행
<a name="driver-quickstart-golang.complete"></a>

다음 코드 예는 애플리케이션의 전체 버전입니다. 이전 단계를 개별적으로 수행하는 대신 이 코드 예를 처음부터 끝까지 복사하여 실행할 수도 있습니다. 이 애플리케이션은 `quick-start`이라는 명칭의 원장에 대한 몇 가지 기본 CRUD 작업을 보여줍니다.

**참고**  
이 코드를 실행하기 전에 `quick-start` 원장에 `People`이라는 명칭의 활성 테이블이 아직 없는지 확인하세요.

```
package main

import (
    "context"
    "fmt"

    "github.com/amzn/ion-go/ion"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/qldbsession"
    "github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver"
)

func main() {
    awsSession := session.Must(session.NewSession(aws.NewConfig().WithRegion("us-east-1")))
    qldbSession := qldbsession.New(awsSession)

    driver, err := qldbdriver.New(
        "quick-start",
        qldbSession,
        func(options *qldbdriver.DriverOptions) {
            options.LoggerVerbosity = qldbdriver.LogInfo
        })
    if err != nil {
        panic(err)
    }
    defer driver.Shutdown(context.Background())

    _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        _, err := txn.Execute("CREATE TABLE People")
        if err != nil {
            return nil, err
        }

        // When working with QLDB, it's recommended to create an index on fields we're filtering on.
        // This reduces the chance of OCC conflict exceptions with large datasets.
        _, err = txn.Execute("CREATE INDEX ON People (firstName)")
        if err != nil {
            return nil, err
        }

        _, err = txn.Execute("CREATE INDEX ON People (age)")
        if err != nil {
            return nil, err
        }

        return nil, nil
    })
    if err != nil {
        panic(err)
    }

    _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        return txn.Execute("INSERT INTO People {'firstName': 'Jane', 'lastName': 'Doe', 'age': 77}")
    })
    if err != nil {
        panic(err)
    }

    type Person struct {
        FirstName string `ion:"firstName"`
        LastName  string `ion:"lastName"`
        Age       int    `ion:"age"`
    }

    person := Person{"John", "Doe", 54}

    _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        return txn.Execute("INSERT INTO People ?", person)
    })
    if err != nil {
        panic(err)
    }

    p, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE age = 54")
        if err != nil {
            return nil, err
        }

        // Assume the result is not empty
        hasNext := result.Next(txn)
        if !hasNext && result.Err() != nil {
            return nil, result.Err()
        }

        ionBinary := result.GetCurrentData()

        temp := new(Person)
        err = ion.Unmarshal(ionBinary, temp)
        if err != nil {
            return nil, err
        }

        return *temp, nil
    })
    if err != nil {
        panic(err)
    }

    var returnedPerson Person
    returnedPerson = p.(Person)

    if returnedPerson != person {
        fmt.Print("Queried result does not match inserted struct")
    }

    person.Age += 10

    _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        return txn.Execute("UPDATE People SET age = ? WHERE firstName = ?", person.Age, person.FirstName)
    })
    if err != nil {
        panic(err)
    }

    p, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        result, err := txn.Execute("SELECT firstName, lastName, age FROM People WHERE firstName = ?", person.FirstName)
        if err != nil {
            return nil, err
        }

        var people []Person
        for result.Next(txn) {
            ionBinary := result.GetCurrentData()

            temp := new(Person)
            err = ion.Unmarshal(ionBinary, temp)
            if err != nil {
                return nil, err
            }

            people = append(people, *temp)
        }
        if result.Err() != nil {
            return nil, result.Err()
        }

        return people, nil
    })
    if err != nil {
        panic(err)
    }

    var people []Person
    people = p.([]Person)

    updatedPerson := Person{"John", "Doe", 64}
    if people[0] != updatedPerson {
        fmt.Print("Queried result does not match updated struct")
    }

    _, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        return txn.Execute("DROP TABLE People")
    })
    if err != nil {
        panic(err)
    }
}
```

# Go용 Amazon QLDB 드라이버 - Cookbook 참조
<a name="driver-cookbook-golang"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 참조 가이드는 Go용 Amazon QLDB 드라이버의 일반적인 사용 사례를 보여줍니다. 이 Go 코드 예제는 드라이버를 사용하여 기본 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행하는 방법을 보여줍니다. 또한 Amazon Ion 데이터를 처리하기 위한 코드 예제도 포함되어 있습니다. 또한 이 가이드에서는 트랜잭션에 멱등성을 부여하고 고유성 제약하는 모범 사례를 중점적으로 설명합니다.

**참고**  
해당하는 경우, 일부 사용 사례에서는 Go용 QLDB 드라이버의 지원되는 각 주요 버전마다 다른 코드 예제가 있습니다.

**Contents**
+ [

## 드라이버 가져오기
](#cookbook-golang.importing)
+ [

## 드라이버 인스턴스화
](#cookbook-golang.instantiating)
+ [

## CRUD 작업
](#cookbook-golang.crud)
  + [

### 테이블 생성
](#cookbook-golang.crud.creating-tables)
  + [

### 인덱스 생성
](#cookbook-golang.crud.creating-indexes)
  + [

### 문서 읽기
](#cookbook-golang.crud.reading)
    + [

#### 쿼리 파라미터 사용
](#cookbook-golang.reading-using-params)
  + [

### 문서 삽입하기
](#cookbook-golang.crud.inserting)
    + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-golang.crud.inserting.multiple)
  + [

### 문서 업데이트
](#cookbook-golang.crud.updating)
  + [

### 문서 삭제
](#cookbook-golang.crud.deleting)
  + [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-golang.crud.multi-statement)
  + [

### 재시도 로직
](#cookbook-golang.crud.retry-logic)
  + [

### 고유성 제약 조건 구현
](#cookbook-golang.crud.uniqueness-constraints)
+ [

## Amazon Ion 작업
](#cookbook-golang.ion)
  + [

### Ion 모듈 가져오기
](#cookbook-golang.ion.import)
  + [

### Ion 유형 생성
](#cookbook-golang.ion.creating-types)
  + [

### Ion 이진 가져오기
](#cookbook-golang.ion.getting-binary)
  + [

### Ion 텍스트 가져오기
](#cookbook-golang.ion.getting-text)

## 드라이버 가져오기
<a name="cookbook-golang.importing"></a>

다음 코드 예제에서는 드라이버 및 기타 필수 AWS 패키지를 가져옵니다.

------
#### [ 3.x ]

```
import (

    "github.com/amzn/ion-go/ion"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/qldbSession"
    "github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver"
)
```

------
#### [ 2.x ]

```
import (

    "github.com/amzn/ion-go/ion"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/qldbsession"
    "github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver"
)
```

------

**참고**  
이 예제에서는 Amazon Ion 패키지(`amzn/ion-go/ion`)도 가져옵니다. 이 참조에서 일부 데이터 작업을 실행할 때 Ion 데이터를 처리하려면 이 패키지가 필요합니다. 자세한 내용은 [Amazon Ion 작업](#cookbook-golang.ion) 섹션을 참조하세요.

## 드라이버 인스턴스화
<a name="cookbook-golang.instantiating"></a>

다음 코드 예제는 지정된 AWS 리전의 지정된 원장 이름에 연결되는 드라이버 인스턴스를 만듭니다.

------
#### [ 3.x ]

```
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
    panic(err)
}

qldbSession := qldbsession.NewFromConfig(cfg, func(options *qldbsession.Options) {
    options.Region = "us-east-1"
})
driver, err := qldbdriver.New(
  "vehicle-registration",
  qldbSession,
  func(options *qldbdriver.DriverOptions) {
    options.LoggerVerbosity = qldbdriver.LogInfo
})
if err != nil {
  panic(err)
}

defer driver.Shutdown(context.Background())
```

------
#### [ 2.x ]

```
awsSession := session.Must(session.NewSession(aws.NewConfig().WithRegion("us-east-1")))
qldbSession := qldbsession.New(awsSession)

driver, err := qldbdriver.New(
  "vehicle-registration",
  qldbSession,
  func(options *qldbdriver.DriverOptions) {
    options.LoggerVerbosity = qldbdriver.LogInfo
  })
if err != nil {
  panic(err)
}
```

------

## CRUD 작업
<a name="cookbook-golang.crud"></a>

QLDB는 트랜잭션의 일부로 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행합니다.

**주의**  
가장 좋은 방법은 쓰기 트랜잭션이 완전한 멱등성을 부여하는 것입니다.

**트랜잭션에 멱등성 부여하기**

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 좋습니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 *멱등성*을 가집니다.

이름이 `Person`인 테이블에 문서를 삽입하는 트랜잭션을 예로 들어 보겠습니다. 트랜잭션은 먼저 문서가 테이블에 이미 존재하는지 여부를 확인해야 합니다. 이렇게 확인하지 않으면 테이블에 문서가 중복될 수 있습니다.

QLDB가 서버 측에서 트랜잭션을 성공적으로 커밋했지만 응답을 기다리는 동안 클라이언트 제한 시간이 초과되었다고 가정해 보겠습니다. 트랜잭션이 멱등성을 가지지 않는 경우 재시도 시 동일한 문서가 두 번 이상 삽입될 수 있습니다.

**인덱스를 사용하여 전체 테이블 스캔 방지**

인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 이 인덱싱된 조회가 없으면 QLDB는 테이블 스캔을 수행해야 하며, 이로 인해 트랜잭션 제한 시간이 초과되거나 *OCC(낙관적 동시성 제어)* 충돌이 발생할 수 있습니다.

OCC에 대한 자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md) 단원을 참조하세요.

**암시적으로 생성된 트랜잭션**

[QLDBDriver.Execute](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#QLDBDriver.Execute) 함수는 [트랜잭션](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#Transaction)의 인스턴스를 수신하는 Lambda 함수를 받아들이며, 이 함수를 사용하여 명령문을 실행할 수 있습니다. `Transaction`의 인스턴스는 암시적으로 생성된 트랜잭션을 래핑합니다.

`Transaction.Execute` 함수를 사용하여 Lambda 함수 내에서 명령문을 실행할 수 있습니다. 드라이버는 Lambda 함수가 반환될 때 트랜잭션을 암시적으로 커밋합니다.

다음 섹션에서는 기본 CRUD 작업을 실행하고, 사용자 지정 재시도 로직을 지정하고, 고유성 제약 조건을 구현하는 방법을 보여줍니다.

**Contents**
+ [

### 테이블 생성
](#cookbook-golang.crud.creating-tables)
+ [

### 인덱스 생성
](#cookbook-golang.crud.creating-indexes)
+ [

### 문서 읽기
](#cookbook-golang.crud.reading)
  + [

#### 쿼리 파라미터 사용
](#cookbook-golang.reading-using-params)
+ [

### 문서 삽입하기
](#cookbook-golang.crud.inserting)
  + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-golang.crud.inserting.multiple)
+ [

### 문서 업데이트
](#cookbook-golang.crud.updating)
+ [

### 문서 삭제
](#cookbook-golang.crud.deleting)
+ [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-golang.crud.multi-statement)
+ [

### 재시도 로직
](#cookbook-golang.crud.retry-logic)
+ [

### 고유성 제약 조건 구현
](#cookbook-golang.crud.uniqueness-constraints)

### 테이블 생성
<a name="cookbook-golang.crud.creating-tables"></a>

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("CREATE TABLE Person")
})
```

### 인덱스 생성
<a name="cookbook-golang.crud.creating-indexes"></a>

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("CREATE INDEX ON Person(GovId)")
})
```

### 문서 읽기
<a name="cookbook-golang.crud.reading"></a>

```
var decodedResult map[string]interface{}

// Assumes that Person table has documents as follows:
// { "GovId": "TOYENC486FH", "FirstName": "Brent" }
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  result, err := txn.Execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")
  if err != nil {
    return nil, err
  }
  for result.Next(txn) {
    ionBinary := result.GetCurrentData()
    err = ion.Unmarshal(ionBinary, &decodedResult)
    if err != nil {
      return nil, err
    }
    fmt.Println(decodedResult) // prints map[GovId: TOYENC486FH FirstName:Brent]
  }
  if result.Err() != nil {
    return nil, result.Err()
  }
  return nil, nil
})
if err != nil {
  panic(err)
}
```

#### 쿼리 파라미터 사용
<a name="cookbook-golang.reading-using-params"></a>

다음 코드 예제는 네이티브 유형 쿼리 파라미터를 사용합니다.

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("SELECT * FROM Person WHERE GovId = ?", "TOYENC486FH")
})
if err != nil {
  panic(err)
}
```

다음 코드 예제는 여러 쿼리 파라미터를 사용합니다.

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", "TOYENC486FH", "Brent")
})
if err != nil {
  panic(err)
}
```

다음 코드 예제는 쿼리 파라미터 목록을 사용합니다.

```
govIDs := []string{}{"TOYENC486FH", "ROEE1", "YH844"}

result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("SELECT * FROM Person WHERE GovId IN (?,?,?)", govIDs...)
})
if err != nil {
  panic(err)
}
```

**참고**  
인덱싱된 조회 없이 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. 이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 [인덱스](ql-reference.create-index.md)를 사용하는 것이 좋습니다. `GovId`에 인덱스를 사용하지 않으면 쿼리에 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삽입하기
<a name="cookbook-golang.crud.inserting"></a>

다음 코드 예제는 네이티브 데이터 유형을 삽입합니다.

```
_, err = driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  // Check if a document with a GovId of TOYENC486FH exists
  // This is critical to make this transaction idempotent
  result, err := txn.Execute("SELECT * FROM Person WHERE GovId = ?", "TOYENC486FH")
  if err != nil {
    return nil, err
  }
  // Check if there are any results
  if result.Next(txn) {
    // Document already exists, no need to insert
  } else {
    person := map[string]interface{}{
      "GovId": "TOYENC486FH",
      "FirstName": "Brent",
    }
    _, err = txn.Execute("INSERT INTO Person ?", person)
    if err != nil {
      return nil, err
    }
  }
  return nil, nil
})
```

이 트랜잭션은 문서를 `Person` 테이블에 삽입합니다. 삽입하기 전에 먼저 문서가 테이블에 이미 있는지 확인합니다. **이 검사를 통해 트랜잭션은 본질적으로 멱등성을 가지게 됩니다.** 이 트랜잭션을 여러 번 실행하더라도 의도하지 않은 부작용이 발생하지는 않습니다.

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

#### 하나의 명령문에 여러 문서 삽입
<a name="cookbook-golang.crud.inserting.multiple"></a>

단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.

```
// people is a list
txn.Execute("INSERT INTO People ?", people)
```

목록을 전달할 때 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

### 문서 업데이트
<a name="cookbook-golang.crud.updating"></a>

다음 코드 예제는 네이티브 데이터 유형을 사용합니다.

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("UPDATE Person SET FirstName = ? WHERE GovId = ?", "John", "TOYENC486FH")
})
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삭제
<a name="cookbook-golang.crud.deleting"></a>

다음 코드 예제는 네이티브 데이터 유형을 사용합니다.

```
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  return txn.Execute("DELETE FROM Person WHERE GovId = ?", "TOYENC486FH")
})
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 하나의 트랜잭션에서 여러 명령문 실행
<a name="cookbook-golang.crud.multi-statement"></a>

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
func InsureCar(driver *qldbdriver.QLDBDriver, vin string) (bool, error) {
    insured, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {

        result, err := txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
        if err != nil {
            return false, err
        }

        hasNext := result.Next(txn)
        if !hasNext && result.Err() != nil {
            return false, result.Err()
        }

        if hasNext {
            _, err = txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
            if err != nil {
                return false, err
            }
            return true, nil
        }
        return false, nil
    })
    if err != nil {
        panic(err)
    }

    return insured.(bool), err
}
```

### 재시도 로직
<a name="cookbook-golang.crud.retry-logic"></a>

드라이버의 `Execute` 함수에는 재시도 가능한 예외(예: 시간 초과 또는 OCC 충돌)가 발생할 경우 트랜잭션을 재시도하는 재시도 메커니즘이 내장되어 있습니다. 최대 재시도 횟수와 백오프 전략을 구성할 수 있습니다.

기본 재시도 제한은 `4`이며, 기본 백오프 전략은 `10`밀리초 단위의 [ExponentialBackoffStrategy](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#ExponentialBackoffStrategy)입니다. [RetryPolicy](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver#RetryPolicy) 인스턴스를 사용하여 드라이버 인스턴스별 및 트랜잭션별로 재시도 정책을 설정할 수 있습니다.

다음 코드 예제는 드라이버 인스턴스에 대한 사용자 지정 재시도 제한 및 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다.

```
import (
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/service/qldbsession"
  "github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver"
)

func main() {
  awsSession := session.Must(session.NewSession(aws.NewConfig().WithRegion("us-east-1")))
  qldbSession := qldbsession.New(awsSession)

  // Configuring retry limit to 2
  retryPolicy := qldbdriver.RetryPolicy{MaxRetryLimit: 2}

  driver, err := qldbdriver.New("test-ledger", qldbSession, func(options *qldbdriver.DriverOptions) {
    options.RetryPolicy = retryPolicy
  })
  if err != nil {
    panic(err)
  }

  // Configuring an exponential backoff strategy with base of 20 milliseconds
  retryPolicy = qldbdriver.RetryPolicy{
    MaxRetryLimit: 2,
    Backoff: qldbdriver.ExponentialBackoffStrategy{SleepBase: 20, SleepCap: 4000,
    }}

  driver, err = qldbdriver.New("test-ledger", qldbSession, func(options *qldbdriver.DriverOptions) {
    options.RetryPolicy = retryPolicy
  })
  if err != nil {
    panic(err)
  }
}
```

다음 코드 예제는 특정 익명 함수에 대한 사용자 지정 재시도 제한 및 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다. `SetRetryPolicy` 함수는 드라이버 인스턴스에 설정된 재시도 정책을 재정의합니다.

```
import (
  "context"
  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/service/qldbsession"
  "github.com/awslabs/amazon-qldb-driver-go/v2/qldbdriver"
)

func main() {
  awsSession := session.Must(session.NewSession(aws.NewConfig().WithRegion("us-east-1")))
  qldbSession := qldbsession.New(awsSession)

  // Configuring retry limit to 2
  retryPolicy1 := qldbdriver.RetryPolicy{MaxRetryLimit: 2}

  driver, err := qldbdriver.New("test-ledger", qldbSession, func(options *qldbdriver.DriverOptions) {
    options.RetryPolicy = retryPolicy1
  })
  if err != nil {
    panic(err)
  }

  // Configuring an exponential backoff strategy with base of 20 milliseconds
  retryPolicy2 := qldbdriver.RetryPolicy{
    MaxRetryLimit: 2,
    Backoff: qldbdriver.ExponentialBackoffStrategy{SleepBase: 20, SleepCap: 4000,
    }}

  // Overrides the retry policy set by the driver instance
  driver.SetRetryPolicy(retryPolicy2)

  driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    return txn.Execute("CREATE TABLE Person")
  })
}
```

### 고유성 제약 조건 구현
<a name="cookbook-golang.crud.uniqueness-constraints"></a>

QLDB는 고유 인덱스를 지원하지 않지만 애플리케이션에서 이 동작을 구현할 수 있습니다.

`Person` 테이블의 `GovId` 필드에 고유성 제약 조건을 구현하려고 한다고 가정해 보겠습니다. 이렇게 하면 다음 작업을 수행하는 트랜잭션을 작성합니다.

1. 테이블에 지정된 `GovId`가 있는 기존 문서가 없는지 확인합니다.

1. 어설션이 통과하면 문서를 삽입합니다.

경쟁 트랜잭션이 어설션을 동시에 통과하면 트랜잭션 중 하나만 성공적으로 커밋됩니다. 다른 트랜잭션은 OCC 충돌 예외가 발생하여 실패합니다.

다음 코드 예제는 이 고유성 제약 조건 구현 방법을 보여줍니다.

```
govID := "TOYENC486FH"

document := map[string]interface{}{
  "GovId":     "TOYENC486FH",
  "FirstName": "Brent",
}

result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
  // Check if doc with GovId = govID exists
  result, err := txn.Execute("SELECT * FROM Person WHERE GovId = ?", govID)
  if err != nil {
    return nil, err
  }
  // Check if there are any results
  if result.Next(txn) {
    // Document already exists, no need to insert
    return nil, nil
  }
  return txn.Execute("INSERT INTO Person ?", document)
})
if err != nil {
  panic(err)
}
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

## Amazon Ion 작업
<a name="cookbook-golang.ion"></a>

다음 섹션에서는 Amazon Ion 모듈을 사용하여 Ion 데이터를 처리하는 방법을 보여줍니다.

**Contents**
+ [

### Ion 모듈 가져오기
](#cookbook-golang.ion.import)
+ [

### Ion 유형 생성
](#cookbook-golang.ion.creating-types)
+ [

### Ion 이진 가져오기
](#cookbook-golang.ion.getting-binary)
+ [

### Ion 텍스트 가져오기
](#cookbook-golang.ion.getting-text)

### Ion 모듈 가져오기
<a name="cookbook-golang.ion.import"></a>

```
import "github.com/amzn/ion-go/ion"
```

### Ion 유형 생성
<a name="cookbook-golang.ion.creating-types"></a>

Go용 Ion 라이브러리는 현재 문서 객체 모델(DOM)을 지원하지 않으므로 Ion 데이터 유형을 만들 수 없습니다. 하지만 QLDB를 사용할 때는 Go 네이티브 유형과 Ion 이진 사이를 마샬링 및 마샬링 취소할 수 있습니다.

### Ion 이진 가져오기
<a name="cookbook-golang.ion.getting-binary"></a>

```
aDict := map[string]interface{}{
  "GovId": "TOYENC486FH",
  "FirstName": "Brent",
}

ionBytes, err := ion.MarshalBinary(aDict)
if err != nil {
  panic(err)
}

fmt.Println(ionBytes) // prints [224 1 0 234 238 151 129 131 222 147 135 190 144 133 71 111 118 73 100 137 70 105 114 115 116 78 97 109 101 222 148 138 139 84 79 89 69 78 67 52 56 54 70 72 139 133 66 114 101 110 116]
```

### Ion 텍스트 가져오기
<a name="cookbook-golang.ion.getting-text"></a>

```
aDict := map[string]interface{}{
  "GovId": "TOYENC486FH",
  "FirstName": "Brent",
}

ionBytes, err := ion.MarshalText(aDict)
if err != nil {
  panic(err)
}

fmt.Println(string(ionBytes)) // prints {FirstName:"Brent",GovId:"TOYENC486FH"}
```

Ion에 대한 자세한 정보는 GitHub의 [Amazon Ion 설명서](http://amzn.github.io/ion-docs/)를 참조하세요. QLDB에서 Ion을 사용하는 방법에 대한 추가 코드 예제는 [Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업](driver-working-with-ion.md) 섹션을 참조하세요.

# Node.js용 Amazon QLDB 드라이버
<a name="getting-started.nodejs"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

원장의 데이터를 사용하려면 AWS 제공된 드라이버를 사용하여 Node.js 애플리케이션에서 Amazon QLDB에 연결할 수 있습니다. 다음 주제에서는 Node.js용 QLDB 드라이버를 시작하는 방법에 대해 설명합니다.

**Topics**
+ [

## 드라이버 리소스
](#getting-started.nodejs.resources)
+ [

## 사전 조건
](#getting-started.nodejs.prereqs)
+ [

## 설치
](#getting-started.nodejs.install)
+ [

## 설정 권장 사항
](#nodejs-setup)
+ [빠른 시작 자습서](driver-quickstart-nodejs.md)
+ [Cookbook 참조](driver-cookbook-nodejs.md)

## 드라이버 리소스
<a name="getting-started.nodejs.resources"></a>

Node.js 드라이버에서 지원하는 기능에 대한 자세한 정보는 다음 리소스를 참조하세요.
+ API 참조: [3.x](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/3.1.0/index.html), [2.x](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/index.html), [1.x](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/1.0.0/index.html)
+ [드라이버 소스 코드(GitHub)](https://github.com/awslabs/amazon-qldb-driver-nodejs)
+ [샘플 애플리케이션 소스 코드(GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs)
+ [Amazon Ion 코드 예제](ion.code-examples.md)
+ [(AWS 블로그)를 사용하여 AWS Lambda QLDB에서 간단한 CRUD 작업 및 데이터 스트림 구축](https://aws.amazon.com/blogs/database/build-a-simple-crud-operation-and-data-stream-on-amazon-qldb-using-aws-lambda/)

## 사전 조건
<a name="getting-started.nodejs.prereqs"></a>

Node.js용 QLDB 드라이버를 시작하기 전에 다음을 진행해야 합니다.

1. 의 AWS 설정 지침을 따릅니다[Amazon QLDB 액세스](accessing.md). 다음 내용이 포함됩니다:

   1. 가입합니다 AWS.

   1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

   1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

1. [Node.js 다운로드](https://nodejs.org/en/download/) 사이트에서 Node.js 버전 14.x 이상을 설치하세요. (이전 버전의 드라이버는 Node.js 버전 10.x 이상을 지원합니다.)

1. [Node.js의 JavaScript용AWS SDK](https://aws.amazon.com/sdk-for-node-js)에 대한 개발 환경을 구성합니다.

   1.  AWS 자격 증명을 설정합니다. 공유 보안 인증 파일을 생성할 것을 권장합니다.

      지침은 *AWS SDK for JavaScript 개발자 안내서*의 [공유 보안 인증 파일에서 Node.js 보안 인증 로드](https://docs.aws.amazon.com/sdk-for-javascript/latest/developer-guide/loading-node-credentials-shared.html)를 참조하세요.

   1. 기본 AWS 리전을 설정하세요. 사용 방법을 배우려면 [AWS 리전설정](https://docs.aws.amazon.com/sdk-for-javascript/latest/developer-guide/setting-region.html)을 참조하세요.

      사용 가능한 리전의 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

다음으로 전체 자습서 샘플 애플리케이션을 다운로드하거나 Node.js 프로젝트에 드라이버만 설치하고 단축 코드 예제를 실행할 수 있습니다.
+ 기존 프로젝트에서 Node.js에 QLDB 드라이버와 AWS SDK for JavaScript를 설치하려면 로 진행합니다[설치](#getting-started.nodejs.install).
+ 프로젝트를 설정하고 원장에 대한 기본 데이터 트랜잭션을 보여주는 단축 코드 예제를 실행하려면 [빠른 시작 자습서](driver-quickstart-nodejs.md)를 참조하세요.
+ 전체 자습서 샘플 애플리케이션에서 데이터 및 관리 API 작업에 대한 보다 심층적인 예제를 실행하려면 [Node.js 자습서](getting-started.nodejs.tutorial.md)를 참조하세요.

## 설치
<a name="getting-started.nodejs.install"></a>

QLDB는 다음 드라이버 버전과 해당 Node.js 종속성을 지원합니다.


****  

| 드라이버 버전 | Node.js 버전 | 상태 표시기 | 최근 릴리스 날짜 | 
| --- | --- | --- | --- | 
| [1.x](https://www.npmjs.com/package/amazon-qldb-driver-nodejs/v/1.0.0) | 10.x 이상 | 프로덕션 릴리스 | 2020년 6월 5일 | 
|  [2.x](https://www.npmjs.com/package/amazon-qldb-driver-nodejs/v/2.2.0) | 10.x 이상 | 프로덕션 릴리스 | 2021년 5월 6일 | 
| [3.x](https://www.npmjs.com/package/amazon-qldb-driver-nodejs/v/3.1.0) | 14.x 이상 | 프로덕션 릴리스 | 2023년 11월 10일 | 

[npm(Node.js 패키지 관리자)](https://www.npmjs.com/)을 사용하여 QLDB 드라이버를 설치하려면 프로젝트 루트 디렉터리에서 다음 명령을 입력합니다.

------
#### [ 3.x ]

```
npm install amazon-qldb-driver-nodejs
```

------
#### [ 2.x ]

```
npm install amazon-qldb-driver-nodejs@2.2.0
```

------
#### [ 1.x ]

```
npm install amazon-qldb-driver-nodejs@1.0.0
```

------

드라이버는 다음 패키지에 대한 피어 종속성을 가집니다. 또한 이러한 패키지를 프로젝트에 종속성으로 설치해야 합니다.

------
#### [ 3.x ]

모듈식 집계 QLDB 클라이언트(관리 API)

```
npm install @aws-sdk/client-qldb
```

모듈식 집계 *QLDB 세션* 클라이언트(데이터 API)

```
npm install @aws-sdk/client-qldb-session
```

Amazon Ion 데이터 형식

```
npm install ion-js
```

`BigInt`의 순수 JavaScript 구현

```
npm install jsbi
```

------
#### [ 2.x ]

AWS SDK for JavaScript

```
npm install aws-sdk
```

Amazon Ion 데이터 형식

```
npm install ion-js@4.0.0
```

`BigInt`의 순수 JavaScript 구현

```
npm install jsbi@3.1.1
```

------
#### [ 1.x ]

AWS SDK for JavaScript

```
npm install aws-sdk
```

Amazon Ion 데이터 형식

```
npm install ion-js@4.0.0
```

`BigInt`의 순수 JavaScript 구현

```
npm install jsbi@3.1.1
```

------

**드라이버를 사용하여 원장에 연결**

그런 다음 드라이버를 가져와서 원장에 연결하는 데 사용할 수 있습니다. 다음 TypeScript 코드 예제에서는 지정된 원장 이름 및 AWS 리전에 대한 드라이버 인스턴스를 생성하는 방법을 보여줍니다.

------
#### [ 3.x ]

```
import { Agent } from 'https';
import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session";
import { QldbDriver, RetryConfig  } from 'amazon-qldb-driver-nodejs';
import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler";

const maxConcurrentTransactions: number = 10;
const retryLimit: number = 4;

//Reuse connections with keepAlive
const lowLevelClientHttpOptions: NodeHttpHandlerOptions = {
    httpAgent: new Agent({
      maxSockets: maxConcurrentTransactions
    })
};

const serviceConfigurationOptions: QLDBSessionClientConfig = {
    region: "us-east-1"
};

//Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
const retryConfig: RetryConfig = new RetryConfig(retryLimit);
const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, lowLevelClientHttpOptions, maxConcurrentTransactions, retryConfig);

qldbDriver.getTableNames().then(function(tableNames: string[]) {
    console.log(tableNames);
});
```

------
#### [ 2.x ]

```
import { Agent } from 'https';
import { QldbDriver, RetryConfig  } from 'amazon-qldb-driver-nodejs';

const maxConcurrentTransactions: number = 10;
const retryLimit: number = 4;

//Reuse connections with keepAlive
const agentForQldb: Agent = new Agent({
    keepAlive: true,
    maxSockets: maxConcurrentTransactions
});

const serviceConfigurationOptions = {
    region: "us-east-1",
    httpOptions: {
        agent: agentForQldb
    }
};

//Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
const retryConfig: RetryConfig = new RetryConfig(retryLimit);
const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);

qldbDriver.getTableNames().then(function(tableNames: string[]) {
    console.log(tableNames);
});
```

------
#### [ 1.x ]

```
import { Agent } from 'https';
import { QldbDriver } from 'amazon-qldb-driver-nodejs';

const poolLimit: number = 10;
const retryLimit: number = 4;

//Reuse connections with keepAlive
const agentForQldb: Agent = new Agent({
    keepAlive: true,
    maxSockets: poolLimit
});

const serviceConfigurationOptions = {
    region: "us-east-1",
    httpOptions: {
        agent: agentForQldb
    }
};

const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, retryLimit, poolLimit);
qldbDriver.getTableNames().then(function(tableNames: string[]) {
    console.log(tableNames);
});
```

------

원장에서 기본 데이터 트랜잭션을 실행하는 방법에 대한 단축 코드 예제는 [Cookbook 참조](driver-cookbook-nodejs.md)를 참조하세요.

## 설정 권장 사항
<a name="nodejs-setup"></a>

### 연결 유지를 통한 연결 재사용
<a name="nodejs-setup.keepalive"></a>

#### QLDB Node.js 드라이버 v3
<a name="nodejs-setup.keepalive-v3"></a>

기본 Node.js HTTP/HTTPS 에이전트는 모든 새 요청에 대해 새로운 TCP 연결을 생성합니다. 새 연결을 설정하는 데 드는 비용을 피하기 위해 AWS SDK for JavaScript v3에서는 기본적으로 TCP 연결을 재사용합니다. 자세한 내용과 연결 재사용을 비활성화하는 방법을 알아보려면 *AWS SDK for JavaScript 개발자 안내서*의 [Node.js에서 연결 유지를 이용해 연결 재사용](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-reusing-connections.html)을 참조하세요.

Node.js용 QLDB 드라이버의 연결을 재사용하려면 기본 설정을 사용할 것을 권장합니다. 드라이버 초기화 중에 하위 수준 클라이언트 HTTP 옵션 `maxSockets`을 `maxConcurrentTransactions`에 설정한 값과 동일하게 설정합니다.

예를 들어, 다음 JavaScript 또는 TypeScript 코드를 참조하세요.

------
#### [ JavaScript ]

```
const qldb = require('amazon-qldb-driver-nodejs');
const https = require('https');

//Replace this value as appropriate for your application
const maxConcurrentTransactions = 50;

const agentForQldb = new https.Agent({
    //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
    //Do not rely on the default value of `Infinity`
    "maxSockets": maxConcurrentTransactions
});

const lowLevelClientHttpOptions = {
    httpAgent: agentForQldb
}

let driver = new qldb.QldbDriver("testLedger", undefined, lowLevelClientHttpOptions, maxConcurrentTransactions);
```

------
#### [ TypeScript ]

```
import { Agent } from 'https';
import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler";
import { QldbDriver } from 'amazon-qldb-driver-nodejs';

//Replace this value as appropriate for your application
const maxConcurrentTransactions: number = 50;

const agentForQldb: Agent = new Agent({
    //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
    //Do not rely on the default value of `Infinity`
    maxSockets: maxConcurrentTransactions
});

const lowLevelClientHttpOptions: NodeHttpHandlerOptions = {
    httpAgent: agentForQldb
};

let driver = new QldbDriver("testLedger", undefined, lowLevelClientHttpOptions, maxConcurrentTransactions);
```

------

#### QLDB Node.js 드라이버 v2
<a name="nodejs-setup.keepalive-v2"></a>

기본 Node.js HTTP/HTTPS 에이전트는 모든 새 요청에 대해 새로운 TCP 연결을 생성합니다. 새 연결 설정 비용이 발생하지 않게 하려면 기존 연결을 재사용할 것을 권장합니다.

Node.js용 QLDB 드라이버의 연결을 재사용하려면 다음 옵션 중 하나를 사용세요.
+ 드라이버 초기화 중에 다음과 같은 하위 수준 클라이언트 HTTP 옵션을 설정하세요.
  + `keepAlive` – `true`
  + `maxSockets` - `maxConcurrentTransactions`에 설정한 것과 동일한 값

  예를 들어, 다음 JavaScript 또는 TypeScript 코드를 참조하세요.

------
#### [ JavaScript ]

  ```
  const qldb = require('amazon-qldb-driver-nodejs');
  const https = require('https');
  
  //Replace this value as appropriate for your application
  const maxConcurrentTransactions = 50;
  
  const agentForQldb = new https.Agent({
      "keepAlive": true,
      //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
      //Do not rely on the default value of `Infinity`
      "maxSockets": maxConcurrentTransactions
  });
  
  const serviceConfiguration = { "httpOptions": {
      "agent": agentForQldb
  }};
  
  let driver = new qldb.QldbDriver("testLedger", serviceConfiguration, maxConcurrentTransactions);
  ```

------
#### [ TypeScript ]

  ```
  import { Agent } from 'https';
  import { ClientConfiguration } from 'aws-sdk/clients/acm';
  import { QldbDriver } from 'amazon-qldb-driver-nodejs';
  
  //Replace this value as appropriate for your application
  const maxConcurrentTransactions: number = 50;
  
  const agentForQldb: Agent = new Agent({
      keepAlive: true,
      //Set this to the same value as `maxConcurrentTransactions`(previously called `poolLimit`)
      //Do not rely on the default value of `Infinity`
      maxSockets: maxConcurrentTransactions
  });
  
  const serviceConfiguration: ClientConfiguration = { httpOptions: {
      agent: agentForQldb
  }};
  
  let driver = new QldbDriver("testLedger", serviceConfiguration, maxConcurrentTransactions);
  ```

------
+ 또는 `AWS_NODEJS_CONNECTION_REUSE_ENABLED` 환경 변수를 `1`로 설정할 수 있습니다. 자세한 정보는 *AWS SDK for JavaScript 개발자 안내서*의 [Node.js에서 연결 유지를 통한 연결 재사용](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html)을 참조하세요.
**참고**  
이 환경 변수를 설정하면 AWS SDK for JavaScript를 사용하는 모든 AWS 서비스 에게 영향을 줍니다.

# Node.js용 Amazon QLDB 드라이버 - 빠른 시작 자습서
<a name="driver-quickstart-nodejs"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 Node.js 용 Amazon QLDB 드라이버를 사용하여 간단한 애플리케이션을 설정하는 방법을 알아봅니다. 이 가이드에는 드라이버 설치 단계 및 기본적인 CRUD(*생성, 읽기, 업데이트 및 삭제*) 작업에 대한 간단한 JavaScript 및 TypeScript 코드 예가 포함되어 있습니다. 전체 샘플 애플리케이션에서 이러한 작업을 보여 주는 자세한 예를 보려면 [Node.js 자습서](getting-started.nodejs.tutorial.md) 섹션을 참조하세요.

**참고**  
해당하는 경우 일부 단계에는 Node.js.용 QLDB 드라이버의 지원되는 각 주요 버전마다 다른 코드 예가 있습니다.

**Topics**
+ [

## 사전 조건
](#driver-quickstart-nodejs.prereqs)
+ [

## 1단계: 프로젝트 설정
](#driver-quickstart-nodejs.step-1)
+ [

## 2단계: 드라이버 초기화
](#driver-quickstart-nodejs.step-2)
+ [

## 3단계: 테이블 및 인덱스 생성
](#driver-quickstart-nodejs.step-3)
+ [

## 4단계: 문서 삽입
](#driver-quickstart-nodejs.step-4)
+ [

## 5단계: 문서 쿼리
](#driver-quickstart-nodejs.step-5)
+ [

## 6단계: 문서 업데이트
](#driver-quickstart-nodejs.step-6)
+ [

## 전체 애플리케이션 실행
](#driver-quickstart-nodejs.complete)

## 사전 조건
<a name="driver-quickstart-nodejs.prereqs"></a>

시작하기 전에 다음을 수행해야 합니다.

1. Node.js. 드라이버에 대한 [사전 조건](getting-started.nodejs.md#getting-started.nodejs.prereqs)을 아직 완료하지 않은 경우, 완료하세요. 여기에는 가입 AWS, 개발을 위한 프로그래밍 방식 액세스 권한 부여, Node.js 설치가 포함됩니다.

1. `quick-start`라는 명칭의 원장을 생성합니다.

   원장 생성 방법을 알아보려면 *콘솔 시작하기*의 [Amazon QLDB 원장의 기본 작업](ledger-management.basics.md) 또는 [1단계: 새 원장 생성](getting-started-step-1.md) 섹션을 참조하세요.

TypeScript를 사용하는 경우 다음 설정 단계도 수행해야 합니다.

### TypeScript 사용
<a name="driver-quickstart-nodejs.prereqs.ts"></a>

**TypeScript를 설치하려면**

1. TypeScript 패키지를 설치합니다. QLDB 드라이버는 TypeScript 3.8.x에서 실행됩니다.

   ```
   $ npm install --global typescript@3.8.0
   ```

1. 패키지를 설치한 후 다음 명령을 실행하여 TypeScript 컴파일러가 설치되었는지 확인합니다.

   ```
   $ tsc --version
   ```

다음 단계에서 코드를 실행하려면 먼저 다음과 같이 TypeScript 파일을 실행 가능한 JavaScript 코드로 변환해야 합니다.

```
$ tsc app.ts; node app.js
```

## 1단계: 프로젝트 설정
<a name="driver-quickstart-nodejs.step-1"></a>

먼저 Node.js 프로젝트를 설정합니다.

1. 애플리케이션을 위한 폴더를 생성합니다.

   ```
   $ mkdir myproject
   $ cd myproject
   ```

1. 프로젝트를 초기화하려면 다음 `npm` 명령을 입력하고 설정 중에 나타나는 질문에 답하세요. 대부분의 질문에 기본값을 사용할 수 있습니다.

   ```
   $ npm init
   ```

1. Node.js용 Amazon QLDB 드라이버를 설치합니다.
   + 버전 3.x 사용

     ```
     $ npm install amazon-qldb-driver-nodejs --save
     ```
   + 버전 2.x 사용

     ```
     $ npm install amazon-qldb-driver-nodejs@2.2.0 --save
     ```
   + 버전 1.x 사용

     ```
     $ npm install amazon-qldb-driver-nodejs@1.0.0 --save
     ```

1. 드라이버의 피어 종속 항목을 설치합니다.
   + 버전 3.x 사용

     ```
     $ npm install @aws-sdk/client-qldb-session --save
     $ npm install ion-js --save
     $ npm install jsbi --save
     ```
   + 버전 2.x 또는 1.x 사용

     ```
     $ npm install aws-sdk --save
     $ npm install ion-js@4.0.0 --save
     $ npm install jsbi@3.1.1 --save
     ```

1. `app.js`라는 명칭의 JavaScript용 새 파일 또는 `app.ts`라는 명칭의 TypeScript용 새 파일을 생성합니다.

   그런 다음, 다음 단계의 코드 예를 점진적으로 추가하여 몇 가지 기본 CRUD 작업을 시도해 보세요. 또는 단계별 자습서를 건너뛰고 [전체 애플리케이션](#driver-quickstart-nodejs.complete)을 실행할 수도 있습니다.

## 2단계: 드라이버 초기화
<a name="driver-quickstart-nodejs.step-2"></a>

`quick-start`라는 명칭의 원장에 연결되는 드라이버의 인스턴스를 초기화합니다. 다음 코드를 `app.js` 또는 `app.ts` 파일에 추가합니다.

### 버전 3.x 사용
<a name="driver-quickstart-nodejs.step-2-v3"></a>

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');
var https = require('https');

function main() {
    const maxConcurrentTransactions = 10;
    const retryLimit = 4;

    const agentForQldb = new https.Agent({
        maxSockets: maxConcurrentTransactions
    });

    const lowLevelClientHttpOptions = {
       httpAgent: agentForQldb
    }

    const serviceConfigurationOptions = {
        region: "us-east-1"
    };

    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    var retryConfig = new qldb.RetryConfig(retryLimit);
    var driver = new qldb.QldbDriver("quick-start", serviceConfigurationOptions, lowlevelClientHttpOptions, maxConcurrentTransactions, retryConfig);
}

main();
```

------
#### [ TypeScript ]

```
import { Agent } from "https";
import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler";
import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session";
import { QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs";

function main(): void {
    const maxConcurrentTransactions: number = 10;
    const agentForQldb: Agent = new Agent({
        maxSockets: maxConcurrentTransactions
    });

    const lowLevelClientHttpOptions: NodeHttpHandlerOptions = {
      httpAgent: agentForQldb
    };

    const serviceConfigurationOptions: QLDBSessionClientConfig = {
        region: "us-east-1"
    };

    const retryLimit: number = 4;
    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    const retryConfig: RetryConfig = new RetryConfig(retryLimit);
    const driver: QldbDriver = new QldbDriver("quick-start", serviceConfigurationOptions, lowLevelClientHttpOptions, maxConcurrentTransactions, retryConfig);
}

if (require.main === module) {
    main();
}
```

------

**참고**  
이 코드 예에서는 *us-east-1*을 원장을 생성한 AWS 리전 으로 바꿉니다.
간소화를 위해 이 가이드의 나머지 코드 예에서는 다음 예 버전 1.x에 지정된 대로 기본 설정이 있는 드라이버를 사용합니다. 사용자 지정 `RetryConfig` 대신 자체 드라이버 인스턴스를 사용할 수도 있습니다.

### 버전 2.x 사용
<a name="driver-quickstart-nodejs.step-2-v2"></a>

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');
var https = require('https');

function main() {
    var maxConcurrentTransactions = 10;
    var retryLimit = 4;

    var agentForQldb = new https.Agent({
        keepAlive: true,
        maxSockets: maxConcurrentTransactions
    });

    var serviceConfigurationOptions = {
        region: "us-east-1",
        httpOptions: {
            agent: agentForQldb
        }
    };

    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    var retryConfig = new qldb.RetryConfig(retryLimit);
    var driver = new qldb.QldbDriver("quick-start", serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);
}

main();
```

------
#### [ TypeScript ]

```
import { QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs";
import { ClientConfiguration } from "aws-sdk/clients/acm";
import { Agent } from "https";

function main(): void {
    const maxConcurrentTransactions: number = 10;
    const agentForQldb: Agent = new Agent({
        keepAlive: true,
        maxSockets: maxConcurrentTransactions
    });
    const serviceConfigurationOptions: ClientConfiguration = {
        region: "us-east-1",
        httpOptions: {
            agent: agentForQldb
        }
    };
    const retryLimit: number = 4;
    // Use driver's default backoff function for this example (no second parameter provided to RetryConfig)
    const retryConfig: RetryConfig = new RetryConfig(retryLimit);
    const driver: QldbDriver = new QldbDriver("quick-start", serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);
}

if (require.main === module) {
    main();
}
```

------

**참고**  
이 코드 예에서는 *us-east-1*을 원장을 생성한 AWS 리전 으로 바꿉니다.
버전 2.x에는 `QldbDriver` 초기화를 위한 새로운 옵션 파라미터 `RetryConfig`가 도입되었습니다.
간소화를 위해 이 가이드의 나머지 코드 예에서는 다음 예 버전 1.x에 지정된 대로 기본 설정이 있는 드라이버를 사용합니다. 사용자 지정 `RetryConfig` 대신 자체 드라이버 인스턴스를 사용할 수도 있습니다.
이 코드 예는 연결 유지 옵션을 설정하여 기존 연결을 재사용하는 드라이버를 초기화합니다. 자세한 설명은 Node.js 드라이버에 대한 [설정 권장 사항](getting-started.nodejs.md#nodejs-setup) 섹션을 참조하세요.

### 버전 1.x 사용
<a name="driver-quickstart-nodejs.step-2-v1"></a>

------
#### [ JavaScript ]

```
const qldb = require('amazon-qldb-driver-nodejs');

function main() {
    // Use default settings
    const driver = new qldb.QldbDriver("quick-start");
}

main();
```

------
#### [ TypeScript ]

```
import { QldbDriver } from "amazon-qldb-driver-nodejs";

function main(): void {
    // Use default settings
    const driver: QldbDriver = new QldbDriver("quick-start");
}

if (require.main === module) {
    main();
}
```

------

**참고**  
`AWS_REGION` 환경 변수를 설정하여 지역을 지정할 수 있습니다. 자세한 설명은 *AWS SDK for JavaScript 개발자 가이드*의 [AWS 리전의 설정](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-region.html)을 참조하세요.

## 3단계: 테이블 및 인덱스 생성
<a name="driver-quickstart-nodejs.step-3"></a>

다음 코드 예는 `CREATE TABLE` 및 `CREATE INDEX` 문을 실행하는 방법을 보여줍니다.

1. `People`라는 일므의 표를 생성하는 다음 함수를 추가합니다.

------
#### [ JavaScript ]

   ```
   async function createTable(txn) {
       await txn.execute("CREATE TABLE People");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function createTable(txn: TransactionExecutor): Promise<void> {
       await txn.execute("CREATE TABLE People");
   }
   ```

------

1. `People` 표의 `firstName` 필드에 대한 인덱스를 만드는 다음 함수를 추가합니다. [인덱스](ql-reference.create-index.md)는 쿼리 성능을 최적화하고 [OCC(낙관적 동시성 제어)](concurrency.md) 충돌 예외를 제한하는 데 필요합니다.

------
#### [ JavaScript ]

   ```
   async function createIndex(txn) {
       await txn.execute("CREATE INDEX ON People (firstName)");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function createIndex(txn: TransactionExecutor): Promise<void> {
       await txn.execute("CREATE INDEX ON People (firstName)");
   }
   ```

------

1. `main` 함수에서는 먼저 `createTable`를 호출한 다음 `createIndex`를 호출합니다.

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn) => {
           console.log("Create table People");
           await createTable(txn);
           console.log("Create index on firstName");
           await createIndex(txn);
       });
   
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Create table People");
           await createTable(txn);
           console.log("Create index on firstName");
           await createIndex(txn);
       });
   
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

1. 코드를 실행하여 표와 인덱스를 생성합니다.

------
#### [ JavaScript ]

   ```
   $ node app.js
   ```

------
#### [ TypeScript ]

   ```
   $ tsc app.ts; node app.js
   ```

------

## 4단계: 문서 삽입
<a name="driver-quickstart-nodejs.step-4"></a>

다음 코드 예에서는 `INSERT` 문을 실행하는 방법을 보여줍니다. QLDB는 [PartiQL](ql-reference.md) 쿼리 언어(SQL 호환) 및 [Amazon Ion](ion.md) 데이터 형식(JSON의 상위 집합)을 지원합니다.

1. `People` 표에 문서를 삽입하는 다음 함수를 추가합니다.

------
#### [ JavaScript ]

   ```
   async function insertDocument(txn) {
       const person = {
           firstName: "John",
           lastName: "Doe",
           age: 42
       };
       await txn.execute("INSERT INTO People ?", person);
   }
   ```

------
#### [ TypeScript ]

   ```
   async function insertDocument(txn: TransactionExecutor): Promise<void> {
       const person: Record<string, any> = {
           firstName: "John",
           lastName: "Doe",
           age: 42
       };
       await txn.execute("INSERT INTO People ?", person);
   }
   ```

------

   이 예에서는 물음표(`?`)를 변수 자리 표시자로 사용하여 문서 정보를 해당 문에 전달합니다. 이 `execute` 메서드는 Amazon Ion 타입과 Node.js 네이티브 타입 모두의 값을 지원합니다.
**작은 정보**  
단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.  

   ```
   // people is a list
   txn.execute("INSERT INTO People ?", people);
   ```
목록을 전달할 때 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

1. `main`함수에서 `createTable` 및 `createIndex` 호출을 제거하고 `insertDocument`에 대한 호출을 추가합니다.

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn) => {
           console.log("Insert document");
           await insertDocument(txn);
       });
   
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Insert document");
           await insertDocument(txn);
       });
   
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

## 5단계: 문서 쿼리
<a name="driver-quickstart-nodejs.step-5"></a>

다음 코드 예에서는 `SELECT` 문을 실행하는 방법을 보여줍니다.

1. `People` 표에서 문서를 쿼리하는 다음 함수를 추가합니다.

------
#### [ JavaScript ]

   ```
   async function fetchDocuments(txn) {
       return await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function fetchDocuments(txn: TransactionExecutor): Promise<dom.Value[]> {
       return (await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John")).getResultList();
   }
   ```

------

1. `main` 함수에서 `insertDocument` 호출 뒤에 다음 `fetchDocuments` 호출을 추가합니다.

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       var resultList = await driver.executeLambda(async (txn) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           var result = await fetchDocuments(txn);
           return result.getResultList();
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       const resultList: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           return await fetchDocuments(txn);
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

## 6단계: 문서 업데이트
<a name="driver-quickstart-nodejs.step-6"></a>

다음 코드 예에서는 `UPDATE` 문을 실행하는 방법을 보여줍니다.

1. `lastName`를 `"Stiles"`으로 변경함으로써 `People` 표의 문서를 업데이트하는 다음 함수를 추가합니다.

------
#### [ JavaScript ]

   ```
   async function updateDocuments(txn) {
       await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
   }
   ```

------
#### [ TypeScript ]

   ```
   async function updateDocuments(txn: TransactionExecutor): Promise<void> {
       await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
   }
   ```

------

1. `main` 함수에서 `fetchDocuments` 호출 뒤에 다음 `updateDocuments` 호출을 추가합니다. 그런 다음 `fetchDocuments`를 다시 호출하여 업데이트된 결과를 확인합니다.

------
#### [ JavaScript ]

   ```
   const qldb = require('amazon-qldb-driver-nodejs');
   
   async function main() {
       // Use default settings
       const driver = new qldb.QldbDriver("quick-start");
   
       var resultList = await driver.executeLambda(async (txn) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           await fetchDocuments(txn);
           console.log("Update document");
           await updateDocuments(txn);
           console.log("Fetch document after update");
           var result = await fetchDocuments(txn);
           return result.getResultList();
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   main();
   ```

------
#### [ TypeScript ]

   ```
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   async function main(): Promise<void> {
       // Use default settings
       const driver: QldbDriver = new QldbDriver("quick-start");
   
       const resultList: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor) => {
           console.log("Insert document");
           await insertDocument(txn);
           console.log("Fetch document");
           await fetchDocuments(txn);
           console.log("Update document");
           await updateDocuments(txn);
           console.log("Fetch document after update");
           return await fetchDocuments(txn);
       });
   
       // Pretty print the result list
       console.log("The result List is ", JSON.stringify(resultList, null, 2));
       driver.close();
   }
   
   if (require.main === module) {
       main();
   }
   ```

------

1. 코드를 실행하여 문서를 삽입, 쿼리 및 업데이트합니다.

------
#### [ JavaScript ]

   ```
   $ node app.js
   ```

------
#### [ TypeScript ]

   ```
   $ tsc app.ts; node app.js
   ```

------

## 전체 애플리케이션 실행
<a name="driver-quickstart-nodejs.complete"></a>

다음 코드 예는 `app.js` 및 `app.ts`의 전체 버전입니다. 이전 단계를 개별적으로 수행하는 대신 이 코드를 처음부터 끝까지 실행할 수도 있습니다. 이 애플리케이션은 `quick-start`이라는 명칭의 원장에 대한 몇 가지 기본 CRUD 작업을 보여줍니다.

**참고**  
이 코드를 실행하기 전에 `quick-start` 원장에 `People`이라는 명칭의 활성 테이블이 아직 없는지 확인하세요.

------
#### [ JavaScript ]

```
const qldb = require('amazon-qldb-driver-nodejs');

async function createTable(txn) {
    await txn.execute("CREATE TABLE People");
}

async function createIndex(txn) {
    await txn.execute("CREATE INDEX ON People (firstName)");
}

async function insertDocument(txn) {
    const person = {
        firstName: "John",
        lastName: "Doe",
        age: 42
    };
    await txn.execute("INSERT INTO People ?", person);
}

async function fetchDocuments(txn) {
    return await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John");
}

async function updateDocuments(txn) {
    await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
}

async function main() {
    // Use default settings
    const driver = new qldb.QldbDriver("quick-start");

    var resultList = await driver.executeLambda(async (txn) => {
        console.log("Create table People");
        await createTable(txn);
        console.log("Create index on firstName");
        await createIndex(txn);
        console.log("Insert document");
        await insertDocument(txn);
        console.log("Fetch document");
        await fetchDocuments(txn);
        console.log("Update document");
        await updateDocuments(txn);
        console.log("Fetch document after update");
        var result = await fetchDocuments(txn);
        return result.getResultList();
    });

    // Pretty print the result list
    console.log("The result List is ", JSON.stringify(resultList, null, 2));
    driver.close();
}

main();
```

------
#### [ TypeScript ]

```
import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
import { dom } from "ion-js";

async function createTable(txn: TransactionExecutor): Promise<void> {
    await txn.execute("CREATE TABLE People");
}

async function createIndex(txn: TransactionExecutor): Promise<void> {
    await txn.execute("CREATE INDEX ON People (firstName)");
}

async function insertDocument(txn: TransactionExecutor): Promise<void> {
    const person: Record<string, any> = {
        firstName: "John",
        lastName: "Doe",
        age: 42
    };
    await txn.execute("INSERT INTO People ?", person);
}

async function fetchDocuments(txn: TransactionExecutor): Promise<dom.Value[]> {
    return (await txn.execute("SELECT firstName, age, lastName FROM People WHERE firstName = ?", "John")).getResultList();
}

async function updateDocuments(txn: TransactionExecutor): Promise<void> {
    await txn.execute("UPDATE People SET lastName = ? WHERE firstName = ?", "Stiles", "John");
};

async function main(): Promise<void> {
    // Use default settings
    const driver: QldbDriver = new QldbDriver("quick-start");

    const resultList: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor) => {
        console.log("Create table People");
        await createTable(txn);
        console.log("Create index on firstName");
        await createIndex(txn);
        console.log("Insert document");
        await insertDocument(txn);
        console.log("Fetch document");
        await fetchDocuments(txn);
        console.log("Update document");
        await updateDocuments(txn);
        console.log("Fetch document after update");
        return await fetchDocuments(txn);
    });

    // Pretty print the result list
    console.log("The result List is ", JSON.stringify(resultList, null, 2));
    driver.close();
}

if (require.main === module) {
    main();
}
```

------

전체 애플리케이션을 실행하려면 다음 명령을 입력하세요.

------
#### [ JavaScript ]

```
$ node app.js
```

------
#### [ TypeScript ]

```
$ tsc app.ts; node app.js
```

------

# Node.js용 Amazon QLDB 드라이버 - Cookbook 참조
<a name="driver-cookbook-nodejs"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 참조 가이드는 Node.js용 Amazon QLDB 드라이버의 일반적인 사용 사례를 보여줍니다. 이 가이드는 드라이버를 사용하여 기본 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행하는 방법을 보여주는 JavaScript 및 TypeScript 코드 예제를 안내합니다. 또한 Amazon Ion 데이터를 처리하기 위한 코드 예제도 포함되어 있습니다. 또한 이 가이드에서는 트랜잭션에 멱등성을 부여하고 고유성 제약하는 모범 사례를 중점적으로 설명합니다.

**Contents**
+ [

## 드라이버 가져오기
](#cookbook-nodejs.importing)
+ [

## 드라이버 인스턴스화
](#cookbook-nodejs.instantiating)
+ [

## CRUD 작업
](#cookbook-nodejs.crud)
  + [

### 테이블 생성
](#cookbook-nodejs.crud.creating-tables)
  + [

### 인덱스 생성
](#cookbook-nodejs.crud.creating-indexes)
  + [

### 문서 읽기
](#cookbook-nodejs.crud.reading)
    + [

#### 쿼리 파라미터 사용
](#cookbook-nodejs.reading-using-params)
  + [

### 문서 삽입하기
](#cookbook-nodejs.crud.inserting)
    + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-nodejs.crud.inserting.multiple)
  + [

### 문서 업데이트
](#cookbook-nodejs.crud.updating)
  + [

### 문서 삭제
](#cookbook-nodejs.crud.deleting)
  + [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-nodejs.crud.multi-statement)
  + [

### 재시도 로직
](#cookbook-nodejs.crud.retry-logic)
  + [

### 고유성 제약 조건 구현
](#cookbook-nodejs.crud.uniqueness-constraints)
+ [

## Amazon Ion 작업
](#cookbook-nodejs.ion)
  + [

### Ion 모듈 가져오기
](#cookbook-nodejs.ion.import)
  + [

### Ion 유형 생성
](#cookbook-nodejs.ion.creating-types)
  + [

### Ion 이진 덤프 가져오기
](#cookbook-nodejs.ion.getting-binary)
  + [

### Ion 텍스트 덤프 가져오기
](#cookbook-nodejs.ion.getting-text)

## 드라이버 가져오기
<a name="cookbook-nodejs.importing"></a>

다음 코드 예제에서는 드라이브를 가져옵니다.

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');
var ionjs = require('ion-js');
```

------
#### [ TypeScript ]

```
import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
import { dom, dumpBinary, load } from "ion-js";
```

------

**참고**  
이 예제에서는 Amazon Ion 패키지(`ion-js`)도 가져옵니다. 이 참조에서 일부 데이터 작업을 실행할 때 Ion 데이터를 처리하려면 이 패키지가 필요합니다. 자세한 내용은 [Amazon Ion 작업](#cookbook-nodejs.ion) 섹션을 참조하세요.

## 드라이버 인스턴스화
<a name="cookbook-nodejs.instantiating"></a>

다음 코드 예제는 기본 설정을 사용하여 지정된 원장 이름에 연결하는 드라이버 인스턴스를 만듭니다.

------
#### [ JavaScript ]

```
const qldbDriver = new qldb.QldbDriver("vehicle-registration");
```

------
#### [ TypeScript ]

```
const qldbDriver: QldbDriver = new QldbDriver("vehicle-registration");
```

------

## CRUD 작업
<a name="cookbook-nodejs.crud"></a>

QLDB는 트랜잭션의 일부로 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행합니다.

**주의**  
가장 좋은 방법은 쓰기 트랜잭션이 완전한 멱등성을 부여하는 것입니다.

**트랜잭션에 멱등성 부여하기**

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 좋습니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 *멱등성*을 가집니다.

이름이 `Person`인 테이블에 문서를 삽입하는 트랜잭션을 예로 들어 보겠습니다. 트랜잭션은 먼저 문서가 테이블에 이미 존재하는지 여부를 확인해야 합니다. 이렇게 확인하지 않으면 테이블에 문서가 중복될 수 있습니다.

QLDB가 서버 측에서 트랜잭션을 성공적으로 커밋했지만 응답을 기다리는 동안 클라이언트 제한 시간이 초과되었다고 가정해 보겠습니다. 트랜잭션이 멱등성을 가지지 않는 경우 재시도 시 동일한 문서가 두 번 이상 삽입될 수 있습니다.

**인덱스를 사용하여 전체 테이블 스캔 방지**

인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 이 인덱싱된 조회가 없으면 QLDB는 테이블 스캔을 수행해야 하며, 이로 인해 트랜잭션 제한 시간이 초과되거나 *OCC(낙관적 동시성 제어)* 충돌이 발생할 수 있습니다.

OCC에 대한 자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md) 단원을 참조하세요.

**암시적으로 생성된 트랜잭션**

[QldbDriver.executeLambda](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_qldbdriver_.qldbdriver.html#executelambda) 메서드는 [TransactionExecutor](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_transactionexecutor_.transactionexecutor.html)의 인스턴스를 수신하는 Lambda 함수를 받아들이며, 이 함수를 사용하여 명령문을 실행할 수 있습니다. `TransactionExecutor`의 인스턴스는 암시적으로 생성된 트랜잭션을 래핑합니다.

트랜잭션 실행자의 [execute](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_transactionexecutor_.transactionexecutor.html#execute) 메서드를 사용하여 lambda 함수 내에서 명령문을 실행할 수 있습니다. 드라이버는 Lambda 함수가 반환될 때 트랜잭션을 암시적으로 커밋합니다.

**참고**  
이 `execute` 메서드는 Amazon Ion 유형과 Node.js 네이티브 유형을 모두 지원합니다. Node.js 네이티브 유형을 인수로 `execute`에 전달하면 드라이버가 `ion-js` 패키지를 사용하여 해당 형식을 Ion 유형으로 변환합니다(지정된 Node.js 데이터 유형에 대한 변환이 지원되는 경우). 지원되는 데이터 유형 및 변환 규칙은 Ion JavaScript DOM [README](https://github.com/amzn/ion-js/blob/master/src/dom/README.md)를 참조하세요.

다음 섹션에서는 기본 CRUD 작업을 실행하고, 사용자 지정 재시도 로직을 지정하고, 고유성 제약 조건을 구현하는 방법을 보여줍니다.

**Contents**
+ [

### 테이블 생성
](#cookbook-nodejs.crud.creating-tables)
+ [

### 인덱스 생성
](#cookbook-nodejs.crud.creating-indexes)
+ [

### 문서 읽기
](#cookbook-nodejs.crud.reading)
  + [

#### 쿼리 파라미터 사용
](#cookbook-nodejs.reading-using-params)
+ [

### 문서 삽입하기
](#cookbook-nodejs.crud.inserting)
  + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-nodejs.crud.inserting.multiple)
+ [

### 문서 업데이트
](#cookbook-nodejs.crud.updating)
+ [

### 문서 삭제
](#cookbook-nodejs.crud.deleting)
+ [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-nodejs.crud.multi-statement)
+ [

### 재시도 로직
](#cookbook-nodejs.crud.retry-logic)
+ [

### 고유성 제약 조건 구현
](#cookbook-nodejs.crud.uniqueness-constraints)

### 테이블 생성
<a name="cookbook-nodejs.crud.creating-tables"></a>

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute("CREATE TABLE Person");
    });
})();
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('CREATE TABLE Person');
    });
}());
```

------

### 인덱스 생성
<a name="cookbook-nodejs.crud.creating-indexes"></a>

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute("CREATE INDEX ON Person (GovId)");
    });
})();
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('CREATE INDEX ON Person (GovId)');
    });
}());
```

------

### 문서 읽기
<a name="cookbook-nodejs.crud.reading"></a>

------
#### [ JavaScript ]

```
(async function() {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn) => {
        const results = (await txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

#### 쿼리 파라미터 사용
<a name="cookbook-nodejs.reading-using-params"></a>

다음 코드 예제는 네이티브 유형 쿼리 파라미터를 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn) => {
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    // Assumes that Person table has documents as follows:
    // { "GovId": "TOYENC486FH", "FirstName": "Brent" }
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

다음 코드 예제는 Ion 유형 쿼리 파라미터를 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const govId = ionjs.load("TOYENC486FH");

        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const govId: dom.Value = load("TOYENC486FH");

        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

다음 코드 예제는 여러 쿼리 파라미터를 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ? AND FirstName = ?', 'TOYENC486FH', 'Brent')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ? AND FirstName = ?', 'TOYENC486FH', 'Brent')).getResultList();
        for (let result of results) {
            console.log(result.get('GovId')); // prints [String: 'TOYENC486FH']
            console.log(result.get('FirstName')); // prints [String: 'Brent']
        }
    });
}());
```

------

다음 코드 예제는 쿼리 파라미터 목록을 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const govIds = ['TOYENC486FH','LOGANB486CG','LEWISR261LL'];
        /*
        Assumes that Person table has documents as follows:
        { "GovId": "TOYENC486FH", "FirstName": "Brent" }
        { "GovId": "LOGANB486CG", "FirstName": "Brent" }
        { "GovId": "LEWISR261LL", "FirstName": "Raul" }
        */
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId IN (?,?,?)', ...govIds)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId'));
            console.log(result.get('FirstName'));
            /*
            prints:
            [String: 'TOYENC486FH']
            [String: 'Brent']
            [String: 'LOGANB486CG']
            [String: 'Brent']
            [String: 'LEWISR261LL']
            [String: 'Raul']
            */
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const govIds: string[] = ['TOYENC486FH','LOGANB486CG','LEWISR261LL'];
        /*
        Assumes that Person table has documents as follows:
        { "GovId": "TOYENC486FH", "FirstName": "Brent" }
        { "GovId": "LOGANB486CG", "FirstName": "Brent" }
        { "GovId": "LEWISR261LL", "FirstName": "Raul" }
        */
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId IN (?,?,?)', ...govIds)).getResultList();
        for (let result of results) {
            console.log(result.get('GovId'));
            console.log(result.get('FirstName'));
            /*
            prints:
            [String: 'TOYENC486FH']
            [String: 'Brent']
            [String: 'LOGANB486CG']
            [String: 'Brent']
            [String: 'LEWISR261LL']
            [String: 'Raul']
            */
        }
    });
}());
```

------

**참고**  
인덱싱된 조회 없이 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. 이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 [인덱스](ql-reference.create-index.md)를 사용하는 것이 좋습니다. `GovId`에 인덱스를 사용하지 않으면 쿼리에 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삽입하기
<a name="cookbook-nodejs.crud.inserting"></a>

다음 코드 예제는 네이티브 데이터 유형을 삽입합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            await txn.execute('INSERT INTO Person ?', doc);
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc: Record<string, string> = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            await txn.execute('INSERT INTO Person ?', doc);
        }
    });
}());
```

------

다음 코드 예제는 Ion 데이터 유형을 삽입합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            // Create a sample Ion doc
            const ionDoc = ionjs.load(ionjs.dumpBinary(doc));

            await txn.execute('INSERT INTO Person ?', ionDoc);
        }
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        // Check if doc with GovId:TOYENC486FH exists
        // This is critical to make this transaction idempotent
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', 'TOYENC486FH')).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            const doc: Record<string, string> = {
                'FirstName': 'Brent',
                'GovId': 'TOYENC486FH',
            };
            // Create a sample Ion doc
            const ionDoc: dom.Value = load(dumpBinary(doc));

            await txn.execute('INSERT INTO Person ?', ionDoc);
        }
    });
}());
```

------

이 트랜잭션은 문서를 `Person` 테이블에 삽입합니다. 삽입하기 전에 먼저 문서가 테이블에 이미 있는지 확인합니다. **이 검사를 통해 트랜잭션은 본질적으로 멱등성을 가지게 됩니다.** 이 트랜잭션을 여러 번 실행하더라도 의도하지 않은 부작용이 발생하지는 않습니다.

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

#### 하나의 명령문에 여러 문서 삽입
<a name="cookbook-nodejs.crud.inserting.multiple"></a>

단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.

```
// people is a list
txn.execute("INSERT INTO People ?", people);
```

목록을 전달할 때 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

### 문서 업데이트
<a name="cookbook-nodejs.crud.updating"></a>

다음 코드 예제는 네이티브 데이터 유형을 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', 'John', 'TOYENC486FH');
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', 'John', 'TOYENC486FH');
    });
}());
```

------

다음 코드 예제는 Ion 데이터 유형을 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const firstName = ionjs.load("John");
        const govId = ionjs.load("TOYENC486FH");

        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', firstName, govId);
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const firstName: dom.Value = load("John");
        const govId: dom.Value = load("TOYENC486FH");

        await txn.execute('UPDATE Person SET FirstName = ? WHERE GovId = ?', firstName, govId);
    });
}());
```

------

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삭제
<a name="cookbook-nodejs.crud.deleting"></a>

다음 코드 예제는 네이티브 데이터 유형을 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute('DELETE FROM Person WHERE GovId = ?', 'TOYENC486FH');
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('DELETE FROM Person WHERE GovId = ?', 'TOYENC486FH');
    });
}());
```

------

다음 코드 예제는 Ion 데이터 유형을 사용합니다.

------
#### [ JavaScript ]

```
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        const govId = ionjs.load("TOYENC486FH");

        await txn.execute('DELETE FROM Person WHERE GovId = ?', govId);
    });
}());
```

------
#### [ TypeScript ]

```
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        const govId: dom.Value = load("TOYENC486FH");

        await txn.execute('DELETE FROM Person WHERE GovId = ?', govId);
    });
}());
```

------

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 하나의 트랜잭션에서 여러 명령문 실행
<a name="cookbook-nodejs.crud.multi-statement"></a>

------
#### [ TypeScript ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
async function insureCar(driver: QldbDriver, vin: string): Promise<boolean> {

    return await driver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)).getResultList();

        if (results.length > 0) {
            await txn.execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin);
            return true;
        }
        return false;
    });
};
```

------

### 재시도 로직
<a name="cookbook-nodejs.crud.retry-logic"></a>

드라이버의 `executeLambda` 메서드에는 재시도 가능한 예외(예: 시간 초과 또는 OCC 충돌)가 발생할 경우 트랜잭션을 재시도하는 재시도 메커니즘이 내장되어 있습니다. 최대 재시도 횟수와 백오프 전략을 구성할 수 있습니다.

기본 재시도 제한은 `4`이며, 기본 백오프 전략은 `10`밀리초 단위의 [defaultBackoffFunction](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/modules/_src_retry_defaultretryconfig_.html#defaultretryconfig)입니다. [RetryConfig](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/2.2.0/classes/_src_retry_retryconfig_.retryconfig.html) 인스턴스를 사용하여 드라이버 인스턴스별 및 트랜잭션별 재시도 구성을 설정할 수 있습니다.

다음 코드 예제는 드라이버 인스턴스에 대한 사용자 지정 재시도 제한 및 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다.

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');

// Configuring retry limit to 2
const retryConfig = new qldb.RetryConfig(2);
const qldbDriver = new qldb.QldbDriver("test-ledger", undefined, undefined, retryConfig);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff = (retryAttempt, error, transactionId) => {
    return 1000 * retryAttempt;
};

const retryConfigCustomBackoff = new qldb.RetryConfig(2, customBackoff);
const qldbDriverCustomBackoff = new qldb.QldbDriver("test-ledger", undefined, undefined, retryConfigCustomBackoff);
```

------
#### [ TypeScript ]

```
import { BackoffFunction, QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs"

// Configuring retry limit to 2
const retryConfig: RetryConfig = new RetryConfig(2);
const qldbDriver: QldbDriver = new QldbDriver("test-ledger", undefined, undefined, retryConfig);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff: BackoffFunction = (retryAttempt: number, error: Error, transactionId: string) => {
    return 1000 * retryAttempt;
};

const retryConfigCustomBackoff: RetryConfig = new RetryConfig(2, customBackoff);
const qldbDriverCustomBackoff: QldbDriver = new QldbDriver("test-ledger", undefined, undefined, retryConfigCustomBackoff);
```

------

다음 코드 예제는 특정 Lambda 실행에 대한 사용자 지정 재시도와 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다. `executeLambda`에 대한 이 구성은 드라이버 인스턴스에 설정된 재시도 로직을 재정의합니다.

------
#### [ JavaScript ]

```
var qldb = require('amazon-qldb-driver-nodejs');

// Configuring retry limit to 2
const retryConfig1 = new qldb.RetryConfig(2);
const qldbDriver = new qldb.QldbDriver("test-ledger", undefined, undefined, retryConfig1);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff = (retryAttempt, error, transactionId) => {
    return 1000 * retryAttempt;
};

const retryConfig2 = new qldb.RetryConfig(2, customBackoff);

// The config `retryConfig1` will be overridden by `retryConfig2`
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        await txn.execute('CREATE TABLE Person');
    }, retryConfig2);
}());
```

------
#### [ TypeScript ]

```
import { BackoffFunction, QldbDriver, RetryConfig, TransactionExecutor } from "amazon-qldb-driver-nodejs"

// Configuring retry limit to 2
const retryConfig1: RetryConfig = new RetryConfig(2);
const qldbDriver: QldbDriver = new QldbDriver("test-ledger", undefined, undefined, retryConfig1);

// Configuring a custom backoff which increases delay by 1s for each attempt.
const customBackoff: BackoffFunction = (retryAttempt: number, error: Error, transactionId: string) => {
    return 1000 * retryAttempt;
};

const retryConfig2: RetryConfig = new RetryConfig(2, customBackoff);

// The config `retryConfig1` will be overridden by `retryConfig2`
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        await txn.execute('CREATE TABLE Person');
    }, retryConfig2);
}());
```

------

### 고유성 제약 조건 구현
<a name="cookbook-nodejs.crud.uniqueness-constraints"></a>

QLDB는 고유 인덱스를 지원하지 않지만 애플리케이션에서 이 동작을 구현할 수 있습니다.

`Person` 테이블의 `GovId` 필드에 고유성 제약 조건을 구현하려고 한다고 가정해 보겠습니다. 이렇게 하면 다음 작업을 수행하는 트랜잭션을 작성합니다.

1. 테이블에 지정된 `GovId`가 있는 기존 문서가 없는지 확인합니다.

1. 어설션이 통과하면 문서를 삽입합니다.

경쟁 트랜잭션이 어설션을 동시에 통과하면 트랜잭션 중 하나만 성공적으로 커밋됩니다. 다른 트랜잭션은 OCC 충돌 예외가 발생하여 실패합니다.

다음 코드 예제는 이 고유성 제약 조건 구현 방법을 보여줍니다.

------
#### [ JavaScript ]

```
const govId = 'TOYENC486FH';
const document = {
    'FirstName': 'Brent',
    'GovId': 'TOYENC486FH',
};
(async function() {
    await qldbDriver.executeLambda(async (txn) => {
        // Check if doc with GovId = govId exists
        const results = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            await txn.execute('INSERT INTO Person ?', document);
        }
    });
})();
```

------
#### [ TypeScript ]

```
const govId: string = 'TOYENC486FH';
const document: Record<string, string> = {
    'FirstName': 'Brent',
    'GovId': 'TOYENC486FH',
};
(async function(): Promise<void> {
    await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
        // Check if doc with GovId = govId exists
        const results: dom.Value[] = (await txn.execute('SELECT * FROM Person WHERE GovId = ?', govId)).getResultList();
        // Insert the document after ensuring it doesn't already exist
        if (results.length == 0) {
            await txn.execute('INSERT INTO Person ?', document);
        }
    });
})();
```

------

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

## Amazon Ion 작업
<a name="cookbook-nodejs.ion"></a>

다음 섹션에서는 Amazon Ion 모듈을 사용하여 Ion 데이터를 처리하는 방법을 보여줍니다.

**Contents**
+ [

### Ion 모듈 가져오기
](#cookbook-nodejs.ion.import)
+ [

### Ion 유형 생성
](#cookbook-nodejs.ion.creating-types)
+ [

### Ion 이진 덤프 가져오기
](#cookbook-nodejs.ion.getting-binary)
+ [

### Ion 텍스트 덤프 가져오기
](#cookbook-nodejs.ion.getting-text)

### Ion 모듈 가져오기
<a name="cookbook-nodejs.ion.import"></a>

------
#### [ JavaScript ]

```
var ionjs = require('ion-js');
```

------
#### [ TypeScript ]

```
import { dom, dumpBinary, dumpText, load } from "ion-js";
```

------

### Ion 유형 생성
<a name="cookbook-nodejs.ion.creating-types"></a>

다음 코드 예제는 Ion 텍스트에서 Ion 객체를 만듭니다.

------
#### [ JavaScript ]

```
const ionText  = '{GovId: "TOYENC486FH", FirstName: "Brent"}';
const ionObj = ionjs.load(ionText);

console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------
#### [ TypeScript ]

```
const ionText: string = '{GovId: "TOYENC486FH", FirstName: "Brent"}';
const ionObj: dom.Value = load(ionText);

console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------

다음 코드 예제는 Node.js 사전에서 Ion 객체를 만듭니다.

------
#### [ JavaScript ]

```
const aDict = {
    'GovId': 'TOYENC486FH',
    'FirstName': 'Brent'
};
const ionObj = ionjs.load(ionjs.dumpBinary(aDict));
console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------
#### [ TypeScript ]

```
const aDict: Record<string, string> = {
    'GovId': 'TOYENC486FH',
    'FirstName': 'Brent'
};
const ionObj: dom.Value = load(dumpBinary(aDict));
console.log(ionObj.get('GovId')); // prints [String: 'TOYENC486FH']
console.log(ionObj.get('FirstName')); // prints [String: 'Brent']
```

------

### Ion 이진 덤프 가져오기
<a name="cookbook-nodejs.ion.getting-binary"></a>

------
#### [ JavaScript ]

```
// ionObj is an Ion struct
console.log(ionjs.dumpBinary(ionObj).toString()); // prints 224,1,0,234,238,151,129,131,222,147,135,190,144,133,71,111,118,73,100,137,70,105,114,115,116,78,97,109,101,222,148,138,139,84,79,89,69,78,67,52,56,54,70,72,139,133,66,114,101,110,116
```

------
#### [ TypeScript ]

```
// ionObj is an Ion struct
console.log(dumpBinary(ionObj).toString()); // prints 224,1,0,234,238,151,129,131,222,147,135,190,144,133,71,111,118,73,100,137,70,105,114,115,116,78,97,109,101,222,148,138,139,84,79,89,69,78,67,52,56,54,70,72,139,133,66,114,101,110,116
```

------

### Ion 텍스트 덤프 가져오기
<a name="cookbook-nodejs.ion.getting-text"></a>

------
#### [ JavaScript ]

```
// ionObj is an Ion struct
console.log(ionjs.dumpText(ionObj)); // prints {GovId:"TOYENC486FH",FirstName:"Brent"}
```

------
#### [ TypeScript ]

```
// ionObj is an Ion struct
console.log(dumpText(ionObj)); // prints {GovId:"TOYENC486FH",FirstName:"Brent"}
```

------

Ion에 대한 자세한 정보는 GitHub의 [Amazon Ion 설명서](http://amzn.github.io/ion-docs/)를 참조하세요. QLDB에서 Ion을 사용하는 방법에 대한 추가 코드 예제는 [Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업](driver-working-with-ion.md) 섹션을 참조하세요.

# Python용 Amazon QLDB 드라이버
<a name="getting-started.python"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

원장의 데이터를 사용하려면 AWS 제공된 드라이버를 사용하여 Python 애플리케이션에서 Amazon QLDB에 연결할 수 있습니다. 다음 주제에서는 Python용 QLDB 드라이버를 시작하는 방법을 설명합니다.

**Topics**
+ [

## 드라이버 리소스
](#getting-started.python.resources)
+ [

## 사전 조건
](#getting-started.python.prereqs)
+ [

## 설치
](#getting-started.python.install)
+ [빠른 시작 자습서](driver-quickstart-python.md)
+ [Cookbook 참조](driver-cookbook-python.md)

## 드라이버 리소스
<a name="getting-started.python.resources"></a>

Python 드라이버에서 지원하는 기능에 대한 자세한 내용은 다음 리소스를 참조하세요.
+ API 참조: [3.x](https://amazon-qldb-driver-python.readthedocs.io/en/latest/), [2.x](https://amazon-qldb-driver-python.readthedocs.io/en/v2.0.2/)
+ [드라이버 소스 코드(GitHub)](https://github.com/awslabs/amazon-qldb-driver-python)
+ [샘플 애플리케이션 소스 코드(GitHub)](https://github.com/aws-samples/amazon-qldb-dmv-sample-python)
+ [Amazon Ion 코드 예제](ion.code-examples.md)

## 사전 조건
<a name="getting-started.python.prereqs"></a>

Python용 QLDB 드라이버를 시작하기 전에 다음을 수행해야 합니다.

1. 의 AWS 설정 지침을 따릅니다[Amazon QLDB 액세스](accessing.md). 다음 내용이 포함됩니다:

   1. 가입합니다 AWS.

   1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

   1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

1. [Python 다운로드](https://www.python.org/downloads/) 사이트에서 다음 버전의 Python 중 하나를 설치합니다.
   + **3.6 이상** - Python v3용 QLDB 드라이버
   + **3.4 이상** - Python v2용 QLDB 드라이버

1.  AWS 자격 증명과 기본값을 설정합니다 AWS 리전. 지침은 AWS SDK for Python (Boto3) 설명서의 [Quickstart](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration)를 참조하세요.

   사용 가능한 리전의 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

다음으로 전체 자습서 샘플 애플리케이션을 다운로드하거나 Python 프로젝트에 드라이버만 설치하고 단축 코드 예제를 실행할 수 있습니다.
+ 기존 프로젝트에 QLDB 드라이버와 AWS SDK for Python (Boto3) 를 설치하려면 로 이동합니다[설치](#getting-started.python.install).
+ 프로젝트를 설정하고 원장에 대한 기본 데이터 트랜잭션을 보여주는 단축 코드 예제를 실행하려면 [빠른 시작 자습서](driver-quickstart-python.md)를 참조하세요.
+ 전체 자습서 샘플 애플리케이션에서 데이터 및 관리 API 작업에 대한 보다 심층적인 예제를 실행하려면 [Python 자습서](getting-started.python.tutorial.md)를 참조하세요.

## 설치
<a name="getting-started.python.install"></a>

QLDB는 다음 드라이버 버전과 해당 Python 종속성을 지원합니다.


****  

| 드라이버 버전 | Python 버전 | 상태 표시기 | 최근 릴리스 날짜 | 
| --- | --- | --- | --- | 
| [2.x](https://pypi.org/project/pyqldb/2.0.2/) | 3.4 이상 | 프로덕션 릴리스 | 2020년 5월 7일 | 
| [3.x](https://pypi.org/project/pyqldb/) | 3.6 이상 | 프로덕션 릴리스 | 2021년 10월 28일 | 

`pip`(Python용 패키지 관리자)를 사용하여 PyPI에서 QLDB 드라이버를 설치하려면 명령줄에 다음을 입력합니다.

------
#### [ 3.x ]

```
pip install pyqldb
```

------
#### [ 2.x ]

```
pip install pyqldb==2.0.2
```

------

드라이버를 설치하면 [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-python) 및 [Amazon Ion](ion.md) 패키지를 포함한 종속 항목도 설치됩니다.

**드라이버를 사용하여 원장에 연결**

그런 다음 드라이버를 가져와서 원장에 연결하는 데 사용할 수 있습니다. 다음 Python 코드 예제에서는 지정된 원장 이름에 대한 세션을 생성하는 방법을 보여줍니다.

------
#### [ 3.x ]

```
from pyqldb.driver.qldb_driver import QldbDriver
qldb_driver = QldbDriver(ledger_name='testLedger')

for table in qldb_driver.list_tables():
    print(table)
```

------
#### [ 2.x ]

```
from pyqldb.driver.pooled_qldb_driver import PooledQldbDriver

qldb_driver = PooledQldbDriver(ledger_name='testLedger')
qldb_session = qldb_driver.get_session()

for table in qldb_session.list_tables():
    print(table)
```

------

원장에서 기본 데이터 트랜잭션을 실행하는 방법에 대한 단축 코드 예제는 [Cookbook 참조](driver-cookbook-python.md)를 참조하세요.

# Python용 Amazon QLDB 드라이버 - 빠른 시작 자습서
<a name="driver-quickstart-python"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 Python용 Amazon QLDB 드라이버의 최신 버전을 사용하여 간단한 애플리케이션을 설정하는 방법을 알아봅니다. 이 안내서에는 드라이버 설치 단계 및 기본적인 CRUD(*생성, 읽기, 업데이트 및 삭제*) 작업에 대한 단축 코드 예제가 포함되어 있습니다. 전체 샘플 애플리케이션에서 이러한 작업을 보여 주는 자세한 예를 보려면 [Python 자습서](getting-started.python.tutorial.md) 섹션을 참조하세요.

**Topics**
+ [

## 사전 조건
](#driver-quickstart-python.prereqs)
+ [

## 1단계: 프로젝트 설정
](#driver-quickstart-python.step-1)
+ [

## 2단계: 드라이버 초기화
](#driver-quickstart-python.step-2)
+ [

## 3단계: 테이블 및 인덱스 생성
](#driver-quickstart-python.step-3)
+ [

## 4단계: 문서 삽입
](#driver-quickstart-python.step-4)
+ [

## 5단계: 문서 쿼리
](#driver-quickstart-python.step-5)
+ [

## 6단계: 문서 업데이트
](#driver-quickstart-python.step-6)
+ [

## 전체 애플리케이션 실행
](#driver-quickstart-python.complete)

## 사전 조건
<a name="driver-quickstart-python.prereqs"></a>

시작하기 전에 다음을 수행해야 합니다.

1. Python 드라이버를 위한 [사전 조건](getting-started.python.md#getting-started.python.prereqs)을 아직 완료하지 않은 경우, 완료하세요. 여기에는 가입 AWS, 개발을 위한 프로그래밍 방식 액세스 권한 부여, Python 버전 3.6 이상 설치가 포함됩니다.

1. `quick-start`라는 명칭의 원장을 생성합니다.

   원장 생성 방법을 알아보려면 *콘솔 시작하기*의 [Amazon QLDB 원장의 기본 작업](ledger-management.basics.md) 또는 [1단계: 새 원장 생성](getting-started-step-1.md) 섹션을 참조하세요.

## 1단계: 프로젝트 설정
<a name="driver-quickstart-python.step-1"></a>

먼저 Python 프로젝트를 설정합니다.

**참고**  
이러한 설정 단계를 자동화하는 기능이 있는 IDE를 사용하는 경우 [2단계: 드라이버 초기화](#driver-quickstart-python.step-2)로 넘어가도 됩니다.

1. 애플리케이션을 위한 폴더를 생성합니다.

   ```
   $ mkdir myproject
   $ cd myproject
   ```

1. PyPI에서 Python용 QLDB 드라이버를 설치하려면 다음 `pip` 명령을 입력합니다.

   ```
   $ pip install pyqldb
   ```

   드라이버를 설치하면 [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-net) 및 [Amazon Ion](ion.md) 패키지를 포함한 해당 종속 항목도 설치됩니다.

1. `app.py`라는 명칭의 새로운 파일을 만듭니다.

   그런 다음, 다음 단계의 코드 예를 점진적으로 추가하여 몇 가지 기본 CRUD 작업을 시도해 보세요. 또는 단계별 자습서를 건너뛰고 [전체 애플리케이션](#driver-quickstart-python.complete)을 실행할 수도 있습니다.

## 2단계: 드라이버 초기화
<a name="driver-quickstart-python.step-2"></a>

`quick-start`라는 명칭의 원장에 연결되는 드라이버의 인스턴스를 초기화합니다. 다음 코드를 `app.py` 파일에 추가합니다.

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

# Configure retry limit to 3
retry_config = RetryConfig(retry_limit=3)

# Initialize the driver
print("Initializing the driver")
qldb_driver = QldbDriver("quick-start", retry_config=retry_config)
```

## 3단계: 테이블 및 인덱스 생성
<a name="driver-quickstart-python.step-3"></a>

다음 코드 예에서는 `CREATE TABLE` 및 `CREATE INDEX` 문을 실행하는 방법을 보여줍니다.

`People`라는 명칭의 표와 해당 표의 `lastName` 필드 인덱스를 만드는 다음 코드를 추가합니다. [인덱스](ql-reference.create-index.md)는 쿼리 성능을 최적화하고 [OCC(낙관적 동시성 제어)](concurrency.md) 충돌 예외를 제한하는 데 필요합니다.

```
def create_table(transaction_executor):
    print("Creating a table")
    transaction_executor.execute_statement("Create TABLE People")

def create_index(transaction_executor):
    print("Creating an index")
    transaction_executor.execute_statement("CREATE INDEX ON People(lastName)")

# Create a table
qldb_driver.execute_lambda(lambda executor: create_table(executor))

# Create an index on the table
qldb_driver.execute_lambda(lambda executor: create_index(executor))
```

## 4단계: 문서 삽입
<a name="driver-quickstart-python.step-4"></a>

다음 코드 예에서는 `INSERT` 문을 실행하는 방법을 보여줍니다. QLDB는 [PartiQL](ql-reference.md) 쿼리 언어(SQL 호환) 및 [Amazon Ion](ion.md) 데이터 형식(JSON의 상위 집합)을 지원합니다.

`People` 테이블에 문서를 삽입하는 다음 코드를 추가합니다.

```
def insert_documents(transaction_executor, arg_1):
    print("Inserting a document")
    transaction_executor.execute_statement("INSERT INTO People ?", arg_1)

# Insert a document
doc_1 = { 'firstName': "John",
          'lastName': "Doe",
          'age': 32,
        }

qldb_driver.execute_lambda(lambda x: insert_documents(x, doc_1))
```

이 예에서는 물음표(`?`)를 변수 자리 표시자로 사용하여 문서 정보를 해당 문에 전달합니다. 이 `execute_statement` 메서드는 Amazon Ion 타입과 Python 네이티브 타입 모두의 값을 지원합니다.

**작은 정보**  
단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.  

```
# people is a list
transaction_executor.execute_statement("INSERT INTO Person ?", people)
```
목록을 전달할 때 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

## 5단계: 문서 쿼리
<a name="driver-quickstart-python.step-5"></a>

다음 코드 예에서는 `SELECT` 문을 실행하는 방법을 보여줍니다.

`People` 테이블에서 문서를 쿼리하는 다음 코드를 추가합니다.

```
def read_documents(transaction_executor):
    print("Querying the table")
    cursor = transaction_executor.execute_statement("SELECT * FROM People WHERE lastName = ?", 'Doe')

    for doc in cursor:
        print(doc["firstName"])
        print(doc["lastName"])
        print(doc["age"])

# Query the table
qldb_driver.execute_lambda(lambda executor: read_documents(executor))
```

## 6단계: 문서 업데이트
<a name="driver-quickstart-python.step-6"></a>

다음 코드 예에서는 `UPDATE` 문을 실행하는 방법을 보여줍니다.

1. `age`를 `42`로 업데이트하여 `People` 표의 문서를 업데이트하는 다음 코드를 추가합니다.

   ```
   def update_documents(transaction_executor, age, lastName):
       print("Updating the document")
       transaction_executor.execute_statement("UPDATE People SET age = ? WHERE lastName = ?", age, lastName)
   
   # Update the document
   age = 42
   lastName = 'Doe'
   
   qldb_driver.execute_lambda(lambda x: update_documents(x, age, lastName))
   ```

1. 표를 다시 쿼리하여 업데이트된 값을 확인합니다.

   ```
   # Query the updated document
   qldb_driver.execute_lambda(lambda executor: read_documents(executor))
   ```

1. 애플리케이션을 실행하려면 프로젝트 디렉터리에서 다음 명령을 입력합니다.

   ```
   $ python app.py
   ```

## 전체 애플리케이션 실행
<a name="driver-quickstart-python.complete"></a>

다음 코드 예는 `app.py` 애플리케이션의 전체 버전입니다. 이전 단계를 개별적으로 수행하는 대신 이 코드 예를 처음부터 끝까지 복사하여 실행할 수도 있습니다. 이 애플리케이션은 `quick-start`이라는 명칭의 원장에 대한 몇 가지 기본 CRUD 작업을 보여줍니다.

**참고**  
이 코드를 실행하기 전에 `quick-start` 원장에 `People`이라는 명칭의 활성 테이블이 아직 없는지 확인하세요.

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

def create_table(transaction_executor):
    print("Creating a table")
    transaction_executor.execute_statement("CREATE TABLE People")

def create_index(transaction_executor):
    print("Creating an index")
    transaction_executor.execute_statement("CREATE INDEX ON People(lastName)")

def insert_documents(transaction_executor, arg_1):
    print("Inserting a document")
    transaction_executor.execute_statement("INSERT INTO People ?", arg_1)

def read_documents(transaction_executor):
    print("Querying the table")
    cursor = transaction_executor.execute_statement("SELECT * FROM People WHERE lastName = ?", 'Doe')
                                                                                                                                          
    for doc in cursor:
        print(doc["firstName"])
        print(doc["lastName"])
        print(doc["age"])

def update_documents(transaction_executor, age, lastName):
    print("Updating the document")
    transaction_executor.execute_statement("UPDATE People SET age = ? WHERE lastName = ?", age, lastName)

# Configure retry limit to 3
retry_config = RetryConfig(retry_limit=3)

# Initialize the driver
print("Initializing the driver")
qldb_driver = QldbDriver("quick-start", retry_config=retry_config)

# Create a table
qldb_driver.execute_lambda(lambda executor: create_table(executor))

# Create an index on the table
qldb_driver.execute_lambda(lambda executor: create_index(executor))

# Insert a document
doc_1 = { 'firstName': "John",
          'lastName': "Doe",
          'age': 32,
        }

qldb_driver.execute_lambda(lambda x: insert_documents(x, doc_1))

# Query the table
qldb_driver.execute_lambda(lambda executor: read_documents(executor))

# Update the document
age = 42
lastName = 'Doe'

qldb_driver.execute_lambda(lambda x: update_documents(x, age, lastName))

# Query the table for the updated document
qldb_driver.execute_lambda(lambda executor: read_documents(executor))
```

전체 애플리케이션을 실행하려면 프로젝트 디렉터리에서 다음 명령을 입력합니다.

```
$ python app.py
```

# Python용 Amazon QLDB 드라이버 - Cookbook 참조
<a name="driver-cookbook-python"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 참조 가이드는 Python용 Amazon QLDB 드라이버의 일반적인 사용 사례를 보여줍니다. 이 Python 코드 예제는 드라이버를 사용하여 기본 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행하는 방법을 보여줍니다. 또한 Amazon Ion 데이터를 처리하기 위한 코드 예제도 포함되어 있습니다. 또한 이 가이드에서는 트랜잭션에 멱등성을 부여하고 고유성 제약하는 모범 사례를 중점적으로 설명합니다.

**참고**  
해당하는 경우, 일부 사용 사례에서는 Python용 QLDB 드라이버의 지원되는 각 주요 버전마다 다른 코드 예제가 있습니다.

**Contents**
+ [

## 드라이버 가져오기
](#cookbook-python.importing)
+ [

## 드라이버 인스턴스화
](#cookbook-python.instantiating)
+ [

## CRUD 작업
](#cookbook-python.crud)
  + [

### 테이블 생성
](#cookbook-python.crud.creating-tables)
  + [

### 인덱스 생성
](#cookbook-python.crud.creating-indexes)
  + [

### 문서 읽기
](#cookbook-python.crud.reading)
    + [

#### 쿼리 파라미터 사용
](#cookbook-python.reading-using-params)
  + [

### 문서 삽입하기
](#cookbook-python.crud.inserting)
    + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-python.crud.inserting.multiple)
  + [

### 문서 업데이트
](#cookbook-python.crud.updating)
  + [

### 문서 삭제
](#cookbook-python.crud.deleting)
  + [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-python.crud.multi-statement)
  + [

### 재시도 로직
](#cookbook-python.crud.retry-logic)
  + [

### 고유성 제약 조건 구현
](#cookbook-python.crud.uniqueness-constraints)
+ [

## Amazon Ion 작업
](#cookbook-python.ion)
  + [

### Ion 모듈 가져오기
](#cookbook-python.ion.import)
  + [

### Ion 유형 생성
](#cookbook-python.ion.creating-types)
  + [

### Ion 이진 덤프 가져오기
](#cookbook-python.ion.getting-binary)
  + [

### Ion 텍스트 덤프 가져오기
](#cookbook-python.ion.getting-text)

## 드라이버 가져오기
<a name="cookbook-python.importing"></a>

다음 코드 예제에서는 드라이브를 가져옵니다.

------
#### [ 3.x ]

```
from pyqldb.driver.qldb_driver import QldbDriver
import amazon.ion.simpleion as simpleion
```

------
#### [ 2.x ]

```
from pyqldb.driver.pooled_qldb_driver import PooledQldbDriver
import amazon.ion.simpleion as simpleion
```

------

**참고**  
이 예제에서는 Amazon Ion 패키지(`amazon.ion.simpleion`)도 가져옵니다. 이 참조에서 일부 데이터 작업을 실행할 때 Ion 데이터를 처리하려면 이 패키지가 필요합니다. 자세한 내용은 [Amazon Ion 작업](#cookbook-python.ion) 섹션을 참조하세요.

## 드라이버 인스턴스화
<a name="cookbook-python.instantiating"></a>

다음 코드 예제는 기본 설정을 사용하여 지정된 원장 이름에 연결하는 드라이버 인스턴스를 만듭니다.

------
#### [ 3.x ]

```
qldb_driver = QldbDriver(ledger_name='vehicle-registration')
```

------
#### [ 2.x ]

```
qldb_driver = PooledQldbDriver(ledger_name='vehicle-registration')
```

------

## CRUD 작업
<a name="cookbook-python.crud"></a>

QLDB는 트랜잭션의 일부로 CRUD(*생성, 읽기, 업데이트, 삭제*) 작업을 실행합니다.

**주의**  
가장 좋은 방법은 쓰기 트랜잭션이 완전한 멱등성을 부여하는 것입니다.

**트랜잭션에 멱등성 부여하기**

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 좋습니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 *멱등성*을 가집니다.

이름이 `Person`인 테이블에 문서를 삽입하는 트랜잭션을 예로 들어 보겠습니다. 트랜잭션은 먼저 문서가 테이블에 이미 존재하는지 여부를 확인해야 합니다. 이렇게 확인하지 않으면 테이블에 문서가 중복될 수 있습니다.

QLDB가 서버 측에서 트랜잭션을 성공적으로 커밋했지만 응답을 기다리는 동안 클라이언트 제한 시간이 초과되었다고 가정해 보겠습니다. 트랜잭션이 멱등성을 가지지 않는 경우 재시도 시 동일한 문서가 두 번 이상 삽입될 수 있습니다.

**인덱스를 사용하여 전체 테이블 스캔 방지**

인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 이 인덱싱된 조회가 없으면 QLDB는 테이블 스캔을 수행해야 하며, 이로 인해 트랜잭션 제한 시간이 초과되거나 *OCC(낙관적 동시성 제어)* 충돌이 발생할 수 있습니다.

OCC에 대한 자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md) 단원을 참조하세요.

**암시적으로 생성된 트랜잭션**

[pyqldb.driver.qldb\$1driver.execute\$1lambda](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/driver/qldb_driver.html#pyqldb.driver.qldb_driver.QldbDriver.execute_lambda) 메서드는 [pyqldb.execution.executor.Executor](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/execution/executor.html#pyqldb.execution.executor.Executor)의 인스턴스를 수신하는 Lambda 함수를 받아들이며, 이 함수를 사용하여 명령문을 실행할 수 있습니다. `Executor`의 인스턴스는 암시적으로 생성된 트랜잭션을 래핑합니다.

트랜잭션 실행자의 [execute\$1statement](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/execution/executor.html#pyqldb.execution.executor.Executor.execute_statement) 메서드를 사용하여 Lambda 함수 내에서 명령문을 실행할 수 있습니다. 드라이버는 Lambda 함수가 반환될 때 트랜잭션을 암시적으로 커밋합니다.

**참고**  
이 `execute_statement` 메서드는 Amazon Ion 유형과 Python 네이티브 유형을 모두 지원합니다. Python 네이티브 유형을 인수로 `execute_statement`에 전달하면 드라이버가 `amazon.ion.simpleion` 모듈을 사용하여 Ion 유형으로 변환합니다(지정된 Python 데이터 유형에 대한 변환이 지원되는 경우). 지원되는 데이터 유형 및 변환 규칙은 [simpleion 소스 코드](https://ion-python.readthedocs.io/en/latest/_modules/amazon/ion/simpleion.html)를 참조하세요.

다음 섹션에서는 기본 CRUD 작업을 실행하고, 사용자 지정 재시도 로직을 지정하고, 고유성 제약 조건을 구현하는 방법을 보여줍니다.

**Contents**
+ [

### 테이블 생성
](#cookbook-python.crud.creating-tables)
+ [

### 인덱스 생성
](#cookbook-python.crud.creating-indexes)
+ [

### 문서 읽기
](#cookbook-python.crud.reading)
  + [

#### 쿼리 파라미터 사용
](#cookbook-python.reading-using-params)
+ [

### 문서 삽입하기
](#cookbook-python.crud.inserting)
  + [

#### 하나의 명령문에 여러 문서 삽입
](#cookbook-python.crud.inserting.multiple)
+ [

### 문서 업데이트
](#cookbook-python.crud.updating)
+ [

### 문서 삭제
](#cookbook-python.crud.deleting)
+ [

### 하나의 트랜잭션에서 여러 명령문 실행
](#cookbook-python.crud.multi-statement)
+ [

### 재시도 로직
](#cookbook-python.crud.retry-logic)
+ [

### 고유성 제약 조건 구현
](#cookbook-python.crud.uniqueness-constraints)

### 테이블 생성
<a name="cookbook-python.crud.creating-tables"></a>

```
def create_table(transaction_executor):
    transaction_executor.execute_statement("CREATE TABLE Person")

qldb_driver.execute_lambda(lambda executor: create_table(executor))
```

### 인덱스 생성
<a name="cookbook-python.crud.creating-indexes"></a>

```
def create_index(transaction_executor):
    transaction_executor.execute_statement("CREATE INDEX ON Person(GovId)")

qldb_driver.execute_lambda(lambda executor: create_index(executor))
```

### 문서 읽기
<a name="cookbook-python.crud.reading"></a>

```
# Assumes that Person table has documents as follows:
# { "GovId": "TOYENC486FH", "FirstName": "Brent" }

def read_documents(transaction_executor):
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = 'TOYENC486FH'")

    for doc in cursor:
        print(doc["GovId"]) # prints TOYENC486FH
        print(doc["FirstName"]) # prints Brent

qldb_driver.execute_lambda(lambda executor: read_documents(executor))
```

#### 쿼리 파라미터 사용
<a name="cookbook-python.reading-using-params"></a>

다음 코드 예제는 네이티브 유형 쿼리 파라미터를 사용합니다.

```
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", 'TOYENC486FH')
```

다음 코드 예제는 Ion 유형 쿼리 파라미터를 사용합니다.

```
name = ion.loads('Brent')
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE FirstName = ?", name)
```

다음 코드 예제는 여러 쿼리 파라미터를 사용합니다.

```
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ? AND FirstName = ?", 'TOYENC486FH', "Brent")
```

다음 코드 예제는 쿼리 파라미터 목록을 사용합니다.

```
gov_ids = ['TOYENC486FH','ROEE1','YH844']
cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId IN (?,?,?)", *gov_ids)
```

**참고**  
인덱싱된 조회 없이 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. 이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 [인덱스](ql-reference.create-index.md)를 사용하는 것이 좋습니다. `GovId`에 인덱스를 사용하지 않으면 쿼리에 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삽입하기
<a name="cookbook-python.crud.inserting"></a>

다음 코드 예제는 네이티브 데이터 유형을 삽입합니다.

```
def insert_documents(transaction_executor, arg_1):
    # Check if doc with GovId:TOYENC486FH exists
    # This is critical to make this transaction idempotent
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", 'TOYENC486FH')
    # Check if there is any record in the cursor
    first_record = next(cursor, None)

    if first_record:
        # Record already exists, no need to insert
        pass
    else:
        transaction_executor.execute_statement("INSERT INTO Person ?", arg_1)

doc_1 = { 'FirstName': "Brent",
          'GovId': 'TOYENC486FH',
        }

qldb_driver.execute_lambda(lambda executor: insert_documents(executor, doc_1))
```

다음 코드 예제는 Ion 데이터 유형을 삽입합니다.

```
def insert_documents(transaction_executor, arg_1):
    # Check if doc with GovId:TOYENC486FH exists
    # This is critical to make this transaction idempotent
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", 'TOYENC486FH')
    # Check if there is any record in the cursor
    first_record = next(cursor, None)

    if first_record:
        # Record already exists, no need to insert
        pass
    else:
        transaction_executor.execute_statement("INSERT INTO Person ?", arg_1)

doc_1 = { 'FirstName': 'Brent',
          'GovId': 'TOYENC486FH',
        }

# create a sample Ion doc
ion_doc_1 = simpleion.loads(simpleion.dumps(doc_1)))

qldb_driver.execute_lambda(lambda executor: insert_documents(executor, ion_doc_1))
```

이 트랜잭션은 문서를 `Person` 테이블에 삽입합니다. 삽입하기 전에 먼저 문서가 테이블에 이미 있는지 확인합니다. **이 검사를 통해 트랜잭션은 본질적으로 멱등성을 가지게 됩니다.** 이 트랜잭션을 여러 번 실행하더라도 의도하지 않은 부작용이 발생하지는 않습니다.

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

#### 하나의 명령문에 여러 문서 삽입
<a name="cookbook-python.crud.inserting.multiple"></a>

단일 [INSERT](ql-reference.insert.md) 문을 사용하여 여러 문서를 삽입하려면 다음과 같이 [목록](driver-working-with-ion.md#driver-ion-list) 타입의 파라미터를 해당 문에 전달할 수 있습니다.

```
# people is a list
transaction_executor.execute_statement("INSERT INTO Person ?", people)
```

목록을 전달할 때 변수 자리 표시자(`?`)를 이중 꺾쇠 괄호( `<<...>>` )로 묶지 마세요. 수동 PartiQL 문에서 이중 꺾쇠 괄호는 *백*으로 알려진 정렬되지 않은 모음을 의미합니다.

### 문서 업데이트
<a name="cookbook-python.crud.updating"></a>

다음 코드 예제는 네이티브 데이터 유형을 사용합니다.

```
def update_documents(transaction_executor, gov_id, name):
    transaction_executor.execute_statement("UPDATE Person SET FirstName = ?  WHERE GovId = ?", name, gov_id)

gov_id = 'TOYENC486FH'
name = 'John'

qldb_driver.execute_lambda(lambda executor: update_documents(executor, gov_id, name))
```

다음 코드 예제는 Ion 데이터 유형을 사용합니다.

```
def update_documents(transaction_executor, gov_id, name):
    transaction_executor.execute_statement("UPDATE Person SET FirstName = ? WHERE GovId = ?", name, gov_id)

# Ion datatypes
gov_id = simpleion.loads('TOYENC486FH')
name = simpleion.loads('John')

qldb_driver.execute_lambda(lambda executor: update_documents(executor, gov_id, name))
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 문서 삭제
<a name="cookbook-python.crud.deleting"></a>

다음 코드 예제는 네이티브 데이터 유형을 사용합니다.

```
def delete_documents(transaction_executor, gov_id):
    cursor = transaction_executor.execute_statement("DELETE FROM Person WHERE GovId = ?", gov_id)

gov_id = 'TOYENC486FH'

qldb_driver.execute_lambda(lambda executor: delete_documents(executor, gov_id))
```

다음 코드 예제는 Ion 데이터 유형을 사용합니다.

```
def delete_documents(transaction_executor, gov_id):
    cursor = transaction_executor.execute_statement("DELETE FROM Person WHERE GovId = ?", gov_id)

# Ion datatypes
gov_id = simpleion.loads('TOYENC486FH')

qldb_driver.execute_lambda(lambda executor: delete_documents(executor, gov_id))
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

### 하나의 트랜잭션에서 여러 명령문 실행
<a name="cookbook-python.crud.multi-statement"></a>

```
# This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
# set your UPDATE to filter on vin and insured, and check if you updated something or not.

def do_insure_car(transaction_executor, vin):
    cursor = transaction_executor.execute_statement(
        "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
    first_record = next(cursor, None)
    if first_record:
        transaction_executor.execute_statement(
            "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
        return True
    else:
        return False

def insure_car(qldb_driver, vin_to_insure):
    return qldb_driver.execute_lambda(
        lambda executor: do_insure_car(executor, vin_to_insure))
```

### 재시도 로직
<a name="cookbook-python.crud.retry-logic"></a>

드라이버의 `execute_lambda` 메서드에는 재시도 가능한 예외(예: 시간 초과 또는 OCC 충돌)가 발생할 경우 트랜잭션을 재시도하는 재시도 메커니즘이 내장되어 있습니다.

------
#### [ 3.x ]

최대 재시도 횟수와 백오프 전략을 구성할 수 있습니다.

기본 재시도 제한은 `4`이며, 기본 백오프 전략은 `10`밀리초 단위의 [지수 백오프 및 Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)입니다. [pyqldb.config.retry\$1config.RetryConfig](https://amazon-qldb-driver-python.readthedocs.io/en/stable/reference/config/retry_config.html#pyqldb.config.retry_config.RetryConfig) 인스턴스를 사용하여 드라이버 인스턴스별 및 트랜잭션별 재시도 구성을 설정할 수 있습니다.

다음 코드 예제는 드라이버 인스턴스에 대한 사용자 지정 재시도 제한 및 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다.

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

# Configuring retry limit to 2
retry_config = RetryConfig(retry_limit=2)
qldb_driver = QldbDriver("test-ledger", retry_config=retry_config)

# Configuring a custom backoff which increases delay by 1s for each attempt.
def custom_backoff(retry_attempt, error, transaction_id):
    return 1000 * retry_attempt

retry_config_custom_backoff = RetryConfig(retry_limit=2, custom_backoff=custom_backoff)
qldb_driver = QldbDriver("test-ledger", retry_config=retry_config_custom_backoff)
```

다음 코드 예제는 특정 Lambda 실행에 대한 사용자 지정 재시도와 사용자 지정 백오프 전략을 사용하여 재시도 로직을 지정합니다. `execute_lambda`에 대한 이 구성은 드라이버 인스턴스에 설정된 재시도 로직을 재정의합니다.

```
from pyqldb.config.retry_config import RetryConfig
from pyqldb.driver.qldb_driver import QldbDriver

# Configuring retry limit to 2
retry_config_1 = RetryConfig(retry_limit=4)
qldb_driver = QldbDriver("test-ledger", retry_config=retry_config_1)

# Configuring a custom backoff which increases delay by 1s for each attempt.
def custom_backoff(retry_attempt, error, transaction_id):
    return 1000 * retry_attempt

retry_config_2 = RetryConfig(retry_limit=2, custom_backoff=custom_backoff)

# The config `retry_config_1` will be overriden by `retry_config_2`
qldb_driver.execute_lambda(lambda txn: txn.execute_statement("CREATE TABLE Person"), retry_config_2)
```

------
#### [ 2.x ]

최대 재시도 횟수는 구성 가능합니다. `PooledQldbDriver`를 초기화할 때 `retry_limit` 속성을 설정하여 재시도 제한을 구성할 수 있습니다.

기본 재시도 한도는 `4`입니다.

------

### 고유성 제약 조건 구현
<a name="cookbook-python.crud.uniqueness-constraints"></a>

QLDB는 고유 인덱스를 지원하지 않지만 애플리케이션에서 이 동작을 구현할 수 있습니다.

`Person` 테이블의 `GovId` 필드에 고유성 제약 조건을 구현하려고 한다고 가정해 보겠습니다. 이렇게 하면 다음 작업을 수행하는 트랜잭션을 작성합니다.

1. 테이블에 지정된 `GovId`가 있는 기존 문서가 없는지 확인합니다.

1. 어설션이 통과하면 문서를 삽입합니다.

경쟁 트랜잭션이 어설션을 동시에 통과하면 트랜잭션 중 하나만 성공적으로 커밋됩니다. 다른 트랜잭션은 OCC 충돌 예외가 발생하여 실패합니다.

다음 코드 예제는 이 고유성 제약 조건 구현 방법을 보여줍니다.

```
def insert_documents(transaction_executor, gov_id, document):
    # Check if doc with GovId = gov_id exists
    cursor = transaction_executor.execute_statement("SELECT * FROM Person WHERE GovId = ?", gov_id)
    # Check if there is any record in the cursor
    first_record = next(cursor, None)

    if first_record:
        # Record already exists, no need to insert
        pass
    else:
        transaction_executor.execute_statement("INSERT INTO Person ?", document)

qldb_driver.execute_lambda(lambda executor: insert_documents(executor, gov_id, document))
```

**참고**  
이 예제에서는 성능을 최적화하기 위해 `GovId` 필드에 인덱스를 사용하는 것이 좋습니다. `GovId`에 인덱스를 설정하지 않으면 명령문의 지연 시간이 길어지고 OCC 충돌 예외 또는 트랜잭션 시간 초과가 발생할 수도 있습니다.

## Amazon Ion 작업
<a name="cookbook-python.ion"></a>

다음 섹션에서는 Amazon Ion 모듈을 사용하여 Ion 데이터를 처리하는 방법을 보여줍니다.

**Contents**
+ [

### Ion 모듈 가져오기
](#cookbook-python.ion.import)
+ [

### Ion 유형 생성
](#cookbook-python.ion.creating-types)
+ [

### Ion 이진 덤프 가져오기
](#cookbook-python.ion.getting-binary)
+ [

### Ion 텍스트 덤프 가져오기
](#cookbook-python.ion.getting-text)

### Ion 모듈 가져오기
<a name="cookbook-python.ion.import"></a>

```
import amazon.ion.simpleion as simpleion
```

### Ion 유형 생성
<a name="cookbook-python.ion.creating-types"></a>

다음 코드 예제는 Ion 텍스트에서 Ion 객체를 만듭니다.

```
ion_text = '{GovId: "TOYENC486FH", FirstName: "Brent"}'
ion_obj = simpleion.loads(ion_text)

print(ion_obj['GovId']) # prints TOYENC486FH
print(ion_obj['Name']) # prints Brent
```

다음 코드 예제는 Python `dict`에서 Ion 객체를 만듭니다.

```
a_dict = { 'GovId': 'TOYENC486FH',
           'FirstName': "Brent"
         }
ion_obj = simpleion.loads(simpleion.dumps(a_dict))

print(ion_obj['GovId']) # prints TOYENC486FH
print(ion_obj['FirstName']) # prints Brent
```

### Ion 이진 덤프 가져오기
<a name="cookbook-python.ion.getting-binary"></a>

```
# ion_obj is an Ion struct
print(simpleion.dumps(ion_obj)) # b'\xe0\x01\x00\xea\xee\x97\x81\x83\xde\x93\x87\xbe\x90\x85GovId\x89FirstName\xde\x94\x8a\x8bTOYENC486FH\x8b\x85Brent'
```

### Ion 텍스트 덤프 가져오기
<a name="cookbook-python.ion.getting-text"></a>

```
# ion_obj is an Ion struct
print(simpleion.dumps(ion_obj, binary=False)) # prints $ion_1_0 {GovId:'TOYENC486FH',FirstName:"Brent"}
```

Ion 작업에 대한 자세한 정보는 GitHub의 [Amazon Ion 설명서](http://amzn.github.io/ion-docs/)를 참조하세요. QLDB에서 Ion을 사용하는 방법에 대한 추가 코드 예제는 [Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업](driver-working-with-ion.md) 섹션을 참조하세요.

# Amazon QLDB에서 드라이버를 사용한 세션 관리에 대한 이해
<a name="driver-session-management"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

관계형 데이터베이스 관리 시스템(RDBMS)을 사용해 본 경험이 있다면 동시 연결에 익숙할 것입니다. 트랜잭션이 HTTP 요청 및 응답 메시지로 실행되기 때문에 QLDB는 기존 RDBMS 연결과 같은 개념을 가지고 있지 않습니다.

QLDB에서 유사한 개념은 *활성 세션*입니다. 세션은 개념적으로 사용자 로그인과 비슷합니다. 즉, 원장에 대한 데이터 트랜잭션 요청에 대한 정보를 관리합니다. 활성 세션은 트랜잭션을 활발하게 실행하는 세션입니다. 또한 서비스가 즉시 다른 트랜잭션을 시작할 것으로 예상하는, 트랜잭션을 최근에 완료한 세션일 수도 있습니다. QLDB는 세션당 하나의 활성 실행 트랜잭션을 지원합니다.

원장당 동시 활성 세션의 한도는 [Amazon QLDB 할당량 및 제한](limits.md#limits.fixed)에 정의되어 있습니다. 이 한도에 도달한 후 트랜잭션을 시작하려는 모든 세션에서 오류(`LimitExceededException`)가 발생합니다.

QLDB 드라이버를 사용하여 애플리케이션에서 세션 풀을 구성하는 모범 사례는 *Amazon QLDB 드라이버 권장 사항*의 [QLDBDriver 객체 구성](driver.best-practices.md#driver.best-practices.configuring) 섹션을 참조하세요.

**Contents**
+ [

## 세션 라이프사이클
](#driver-session-mgmt.lifecycle)
+ [

## 세션 만료
](#driver-session-mgmt.expiration)
+ [

## QLDB 드라이버에서의 세션 처리
](#driver-session-mgmt.handling)
  + [

### 세션 풀링 개요
](#driver-session-mgmt.pooling-overview)
  + [

### 세션 풀링 및 트랜잭션 로직
](#driver-session-mgmt.pooling-logic)
  + [

### 풀로 세션 반환하기
](#driver-session-mgmt.pooling-return)

## 세션 라이프사이클
<a name="driver-session-mgmt.lifecycle"></a>

다음 [QLDB 세션 API](https://docs.aws.amazon.com/qldb/latest/developerguide/API_Types_Amazon_QLDB_Session.html) 작업 시퀀스는 QLDB 세션의 일반적인 라이프사이클를 나타냅니다.

1. `StartSession`

1. `StartTransaction`

1. `ExecuteStatement`

1. `CommitTransaction`

1. 더 많은 트랜잭션을 시작하려면 2-4단계를 반복합니다(한 번에 한 트랜잭션씩).

1. `EndSession`

## 세션 만료
<a name="driver-session-mgmt.expiration"></a>

QLDB는 트랜잭션이 활발하게 실행되고 있는지 여부와 상관없이 총 **13\$117분**이 지나면 세션이 만료되고 삭제됩니다. 세션은 하드웨어 장애, 네트워크 장애 또는 애플리케이션 재시작과 같은 여러 가지 이유로 손실되거나 손상될 수 있습니다. QLDB는 클라이언트 애플리케이션이 세션 장애에 대해 복원력을 유지할 수 있도록 세션에 최대 수명을 적용합니다.

## QLDB 드라이버에서의 세션 처리
<a name="driver-session-mgmt.handling"></a>

 AWS SDK를 사용하여 *QLDB 세션* API와 직접 상호 작용할 수 있지만 이로 인해 복잡성이 커밋 다이제스트를 계산해야 합니다. QLDB는 이 체결 다이제스트를 사용하여 트랜잭션 무결성을 보장합니다. 이 API와 직접 상호 작용하는 대신 QLDB 드라이버를 사용하는 것이 좋습니다.

드라이버는 트랜잭션 데이터 API 위에 높은 수준의 추상 계층을 제공합니다. [SendCommand](https://docs.aws.amazon.com/qldb/latest/developerguide/API_QLDB-Session_SendCommand.html) API 호출을 관리하여 원장 데이터에서 [PartiQL](ql-reference.md) 문을 실행하는 프로세스를 간소화합니다. 이러한 API 호출에는 세션, 트랜잭션 관리, 오류 발생 시 재시도 정책 등 드라이버가 대신 처리하는 여러 파라미터가 필요합니다.

**Topics**
+ [

### 세션 풀링 개요
](#driver-session-mgmt.pooling-overview)
+ [

### 세션 풀링 및 트랜잭션 로직
](#driver-session-mgmt.pooling-logic)
+ [

### 풀로 세션 반환하기
](#driver-session-mgmt.pooling-return)

### 세션 풀링 개요
<a name="driver-session-mgmt.pooling-overview"></a>

이전 버전의 QLDB 드라이버(예: [Java v1.1.0](https://github.com/awslabs/amazon-qldb-driver-java/releases/tag/v1.1.0))에서는 드라이버 객체의 두 가지 구현, 즉 표준, 비풀링 `QldbDriver` 및 `PooledQldbDriver`를 제공했습니다. 명칭에서 알 수 있듯이 `PooledQldbDriver`는 여러 트랜잭션에서 재사용되는 세션 풀을 유지 관리합니다.

사용자 피드백에 따르면 개발자들은 표준 드라이버를 사용하는 대신 풀링 기능과 그 이점을 기본으로 사용하는 것을 선호합니다. 따러서 `PooledQldbDriver`를 제거하고 세션 풀링 기능을 `QldbDriver`로 옮겼습니다. 이 변경 사항은 각 드라이버의 최근 릴리스(예: [Java v2.0.0](https://github.com/awslabs/amazon-qldb-driver-java/releases/tag/v2.0.0))에 포함되어 있습니다.

드라이버는 세 가지 수준의 추상화를 제공합니다.
+ **드라이버**(풀링된 드라이버 구현) - 최상위 추상화입니다. 드라이버는 세션 풀을 유지 및 관리합니다. 드라이버에게 트랜잭션을 실행하도록 요청하면 드라이버는 풀에서 세션을 선택하고 이를 사용하여 트랜잭션을 실행합니다. 세션 오류(`InvalidSessionException`)로 인해 트랜잭션이 실패하는 경우 드라이버는 다른 세션을 사용하여 트랜잭션을 재시도합니다. 기본적으로 드라이버는 종합 관리형 세션 환경을 제공합니다.
+ **세션** - 드라이버 추상화보다 한 수준 낮습니다. 세션은 풀에 포함되며 드라이버는 세션의 라이프사이클를 관리합니다. 트랜잭션이 실패하면 드라이버는 동일한 세션을 사용하여 지정된 횟수까지 트랜잭션을 재시도합니다. 그러나 세션에서 오류(`InvalidSessionException`)가 발생하면 QLDB는 해당 오류를 내부적으로 삭제합니다. 그러면 드라이버는 풀에서 다른 세션을 가져와 트랜잭션을 재시도해야 합니다.
+ **트랜잭션** - 가장 낮은 추상화 수준입니다. 트랜잭션은 세션에 포함되며 세션은 트랜잭션의 라이프사이클를 관리합니다. 세션은 오류 발생 시 트랜잭션을 재시도합니다. 또한 세션은 체결되거나 취소되지 않은 열린 트랜잭션이 유출되지 않도록 합니다.

각 드라이버의 최신 버전에서는 드라이버 추상화 수준에서만 작업을 수행할 수 있습니다. 개별 세션과 트랜잭션을 직접 제어할 수 없습니다. (즉, 새 세션이나 트랜잭션을 수동으로 시작하는 API 작업이 없습니다.)

### 세션 풀링 및 트랜잭션 로직
<a name="driver-session-mgmt.pooling-logic"></a>

각 드라이버의 최근 릴리스에서는 더 이상 드라이버 객체의 비풀링 구현을 제공하지 않습니다. 기본적으로 `QldbDriver` 객체는 세션 풀을 관리합니다. 트랜잭션을 실행하기 위해 호출을 하면 드라이버가 다음 단계를 수행합니다.

1. 드라이버는 세션 풀 한도에 도달했는지 확인합니다. 그럴 경우 드라이버는 즉시 `NoSessionAvailable` 예외를 발생시킵니다. 아니면 그 다음 단계로 이동합니다.

1. 드라이버는 풀에 사용 가능한 세션이 있는지 확인합니다.
   + 풀에서 세션을 사용할 수 있는 경우 드라이버는 해당 세션을 사용하여 트랜잭션을 실행합니다.
   + 풀에 사용 가능한 세션이 없는 경우 드라이버는 새 세션을 만들고 이를 사용하여 트랜잭션을 실행합니다.

1. 드라이버가 2단계에서 세션을 가져오면 드라이버가 세션 인스턴스에서 `execute` 작업을 호출합니다.

1. 세션의 `execute` 작업에서 드라이버는 `startTransaction`를 호출하여 트랜잭션을 시작하려고 합니다.
   + 세션이 유효하지 않으면 `startTransaction` 호출이 실패하고 드라이버는 1단계로 돌아갑니다.
   + `startTransaction` 호출이 성공하면 드라이버는 다음 단계로 진행됩니다.

1. 드라이버가 Lambda 표현식을 실행합니다. 이 Lambda 표현식에는 PartiQL 문을 실행하기 위한 하나 이상의 호출이 포함될 수 있습니다. Lambda 표현식이 오류 없이 실행을 마치면 드라이버는 트랜잭션 체결을 진행합니다.

1. 트랜잭션을 체결하면 다음 두 가지 결과 중 하나가 발생할 수 있습니다.
   + 체결이 성공하고 드라이버가 제어권을 애플리케이션 코드에 반환합니다.
   + OCC(낙관적 동시성 제어) 충돌로 인해 체결이 실패합니다. 이 경우 드라이버는 동일한 세션을 사용하여 4\$16단계를 재시도합니다. 최대 재시도 횟수는 애플리케이션 코드에서 구성할 수 있습니다. 기본 한도는 `4`입니다.

**참고**  
4\$16단계에서 `InvalidSessionException`가 발생하면 드라이버는 세션을 닫힌 것으로 표시하고 1단계로 돌아가 트랜잭션을 재시도합니다.  
4\$16단계에서 다른 예외가 발생하는 경우 드라이버는 예외를 재시도할 수 있는지 확인합니다. 그럴 경우 지정된 재시도 횟수까지 트랜잭션을 재시도합니다. 그렇지 않으면 예외가 애플리케이션 코드에 전파(버블 업되어 발생)됩니다.

### 풀로 세션 반환하기
<a name="driver-session-mgmt.pooling-return"></a>

활성 트랜잭션이 진행되는 동안 언제든지 `InvalidSessionException`가 발생하는 경우 드라이버는 세션을 풀로 반환하지 않습니다. QLDB는 대신 세션을 삭제하고 드라이버는 풀에서 다른 세션을 가져옵니다. 다른 모든 경우에는 드라이버가 세션을 풀에 반환합니다.

# Amazon QLDB 드라이버 권장 사항
<a name="driver.best-practices"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 섹션에서는 지원되는 모든 언어에 대해 Amazon QLDB 드라이버를 구성하고 사용하는 모범 사례를 설명합니다. 제공된 코드 예제는 Java 전용입니다.

이러한 권장 사항은 대부분의 일반적인 사용 사례에 적용되지만 한 가지 방법이 모든 사용 사례에 적합하지는 않습니다. 애플리케이션에 적합하다고 판단되는 대로 다음 권장 사항을 사용하세요.

**Topics**
+ [

## QLDBDriver 객체 구성
](#driver.best-practices.configuring)
+ [

## 예외에 대한 재시도
](#driver.best-practices.retrying)
+ [

## 성능 최적화
](#driver.best-practices.performance)
+ [

## 트랜잭션당 여러 명령문 실행
](#driver.best-practices.multiple-statements)

## QLDBDriver 객체 구성
<a name="driver.best-practices.configuring"></a>

이 `QldbDriver` 객체는 트랜잭션 간에 재사용되는 *세션* 풀을 유지 관리하여 원장과의 연결을 관리합니다. [세션](driver-session-management.md)은 원장에 대한 단일 연결을 나타냅니다. QLDB는 세션당 하나의 활성 실행 트랜잭션을 지원합니다.

**중요**  
이전 드라이버 버전의 경우 세션 풀링 기능은 `QldbDriver` 대신 여전히 `PooledQldbDriver` 객체에 있습니다. 다음 버전 중 하나를 사용하는 경우, 이 주제의 나머지에서는 `QldbDriver`에 대한 언급을 `PooledQldbDriver`로 바꿉니다.  


****  

| 드라이버 | 버전 | 
| --- | --- | 
| Java | 1.1.0 또는 이전 | 
| .NET | 0.1.0-beta | 
| Node.js | 1.0.0-rc.1 또는 이전 | 
| Python | 2.0.2 또는 이전 | 
이 `PooledQldbDriver` 객체는 최신 버전의 드라이버에서 더 이상 사용되지 않습니다. 최신 버전으로 업그레이드하고 `PooledQldbDriver`의 모든 인스턴스를 `QldbDriver`로 변환하는 것이 좋습니다.

**QldbDriver를 글로벌 객체로 구성**

드라이버 및 세션 사용을 최적화하려면 애플리케이션 인스턴스에 드라이버의 글로벌 인스턴스가 하나만 있어야 합니다. 예를 들어, Java에서는 [Spring](https://spring.io/), [Google Guice](https://github.com/google/guice) 또는 [Dagger](https://dagger.dev/)와 같은 *종속성 주입* 프레임워크를 사용할 수 있습니다. 다음 코드 예제는 `QldbDriver`를 싱글톤으로 구성하는 방법을 보여줍니다.

```
@Singleton
public QldbDriver qldbDriver (AWSCredentialsProvider credentialsProvider,
                                    @Named(LEDGER_NAME_CONFIG_PARAM) String ledgerName) {
    QldbSessionClientBuilder builder = QldbSessionClient.builder();
    if (null != credentialsProvider) {
        builder.credentialsProvider(credentialsProvider);
    }
    return QldbDriver.builder()
            .ledger(ledgerName)
            .transactionRetryPolicy(RetryPolicy
                .builder()
                .maxRetries(3)
                .build())
            .sessionClientBuilder(builder)
            .build();
}
```

**재시도 시도 구성**

드라이버는 일반적인 일시적 예외(예: `SocketTimeoutException` 또는 `NoHttpResponseException`)가 발생할 경우 자동으로 트랜잭션을 재시도합니다. 최대 재시도 횟수를 설정하려면 `QldbDriver`의 인스턴스를 만들 때 `transactionRetryPolicy` 구성 객체의 `maxRetries` 파라미터를 사용할 수 있습니다. (이전 섹션에 나열된 이전 드라이버 버전의 경우 `PooledQldbDriver`의 `retryLimit` 파라미터를 사용하세요.)

`maxRetries`의 기본값은 `4`입니다.

`InvalidParameterException`와 같은 클라이언트 측 오류는 다시 시도할 수 없습니다. 오류가 발생하면 트랜잭션이 중단되고 세션이 풀로 반환되며 드라이버의 클라이언트에 예외가 발생합니다.

**동시에 접속할 수 있는 최대 세션 및 트랜잭션 수 설정**

`QldbDriver`의 인스턴스가 트랜잭션을 실행하는 데 사용하는 최대 원장 세션 수는 해당 `maxConcurrentTransactions` 파라미터에 의해 정의됩니다. (이전 섹션에 나열된 이전 드라이버 버전의 경우 이는 `PooledQldbDriver`의 `poolLimit` 파라미터로 정의됩니다.)

이 제한은 0보다 크고 특정 AWS SDK에서 정의한 대로 세션 클라이언트가 허용하는 최대 열린 HTTP 연결 수보다 작거나 같아야 합니다. 예를 들어, Java의 경우 최대 연결 수는 [ClientConfiguration](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html#getMaxConnections--) 객체에 설정됩니다.

의 기본값은 AWS SDK의 최대 연결 설정`maxConcurrentTransactions`입니다.

애플리케이션에서 `QldbDriver`를 구성할 때는 다음과 같은 크기 조정 고려 사항을 고려하세요.
+ 풀에는 항상 계획한 동시 실행 트랜잭션 수만큼 많은 세션이 있어야 합니다.
+ 감독자 스레드가 작업자 스레드에 위임하는 다중 스레드 모델에서는 드라이버에 최소한 작업자 스레드 수 만큼의 세션이 있어야 합니다. 그렇지 않으면 부하가 최대일 때 스레드가 사용 가능한 세션을 기다리게 됩니다.
+ 원장당 동시 활성 세션의 서비스 한도는 [Amazon QLDB 할당량 및 제한](limits.md#limits.fixed)에 정의되어 있습니다. 모든 클라이언트의 단일 원장에 사용할 동시 세션 한도를 초과하여 구성하지 않도록 하세요.

## 예외에 대한 재시도
<a name="driver.best-practices.retrying"></a>

QLDB에서 발생한 예외에 대해 재시도할 때는 다음 권장 사항을 고려하세요.

**OccConflictException 재시도**

*OCC(낙관적 동시성 제어)* 충돌 예외는 트랜잭션이 시작된 이후 트랜잭션에서 액세스하는 데이터가 변경된 경우 발생합니다. QLDB는 트랜잭션 커밋을 시도하는 동안 이 예외를 발생시킵니다. 드라이버는 `maxRetries`이 구성된 횟수만큼 트랜잭션을 재시도합니다.

OCC에 대한 자세한 내용과 인덱스를 사용하여 OCC 충돌을 제한하는 모범 사례에 대한 자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md) 섹션을 참조하세요.

**QldbDriver 외부의 다른 예외에 대한 재시도**

런타임 중에 애플리케이션에서 정의한 사용자 지정 예외가 발생할 때 드라이버 외부에서 트랜잭션을 재시도하려면 트랜잭션을 래핑해야 합니다. 예를 들어, Java의 경우 다음 코드는 [Reslience4J](https://resilience4j.readme.io/) 라이브러리를 사용하여 QLDB에서 트랜잭션을 재시도하는 방법을 보여줍니다.

```
private final RetryConfig retryConfig = RetryConfig.custom()
        .maxAttempts(MAX_RETRIES)
        .intervalFunction(IntervalFunction.ofExponentialRandomBackoff())
        // Retry this exception
        .retryExceptions(InvalidSessionException.class, MyRetryableException.class)
        // But fail for any other type of exception extended from RuntimeException
        .ignoreExceptions(RuntimeException.class)
        .build();

// Method callable by a client
public void myTransactionWithRetries(Params params) {
    Retry retry = Retry.of("registerDriver", retryConfig);

    Function<Params, Void> transactionFunction = Retry.decorateFunction(
            retry,
            parameters ->  transactionNoReturn(params));
    transactionFunction.apply(params);
}

private Void transactionNoReturn(Params params) {
    try (driver.execute(txn -> {
            // Transaction code
        });
    }
    return null;
}
```

**참고**  
QLDB 드라이버 외부에서 트랜잭션을 재시도하면 승수 효과를 누릴 수 있습니다. 예를 들어, `QldbDriver`가 세 번 재시도하도록 구성되어 있고, 사용자 지정 재시도 로직도 세 번 재시도하면 동일한 트랜잭션을 최대 9번까지 재시도할 수 있습니다.

**트랜잭션에 멱등성 부여하기**

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 가장 좋은 방법입니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 *멱등성*을 가집니다.

자세한 내용은 [Amazon QLDB 동시성 모델](concurrency.md#concurrency.idempotent) 섹션을 참조하세요.

## 성능 최적화
<a name="driver.best-practices.performance"></a>

드라이버를 사용하여 트랜잭션을 실행할 때 성능을 최적화하려면 다음 사항을 고려하세요.
+ 이 `execute` 작업은 항상 다음 명령을 포함하여 QLDB에 대해 최소 세 번의 `SendCommand` API 호출을 수행합니다.

  1. `StartTransaction`

  1. `ExecuteStatement`

     이 명령은 `execute` 블록에서 실행하는 각 PartiQL 명령문에 대해 호출됩니다.

  1. `CommitTransaction`

  애플리케이션의 전체 워크로드를 계산할 때 수행되는 총 API 호출 수를 고려하세요.
+ 일반적으로 단일 스레드 작성기로 시작하여 단일 트랜잭션 내에서 여러 명령문을 일괄 처리하여 트랜잭션을 최적화하는 것이 좋습니다. [Amazon QLDB 할당량 및 제한](limits.md#limits.fixed)에 정의된 대로 트랜잭션 크기, 문서 크기 및 트랜잭션당 문서 수의 할당량을 최대화합니다.
+ 대규모 트랜잭션 로드에 일괄 처리가 충분하지 않은 경우, 작성자를 추가하여 멀티스레딩을 시도할 수 있습니다. 그러나 문서 및 트랜잭션 시퀀싱에 대한 애플리케이션 요구 사항과 이로 인해 발생하는 추가적인 복잡성을 신중하게 고려해야 합니다.

## 트랜잭션당 여러 명령문 실행
<a name="driver.best-practices.multiple-statements"></a>

[이전 섹션](#driver.best-practices.performance)에서 설명한 대로 트랜잭션당 여러 명령문을 실행하여 애플리케이션의 성능을 최적화할 수 있습니다. 다음 코드 예제에서는 테이블을 쿼리하고 트랜잭션 내에서 해당 테이블의 문서를 업데이트합니다. Lambda 표현식을 `execute` 작업에 전달하여 이를 수행할 수 있습니다.

------
#### [ Java ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static boolean InsureCar(QldbDriver qldbDriver, final String vin) {
    final IonSystem ionSystem = IonSystemBuilder.standard().build();
    final IonString ionVin = ionSystem.newString(vin);

    return qldbDriver.execute(txn -> {
        Result result = txn.execute(
                "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE",
                ionVin);
        if (!result.isEmpty()) {
            txn.execute("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------
#### [ .NET ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
public static async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin)
{
    ValueFactory valueFactory = new ValueFactory();
    IIonValue ionVin = valueFactory.NewString(vin);

    return await driver.Execute(async txn =>
    {
        // Check if the vehicle is insured.
        Amazon.QLDB.Driver.IAsyncResult result = await txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin);

        if (await result.CountAsync() > 0)
        {
            // If the vehicle is not insured, insure it.
            await txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin);
            return true;
        }
        return false;
    });
}
```

------
#### [ Go ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
func InsureCar(driver *qldbdriver.QLDBDriver, vin string) (bool, error) {
    insured, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {

        result, err := txn.Execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
        if err != nil {
            return false, err
        }

        hasNext := result.Next(txn)
        if !hasNext && result.Err() != nil {
            return false, result.Err()
        }

        if hasNext {
            _, err = txn.Execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
            if err != nil {
                return false, err
            }
            return true, nil
        }
        return false, nil
    })
    if err != nil {
        panic(err)
    }

    return insured.(bool), err
}
```

------
#### [ Node.js ]

```
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
// set your UPDATE to filter on vin and insured, and check if you updated something or not.
async function insureCar(driver: QldbDriver, vin: string): Promise<boolean> {

    return await driver.executeLambda(async (txn: TransactionExecutor) => {
        const results: dom.Value[] = (await txn.execute(
            "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)).getResultList();

        if (results.length > 0) {
            await txn.execute(
                "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin);
            return true;
        }
        return false;
    });
};
```

------
#### [ Python ]

```
# This code snippet is intentionally trivial. In reality you wouldn't do this because you'd
# set your UPDATE to filter on vin and insured, and check if you updated something or not.

def do_insure_car(transaction_executor, vin):
    cursor = transaction_executor.execute_statement(
        "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)
    first_record = next(cursor, None)
    if first_record:
        transaction_executor.execute_statement(
            "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin)
        return True
    else:
        return False

def insure_car(qldb_driver, vin_to_insure):
    return qldb_driver.execute_lambda(
        lambda executor: do_insure_car(executor, vin_to_insure))
```

------

드라이버의 `execute` 작업은 세션 및 해당 세션에서 트랜잭션을 암시적으로 시작합니다. Lambda 표현식에서 실행하는 각 명령문은 트랜잭션에 래핑됩니다. 모든 명령문이 실행된 후 드라이버는 트랜잭션을 자동 커밋합니다. 자동 재시도 한도를 모두 사용한 후 명령문이 하나라도 실패하면 트랜잭션이 중단됩니다.

**트랜잭션에 예외 전파**

트랜잭션당 여러 명령문을 실행하는 경우 일반적으로 트랜잭션 내에서 예외를 캐치하고 가리지 않는 것이 좋습니다.

예를 들어, Java에서 다음 프로그램은 `RuntimeException`의 모든 인스턴스를 캐치하고 오류를 기록하고 계속합니다. 이 코드 예제는 `UPDATE` 명령문이 실패하더라도 트랜잭션이 성공하기 때문에 잘못된 관행으로 간주됩니다. 따라서 클라이언트는 업데이트가 성공하지 못했지만 성공했다고 가정할 수 있습니다.

**주의**  
이 코드 예제를 사용하지 마세요. 이 예제는 잘못된 관행으로 간주되는 안티 패턴 예제를 보여주기 위해 제공되었습니다.

```
// DO NOT USE this code example because it is considered bad practice
public static void main(final String... args) {
    ConnectToLedger.getDriver().execute(txn -> {
        final Result selectTableResult = txn.execute("SELECT * FROM Vehicle WHERE VIN ='123456789'");
        // Catching an error inside the transaction is an anti-pattern because the operation might
        // not succeed.
        // In this example, the transaction succeeds even when the update statement fails.
        // So, the client might assume that the update succeeded when it didn't.
        try {
            processResults(selectTableResult);
            String model = // some code that extracts the model
            final Result updateResult = txn.execute("UPDATE Vehicle SET model = ? WHERE VIN = '123456789'",
                    Constants.MAPPER.writeValueAsIonValue(model));
        } catch (RuntimeException e) {
            log.error("Exception when updating the Vehicle table {}", e.getMessage());
        }
    });
    log.info("Vehicle table updated successfully.");
}
```

대신 예외를 전파(버블 업)하세요. 트랜잭션의 일부가 실패하는 경우, 클라이언트가 그에 따라 예외를 처리할 수 있도록 `execute` 작업을 통해 트랜잭션을 중단합니다.

# Amazon QLDB의 드라이버를 사용한 재시도 정책에 대한 이해
<a name="driver-retry-policy"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 드라이버는 재시도 정책을 사용하여 실패한 트랜잭션을 투명하게 재시도하여 일시적인 예외를 처리합니다. 이러한 예외(예: `CapacityExceededException` 및 `RateExceededException`)는 일반적으로 일정 시간이 지나면 자동으로 수정됩니다. 예외와 함께 실패한 트랜잭션을 적절한 지연 후에 다시 시도하면 성공할 가능성이 높습니다. 이는 QLDB를 사용하는 애플리케이션의 안정성을 개선하는 데 도움이 됩니다.

**Topics**
+ [

## 재시도 가능한 오류 타입
](#driver-retry-policy.retryable-errors)
+ [

## 기본 키 정책
](#driver-retry-policy.default)

## 재시도 가능한 오류 타입
<a name="driver-retry-policy.retryable-errors"></a>

드라이버는 트랜잭션 내에서 작업을 수행하는 동안 다음과 같은 예외가 하나라도 발생하는 경우에만 트랜잭션을 자동으로 재시도합니다.
+ [CapacityExceededException](driver-errors.md) - 요청이 원장의 처리 용량을 초과할 때 반환됩니다.
+ [InvalidSessionException](driver-errors.md) - 세션이 더 이상 유효하지 않거나 세션이 존재하지 않을 때 반환됩니다.
+ [LimitExceededException](driver-errors.md) - 활성 세션 수와 같은 리소스 제한을 초과할 경우 반환됩니다.
+ [OccConflictException](concurrency.md) - OCC(*낙관적 동시성 제어*)의 검증 단계에서 실패로 인해 트랜잭션을 저널에 기록할 수 없을 때 반환됩니다.
+ [RateExceededException](driver-errors.md) - 요청 비율이 허용된 처리량을 초과할 때 반환됩니다.

## 기본 키 정책
<a name="driver-retry-policy.default"></a>

재시도 정책은 재시도 조건과 백오프 전략으로 구성됩니다. 재시도 조건은 트랜잭션을 재시도해야 하는 시기를 정의하고, 백오프 전략은 트랜잭션을 재시도하기 전에 기다려야 하는 시간을 정의합니다.

드라이버 인스턴스를 만들 때 기본 재시도 정책은 최대 4회까지 재시도하고 지수 백오프 전략을 사용하도록 지정합니다. 지수 백오프 전략은 지터가 동일한 최소 10밀리초와 최대 5000밀리초의 지연을 사용합니다. 재시도 정책 내에서 트랜잭션을 성공적으로 체결할 수 없는 경우 다른 시간에 트랜잭션을 시도하는 것이 좋습니다.

지수 백오프의 개념은 오류 응답이 연속될 때마다 재시도 간 대기 시간을 점진적으로 늘린다는 것입니다. 자세한 내용은 블로그 AWS 게시물 지수 백오프 및 지터를 참조하세요. [https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) 

# Amazon QLDB 드라이버에서 발생하는 일반적인 오류
<a name="driver-errors"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 섹션에서는 [QLDB 세션 API](https://docs.aws.amazon.com/qldb/latest/developerguide/API_Types_Amazon_QLDB_Session.html)와 상호 작용할 때 Amazon QLDB 드라이버에서 발생할 수 있는 런타임 오류에 대해 설명합니다.

다음은 드라이버가 반환하는 일반적인 예외 목록입니다. 각 예외에는 특정 오류 메시지와 가능한 해결 방법에 대한 간단한 설명 및 제안이 포함됩니다.<a name="driver-errors.varlist"></a>

**CapacityExceededException**  
메시지: 용량 초과  
Amazon QLDB는 원장의 처리 용량을 초과했기 때문에 요청을 거부했습니다. QLDB는 서비스의 상태와 성능을 유지하기 위해 원장당 내부 크기 조정 한도를 적용합니다. 이 한도는 각 개별 요청의 워크로드 크기에 따라 달라집니다. 예를 들어, 인덱스가 아닌 정규화된 쿼리로 인한 테이블 스캔과 같은 비효율적인 데이터 트랜잭션을 수행하는 요청의 경우 워크로드가 증가할 수 있습니다.  
요청을 재시도하기 전에 잠시 기다릴 것을 권장합니다. 애플리케이션에서 이 예외가 지속적으로 발생하는 경우 문을 최적화하고 원장에 보내는 요청의 속도와 볼륨을 줄이세요. 명령문 최적화의 예로는 트랜잭션당 더 적은 수의 명령문을 실행하고 테이블 인덱스를 조정하는 경우를 들 수 있습니다. 명령문을 최적화하고 테이블 스캔을 방지하는 방법을 알아보려면 [쿼리 성능 최적화](working.optimize.md)을 참조하세요.  
또한 최신 버전의 QLDB 드라이버를 사용할 것을 권장합니다. 드라이버에는 [지수 백오프 및 Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)를 사용하여 이와 같은 예외가 발생할 경우 자동으로 재시도하는 기본 재시도 정책이 있습니다. 지수 백오프의 개념은 오류 응답이 연속될 때마다 재시도 간 대기 시간을 점진적으로 늘린다는 것입니다.

**InvalidSessionException**  
메시지: 트랜잭션 *transactionId*가 만료되었습니다  
트랜잭션이 최대 수명을 초과했습니다. 트랜잭션은 커밋되기 전까지 최대 30초 동안 실행될 수 있습니다. 이 시간 초과 제한 이후에는 트랜잭션에 대해 수행된 모든 작업이 거부되고 QLDB는 세션을 삭제합니다. 이 제한은 트랜잭션을 시작하고 커밋하거나 취소하지 않기 때문에 클라이언트의 세션 유출을 방지합니다.  
이것이 애플리케이션에서 일반적인 예외인 경우, 트랜잭션을 실행하는 데 시간이 너무 오래 걸릴 수 있습니다. 트랜잭션 런타임이 30초 이상 걸리는 경우 문을 최적화하여 트랜잭션 속도를 높이세요. 명령문 최적화의 예로는 트랜잭션당 더 적은 수의 명령문을 실행하고 테이블 인덱스를 조정하는 경우를 들 수 있습니다. 자세한 내용은 [쿼리 성능 최적화](working.optimize.md)을 참조하세요.

**InvalidSessionException**  
메시지: 세션 *sessionId*가 만료되었습니다  
최대 총 수명을 초과했기 때문에 QLDB가 세션을 삭제했습니다. QLDB는 활성 트랜잭션과 상관없이 13\$117분 후에 세션을 삭제합니다. 세션은 하드웨어 장애, 네트워크 장애 또는 애플리케이션 재시작과 같은 여러 가지 이유로 손실되거나 손상될 수 있습니다. 따라서 QLDB는 클라이언트 소프트웨어가 세션 장애에 대해 복원력을 유지할 수 있도록 세션에 최대 수명을 적용합니다.  
이 예외가 발생하는 경우 새 세션을 획득하고 트랜잭션을 다시 시도할 것을 권장합니다. 또한 애플리케이션을 대신하여 세션 풀과 해당 상태를 관리하는 최신 버전의 QLDB 드라이버를 사용할 것을 권장합니다.

**InvalidSessionException**  
메시지: 해당 세션이 없습니다  
클라이언트가 존재하지 않는 세션을 사용하여 QLDB와 트랜잭션을 시도했습니다. 클라이언트가 이전에 존재했던 세션을 사용하고 있다고 가정하면 다음 중 하나로 인해 세션이 더 이상 존재하지 않을 수 있습니다.  
+ 세션이 내부 서버 장애(즉, HTTP 응답 코드 500 오류)와 관련된 경우, QLDB는 고객이 불확실한 상태의 세션과 트랜잭션하도록 허용하는 대신 해당 세션을 완전히 삭제하도록 선택할 수 있습니다. 그러면 해당 세션에 대한 모든 재시도가 실패하고 이 오류가 발생합니다.
+ QLDB는 만료된 세션을 결국 잊어버립니다. 그런 다음 세션을 계속 사용하려고 하면 초기 `InvalidSessionException`이 아닌 이 오류가 발생합니다.
이 예외가 발생하는 경우 새 세션을 획득하고 트랜잭션을 다시 시도할 것을 권장합니다. 또한 애플리케이션을 대신하여 세션 풀과 해당 상태를 관리하는 최신 버전의 QLDB 드라이버를 사용할 것을 권장합니다.

**RateExceededException**  
메시지: 속도가 초과되었습니다  
QLDB는 발신자의 ID를 기반으로 클라이언트를 제한했습니다. QLDB는 [토큰 버킷](https://en.wikipedia.org/wiki/Token_bucket) 제한 알고리즘을 사용하여 리전별, 계정별로 제한을 적용합니다. QLDB는 서비스의 성과를 돕고 모든 QLDB 고객의 공정한 사용을 보장하기 위해 이를 수행합니다. 예를 들어, `StartSessionRequest` 작업을 사용하여 많은 수의 동시 세션을 획득하려고 하면 제한이 발생할 수 있습니다.  
애플리케이션 상태를 유지하고 추가적인 제한을 완화하기 위해 [지수 백오프 및 Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)를 사용하여 이 예외를 다시 시도할 수 있습니다. 지수 백오프의 개념은 오류 응답이 연속될 때마다 재시도 간 대기 시간을 점진적으로 늘린다는 것입니다. 최신 버전의 QLDB 드라이버를 사용하는 것이 좋습니다. 드라이버에는 지수 백오프 및 Jitter를 사용하여 이와 같은 예외가 발생할 경우 자동으로 재시도하는 기본 재시도 정책이 있습니다.  
최신 버전의 QLDB 드라이버는 애플리케이션이 `StartSessionRequest` 호출에 대해 QLDB에 의해 지속적으로 제한되는 경우에도 도움이 될 수 있습니다. 드라이버는 여러 트랜잭션에서 재사용되는 세션 풀을 유지 관리하므로 애플리케이션이 수행하는 `StartSessionRequest` 호출 수를 줄이는데 도움이 될 수 있습니다. API 호출 한도 증가를 요청하려면 [AWS Support 센터](https://console.aws.amazon.com/support/home#/)로 문의하세요.

**LimitExceededException**  
메시지: 세션 제한을 초과했습니다  
원장이 활성 세션 수의 할당량(*한도*라고도 함)을 초과했습니다. 이 할당량은 [Amazon QLDB 할당량 및 제한](limits.md)에 정의되어 있습니다. 원장의 활성 세션 수는 최종적으로 일정하게 유지되며, 할당량 근처에서 지속적으로 실행되는 원장에서는 이 예외가 주기적으로 발생할 수 있습니다.  
애플리케이션의 상태를 유지하려면 이 예외를 다시 시도할 것을 권장합니다. 이 예외를 피하려면 모든 클라이언트에서 단일 원장에 1,500개 이상의 동시 세션을 사용하도록 구성하지 않았는지 확인하세요. 예를 들어, [Java용 Amazon QLDB 드라이버](https://github.com/awslabs/amazon-qldb-driver-java/)의 [MaxConcurrentTransactions](https://github.com/awslabs/amazon-qldb-driver-java/blob/master/src/main/java/software/amazon/qldb/QldbDriverBuilder.java#L125) 메서드를 사용하여 드라이버 인스턴스에서 사용 가능한 최대 세션 수를 구성할 수 있습니다.

**QldbClientException**  
메시지: 스트리밍된 결과는 상위 트랜잭션이 열려 있을 때만 유효합니다  
트랜잭션이 종료되었으며 QLDB에서 결과를 검색하는 데 사용할 수 없습니다. 트랜잭션은 커밋되거나 취소되면 종료됩니다.  
이 예외는 클라이언트가 `Transaction` 객체로 직접 작업하고 트랜잭션을 커밋하거나 취소한 후 QLDB에서 결과를 검색하려고 할 때 발생합니다. 이 문제를 완화하려면 클라이언트가 트랜잭션을 닫기 전에 데이터를 읽어야 합니다.

# 샘플 애플리케이션 자습서를 사용하여 Amazon QLDB 시작하기
<a name="getting-started-sample-app"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 AWS SDK와 함께 Amazon QLDB 드라이버를 사용하여 QLDB 원장을 생성하고 샘플 데이터로 채웁니다. 드라이버를 사용하면 애플리케이션이 트랜잭션 데이터 API를 사용하여 QLDB와 상호 작용할 수 있습니다. AWS SDK는 QLDB 리소스 관리 API와의 상호 작용을 지원합니다.

이 시나리오에서 생성하는 샘플 원장은 차량 등록에 대한 전체 기록 정보를 추적하는 자동차 부서(DMV) 데이터베이스입니다. 다음 항목에서는 차량 등록을 추가하고, 수정하고, 해당 등록의 변경 기록을 보는 방법을 설명합니다. 또한 이 안내서는 등록 문서를 암호적으로 검증하는 방법을 보여주며, 리소스를 정리하고 샘플 원장을 삭제하는 것으로 마무리합니다.

샘플 애플리케이션 자습서는 다음 프로그래밍 언어로 제공됩니다.

**Topics**
+ [Java 자습서](getting-started.java.tutorial.md)
+ [Node.js 자습서](getting-started.nodejs.tutorial.md)
+ [Python 자습서](getting-started.python.tutorial.md)

# Amazon QLDB Java 자습서
<a name="getting-started.java.tutorial"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서 샘플 애플리케이션 구현에서는 Amazon QLDB 드라이버 AWS SDK for Java 를와 함께 사용하여 QLDB 원장을 생성하고 샘플 데이터로 채웁니다.

이 자습서로 학습하면서 관리 API 작업에 대해서 [AWS SDK for Java API 참조](https://docs.aws.amazon.com/sdk-for-java/latest/reference/)를 참조할 수 있습니다. 트랜잭션 데이터 작업의 경우 [Java용 QLDB 드라이버 API 참조](https://javadoc.io/doc/software.amazon.qldb/amazon-qldb-driver-java/latest/index.html)를 참조할 수 있습니다.

**참고**  
해당하는 경우 일부 자습서 단계에는 Java용 QLDB 드라이버의 지원되는 각 메이저 버전마다 서로 다른 명령 또는 코드 예제가 있습니다.

**Topics**
+ [

# Amazon QLDB Java 샘플 애플리케이션 설치
](sample-app.java.md)
+ [

# 1단계: 새 원장 생성
](getting-started.java.step-1.md)
+ [

# 2단계: 원장과의 연결을 테스트
](getting-started.java.step-2.md)
+ [

# 3단계: 테이블, 인덱스 및 샘플 데이터 생성
](getting-started.java.step-3.md)
+ [

# 4단계: 원장에서 테이블 쿼리
](getting-started.java.step-4.md)
+ [

# 5단계: 원장의 문서 수정
](getting-started.java.step-5.md)
+ [

# 6단계: 문서의 개정 기록 보기
](getting-started.java.step-6.md)
+ [

# 7단계: 원장에 있는 문서 검증
](getting-started.java.step-7.md)
+ [

# 8단계: 원장의 저널 데이터 내보내기 및 검증
](getting-started.java.step-8.md)
+ [

# 9단계(선택 사항): 리소스 정리
](getting-started.java.step-9.md)

# Amazon QLDB Java 샘플 애플리케이션 설치
<a name="sample-app.java"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 섹션에서는 단계별 Java 자습서를 위해 제공된 Amazon QLDB 샘플 애플리케이션을 설치하고 실행하는 방법을 설명합니다. 이 샘플 애플리케이션의 사용 사례는 차량 등록에 대한 전체 기록 정보를 추적하는 자동차 부서(DMV) 데이터베이스입니다.

Java용 DMV 샘플 애플리케이션은 GitHub 리포지토리 [aws-samples/amazon-qldb-dmv-sample-java](https://github.com/aws-samples/amazon-qldb-dmv-sample-java)의 오픈 소스입니다.

## 사전 조건
<a name="sample-app.java.prereqs"></a>

시작하기 전에 Java [사전 조건](getting-started.java.md#getting-started.java.prereqs)용 QLDB 드라이버를 완료했는지 확인합니다. 다음 내용이 포함됩니다:

1. 가입합니다 AWS.

1. 적절한 QLDB 권한을 가진 사용자를 생성합니다. 이 자습서의 모든 단계를 완료하려면 QLDB API를 통해 원장 리소스에 대한 전체 관리 액세스 권한이 필요합니다.

1. 이외의 IDE를 사용하는 경우 Java를 AWS Cloud9설치하고 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

## 설치
<a name="sample-app.java.install"></a>

다음 단계는 로컬 개발 환경에서 샘플 애플리케이션을 다운로드하고 설정하는 방법을 설명합니다. 또는를 IDE AWS Cloud9 로 사용하고 CloudFormation 템플릿을 사용하여 개발 리소스를 프로비저닝하여 샘플 애플리케이션 설정을 자동화할 수 있습니다.

### 로컬 개발 환경
<a name="sample-app.java.local-ide"></a>

이 지침은 자체 리소스 및 개발 환경을 사용하여 QLDB Java 샘플 애플리케이션을 다운로드하고 설치하는 방법을 설명합니다.

**샘플 애플리케이션을 다운로드하여 실행하려면**

1. 다음 명령을 입력하여 GitHub에서 샘플 애플리케이션을 복제합니다.

------
#### [ 2.x ]

   ```
   git clone https://github.com/aws-samples/amazon-qldb-dmv-sample-java.git
   ```

------
#### [ 1.x ]

   ```
   git clone -b v1.2.0 https://github.com/aws-samples/amazon-qldb-dmv-sample-java.git
   ```

------

   이 패키지에는 Gradle 구성 및 [Java 자습서](getting-started.java.tutorial.md)의 전체 코드가 포함되어 있습니다.

1. 제공된 애플리케이션을 로드하고 실행합니다.
   + Eclipse를 사용하는 경우:

     1. Eclipse를 시작하고 **Eclipse** 메뉴에서 **파일**, **가져오기**, **기존 Gradle 프로젝트**를 차례로 선택합니다.

     1. 프로젝트 루트 디렉터리에서 `build.gradle` 파일이 포함된 애플리케이션 디렉터리를 찾아 선택합니다. 그런 다음 **완료**를 선택하여 기본 Gradle 설정을 가져오기에 사용합니다.

     1. 예를 들어, `ListLedgers` 프로그램을 실행해 볼 수 있습니다. `ListLedgers.java` 파일에 대한 컨텍스트 메뉴를 열고(마우스 오른쪽 버튼 클릭) **Java 애플리케이션으로 실행**을 선택합니다.
   + IntelliJ를 사용하는 경우:

     1. IntelliJ를 시작하고 **IntelliJ** 메뉴에서 **파일**을 선택한 다음 **열기**를 선택합니다.

     1. 프로젝트 루트 디렉터리에서 `build.gradle` 파일이 포함된 애플리케이션 디렉터리를 찾아 선택합니다. 그 다음에 **확인**을 선택합니다. 기본 설정을 유지하고 **확인**을 다시 선택합니다.

     1. 예를 들어, `ListLedgers` 프로그램을 실행해 볼 수 있습니다. `ListLedgers.java` 파일에 대한 컨텍스트 메뉴를 열고(마우스 오른쪽 버튼 클릭) **'ListLedgers’ 실행**을 선택합니다.

1. [1단계: 새 원장 생성](getting-started.java.step-1.md)로 진행하여 자습서를 시작하고 원장을 생성하세요.

### AWS Cloud9
<a name="sample-app.java.cfn-ac9"></a>

이 지침에서는 [AWS Cloud9](https://aws.amazon.com/cloud9)을 IDE로 사용하여 Java용 Amazon QLDB 차량 등록 샘플 애플리케이션을 자동으로 설정하는 방법을 설명합니다. 이 안내서에서는 [CloudFormation](https://aws.amazon.com/cloudformation) 템플릿을 사용하여 개발 리소스를 프로비저닝합니다.

에 대한 자세한 내용은 [AWS Cloud9 사용 설명서를](https://docs.aws.amazon.com/cloud9/latest/user-guide/) AWS Cloud9참조하세요. CloudFormation에 대한 자세한 내용은 [AWS CloudFormation 사용 설명서](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/)를 참조하세요.

**Topics**
+ [

#### 1부: 리소스 프로비저닝
](#sample-app.java.cfn-ac9.step-1)
+ [

#### 2부: IDE 설정
](#sample-app.java.cfn-ac9.step-2)
+ [

#### 3부: QLDB DMV 샘플 애플리케이션 실행
](#sample-app.java.cfn-ac9.step-3)

#### 1부: 리소스 프로비저닝
<a name="sample-app.java.cfn-ac9.step-1"></a>

이 첫 번째 단계에서는 CloudFormation 를 사용하여 Amazon QLDB 샘플 애플리케이션을 사용하여 개발 환경을 설정하는 데 필요한 리소스를 프로비저닝합니다.

**CloudFormation 콘솔을 열고 QLDB 샘플 애플리케이션 템플릿을 로드하려면**

1. 에 로그인 AWS Management Console 하고 [https://console.aws.amazon.com/cloudformation](https://console.aws.amazon.com/cloudformation/) CloudFormation 콘솔을 엽니다.

   QLDB를 지원하는 리전으로 전환합니다. 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요. 의 다음 스크린샷은 미국 동부(버지니아 북부)를 선택한 로 AWS Management Console 보여줍니다 AWS 리전.  
![\[AWS Management Console 미국 동부(버지니아 북부)를 선택한 로 표시합니다 AWS 리전.\]](http://docs.aws.amazon.com/ko_kr/qldb/latest/developerguide/images/cfn-ac9/aws-region-us-east-1.png)

1.  CloudFormation 콘솔에서 **스택 생성을** 선택한 다음 **새 리소스 사용(표준)**을 선택합니다.

1. **템플릿 지정** 아래 **스택 생성** 페이지에서 **Amazon S3 URL**을 선택합니다.

1. 다음 URL을 입력하고 **다음**을 선택합니다.

   ```
   https://amazon-qldb-assets.s3.amazonaws.com/templates/QLDB-DMV-SampleApp.yml
   ```

1. **qldb-sample-app**와 같은 **스택 이름**을 입력한 후 **다음**을 선택합니다.

1. 원하는 대로 태그를 추가하고 기본 옵션을 유지할 수 있습니다. 그런 다음 **다음**을 선택합니다.

1. 스택 설정을 검토하고 **스택 생성**을 선택합니다. CloudFormation 스크립트를 완료하는 데 몇 분 정도 걸릴 수 있습니다.

   이 스크립트는이 자습서에서 QLDB 샘플 애플리케이션을 실행하는 데 사용하는 연결된 Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스로 AWS Cloud9 환경을 프로비저닝합니다. 또한 [aws-samples/amazon-qldb-dmv-sample-java](https://github.com/aws-samples/amazon-qldb-dmv-sample-java/) 리포지토리를 GitHub에서 AWS Cloud9 개발 환경으로 복제합니다.

#### 2부: IDE 설정
<a name="sample-app.java.cfn-ac9.step-2"></a>

이 단계에서는 클라우드 개발 환경 설정을 완료합니다. 제공된 쉘 스크립트를 다운로드하고 실행하여 샘플 애플리케이션의 종속성을 사용하여 AWS Cloud9 IDE를 설정합니다.

**AWS Cloud9 환경을 설정하려면**

1. [https://console.aws.amazon.com/cloud9/](https://console.aws.amazon.com/cloud9/) AWS Cloud9 콘솔을 엽니다.

1. **사용자 환경**에서 **QLDB DMV 샘플 애플리케이션**이라는 환경 이름의 카드를 찾아 **IDE 열기**를 선택합니다. 기본 EC2 인스턴스가 시작될 때 환경을 로드하는 데 1분 정도 걸릴 수 있습니다.

    AWS Cloud9 환경은 자습서를 실행하는 데 필요한 시스템 종속성으로 사전 구성되어 있습니다. 콘솔의 **환경** 탐색 창에 `QLDB DMV Sample Application`로 이름이 지정된 폴더가 표시되는지 확인합니다. AWS Cloud9 콘솔의 다음 스크린샷은 QLDB DMV 샘플 애플리케이션 환경 폴더 창을 보여줍니다.  
![\[AWS Cloud9 QLDB DMV 샘플 애플리케이션 환경 폴더 창을 보여주는 콘솔.\]](http://docs.aws.amazon.com/ko_kr/qldb/latest/developerguide/images/cfn-ac9/cloud9-folders.png)

   탐색 창이 보이지 않으면 콘솔 왼쪽에 있는 **환경** 탭을 전환하세요. 패널에 폴더가 보이지 않는 경우, 설정 아이콘(![\[Settings icon\]](http://docs.aws.amazon.com/ko_kr/qldb/latest/developerguide/images/settings.png))을 사용하여 **환경 루트 표시**를 활성화하세요.

1. 콘솔 하단 창에 열린 `bash` 터미널 창이 보일 것입니다. 이 화면이 보이지 않으면 콘솔 상단의 **창** 메뉴에서 **새 터미널**을 선택합니다.

1. 그런 다음 설치 스크립트를 다운로드하여 실행하여 OpenJDK 8을 설치하고 해당하는 경우 Git 리포지토리에서 적절한 브랜치를 확인합니다. 이전 단계에서 생성한 AWS Cloud9 터미널에서 다음 두 명령을 순서대로 실행합니다.

------
#### [ 2.x ]

   ```
   aws s3 cp s3://amazon-qldb-assets/setup-scripts/dmv-setup-v2.sh .
   ```

   ```
   sh dmv-setup-v2.sh
   ```

------
#### [ 1.x ]

   ```
   aws s3 cp s3://amazon-qldb-assets/setup-scripts/dmv-setup.sh .
   ```

   ```
   sh dmv-setup.sh
   ```

------

   완료되면 터미널에서 다음 메시지가 출력됩니다.

   ```
   ** DMV Sample App setup completed , enjoy!! **
   ```

1. 잠시 시간을 내어 샘플 애플리케이션 코드 AWS Cloud9, 특히 디렉터리 경로에서 샘플 애플리케이션 코드를 찾아보십시오`src/main/java/software/amazon/qldb/tutorial`.

#### 3부: QLDB DMV 샘플 애플리케이션 실행
<a name="sample-app.java.cfn-ac9.step-3"></a>

이 단계에서는를 사용하여 Amazon QLDB DMV 샘플 애플리케이션 작업을 실행하는 방법을 알아봅니다 AWS Cloud9. 샘플 코드를 실행하려면 *2부: IDE 설정*에서와 같이 AWS Cloud9 터미널로 돌아가거나 새 터미널 창을 생성합니다.

**샘플 애플리케이션을 실행하려면**

1. 터미널에서 다음 명령을 실행하여 프로젝트 루트 디렉터리로 전환합니다.

   ```
   cd ~/environment/amazon-qldb-dmv-sample-java
   ```

   다음 디렉터리 경로에서 예제를 실행하고 있는지 확인하세요.

   ```
   /home/ec2-user/environment/amazon-qldb-dmv-sample-java/
   ```

1. 다음 명령어는 각 작업을 실행하는 Gradle 구문을 보여줍니다.

   ```
   ./gradlew run -Dtutorial=Task
   ```

   예를 들어 다음 명령을 실행하여 AWS 계정 및 현재 리전의 모든 원장을 나열합니다.

   ```
   ./gradlew run -Dtutorial=ListLedgers
   ```

1. [1단계: 새 원장 생성](getting-started.java.step-1.md)로 진행하여 자습서를 시작하고 원장을 생성하세요.

1. (선택 사항) 자습서를 완료한 후 CloudFormation 리소스가 더 이상 필요하지 않으면 정리하세요.

   1. [https://console.aws.amazon.com/cloudformation](https://console.aws.amazon.com/cloudformation/) CloudFormation 콘솔을 열고 *1부: 리소스 프로비저닝에서 생성한 스택을 삭제합니다*.

   1. 또한 CloudFormation 템플릿이 생성한 AWS Cloud9 스택을 삭제합니다.

# 1단계: 새 원장 생성
<a name="getting-started.java.step-1"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 단계에서는 `vehicle-registration`라는 이름의 새 Amazon QLDB 원장을 생성합니다

**새 원장을 생성하려면**

1. 이 자습서의 다른 모든 프로그램에서 사용하는 상수 값이 들어 있는 다음 파일(`Constants.java`)을 검토하세요.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonSystem;
   import com.amazon.ion.system.IonSystemBuilder;
   import com.fasterxml.jackson.databind.SerializationFeature;
   import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
   import com.fasterxml.jackson.dataformat.ion.ionvalue.IonValueMapper;
   
   /**
    * Constant values used throughout this tutorial.
    */
   public final class Constants {
       public static final int RETRY_LIMIT = 4;
       public static final String LEDGER_NAME = "vehicle-registration";
       public static final String STREAM_NAME = "vehicle-registration-stream";
       public static final String VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration";
       public static final String VEHICLE_TABLE_NAME = "Vehicle";
       public static final String PERSON_TABLE_NAME = "Person";
       public static final String DRIVERS_LICENSE_TABLE_NAME = "DriversLicense";
       public static final String VIN_INDEX_NAME = "VIN";
       public static final String PERSON_GOV_ID_INDEX_NAME = "GovId";
       public static final String VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber";
       public static final String DRIVER_LICENSE_NUMBER_INDEX_NAME = "LicenseNumber";
       public static final String DRIVER_LICENSE_PERSONID_INDEX_NAME = "PersonId";
       public static final String JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export";
       public static final String USER_TABLES = "information_schema.user_tables";
       public static final String LEDGER_NAME_WITH_TAGS = "tags";
       public static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
       public static final IonObjectMapper MAPPER = new IonValueMapper(SYSTEM);
   
       private Constants() { }
   
       static {
           MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
       }
   }
   ```

------
#### [ 1.x ]

**중요**  
Amazon Ion 패키지의 경우 애플리케이션에서 네임스페이스 `com.amazon.ion`을 사용해야 합니다. 는 네임스페이스 아래의 다른 Ion 패키지에 AWS SDK for Java 종속되지만 `software.amazon.ion`QLDB 드라이버와 호환되지 않는 레거시 패키지입니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonSystem;
   import com.amazon.ion.system.IonSystemBuilder;
   import com.fasterxml.jackson.databind.SerializationFeature;
   import com.fasterxml.jackson.dataformat.ion.IonObjectMapper;
   import com.fasterxml.jackson.dataformat.ion.ionvalue.IonValueMapper;
   
   /**
    * Constant values used throughout this tutorial.
    */
   public final class Constants {
       public static final int RETRY_LIMIT = 4;
       public static final String LEDGER_NAME = "vehicle-registration";
       public static final String STREAM_NAME = "vehicle-registration-stream";
       public static final String VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration";
       public static final String VEHICLE_TABLE_NAME = "Vehicle";
       public static final String PERSON_TABLE_NAME = "Person";
       public static final String DRIVERS_LICENSE_TABLE_NAME = "DriversLicense";
       public static final String VIN_INDEX_NAME = "VIN";
       public static final String PERSON_GOV_ID_INDEX_NAME = "GovId";
       public static final String VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber";
       public static final String DRIVER_LICENSE_NUMBER_INDEX_NAME = "LicenseNumber";
       public static final String DRIVER_LICENSE_PERSONID_INDEX_NAME = "PersonId";
       public static final String JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export";
       public static final String USER_TABLES = "information_schema.user_tables";
       public static final String LEDGER_NAME_WITH_TAGS = "tags";
       public static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
       public static final IonObjectMapper MAPPER = new IonValueMapper(SYSTEM);
   
       private Constants() { }
   
       static {
           MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
       }
   }
   ```

------
**참고**  
이 `Constants` 클래스에는 오픈 소스 Jackson `IonValueMapper` 클래스의 인스턴스가 포함되어 있습니다. 읽기 및 쓰기 트랜잭션을 수행할 때 이 매퍼를 사용하여 [Amazon Ion](ion.md) 데이터를 처리할 수 있습니다.

   또한 `CreateLedger.java` 파일은 원장의 현재 상태를 설명하는 다음 프로그램(`DescribeLedger.java`)에 대한 종속성을 가집니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.DescribeLedgerRequest;
   import com.amazonaws.services.qldb.model.DescribeLedgerResult;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Describe a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class DescribeLedger {
       public static AmazonQLDB client = CreateLedger.getClient();
       public static final Logger log = LoggerFactory.getLogger(DescribeLedger.class);
   
       private DescribeLedger() { }
   
       public static void main(final String... args) {
           try {
   
               describe(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to describe a ledger!", e);
           }
       }
   
       /**
        * Describe a ledger.
        *
        * @param name
        *              Name of the ledger to describe.
        * @return {@link DescribeLedgerResult} from QLDB.
        */
       public static DescribeLedgerResult describe(final String name) {
           log.info("Let's describe ledger with name: {}...", name);
           DescribeLedgerRequest request = new DescribeLedgerRequest().withName(name);
           DescribeLedgerResult result = client.describeLedger(request);
           log.info("Success. Ledger description: {}", result);
           return result;
       }
   }
   ```

1. `CreateLedger.java` 프로그램을 컴파일하고 실행하여 `vehicle-registration`라는 이름의 원장을 생성합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.client.builder.AwsClientBuilder;
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.AmazonQLDBClientBuilder;
   import com.amazonaws.services.qldb.model.CreateLedgerRequest;
   import com.amazonaws.services.qldb.model.CreateLedgerResult;
   import com.amazonaws.services.qldb.model.DescribeLedgerResult;
   import com.amazonaws.services.qldb.model.LedgerState;
   import com.amazonaws.services.qldb.model.PermissionsMode;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Create a ledger and wait for it to be active.
    * <p>
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateLedger {
       public static final Logger log = LoggerFactory.getLogger(CreateLedger.class);
       public static final Long LEDGER_CREATION_POLL_PERIOD_MS = 10_000L;
       public static String endpoint = null;
       public static String region = null;
       public static AmazonQLDB client = getClient();
   
       private CreateLedger() {
       }
   
       /**
        * Build a low-level QLDB client.
        *
        * @return {@link AmazonQLDB} control plane client.
        */
       public static AmazonQLDB getClient() {
           AmazonQLDBClientBuilder builder = AmazonQLDBClientBuilder.standard();
           if (null != endpoint && null != region) {
               builder.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region));
           }
           return builder.build();
       }
   
       public static void main(final String... args) throws Exception {
           try {
               client = getClient();
   
               create(Constants.LEDGER_NAME);
   
               waitForActive(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to create the ledger!", e);
               throw e;
           }
       }
   
       /**
        * Create a new ledger with the specified ledger name.
        *
        * @param ledgerName Name of the ledger to be created.
        * @return {@link CreateLedgerResult} from QLDB.
        */
       public static CreateLedgerResult create(final String ledgerName) {
           log.info("Let's create the ledger with name: {}...", ledgerName);
           CreateLedgerRequest request = new CreateLedgerRequest()
                   .withName(ledgerName)
                   .withPermissionsMode(PermissionsMode.ALLOW_ALL);
           CreateLedgerResult result = client.createLedger(request);
           log.info("Success. Ledger state: {}.", result.getState());
           return result;
       }
   
       /**
        * Wait for a newly created ledger to become active.
        *
        * @param ledgerName Name of the ledger to wait on.
        * @return {@link DescribeLedgerResult} from QLDB.
        * @throws InterruptedException if thread is being interrupted.
        */
       public static DescribeLedgerResult waitForActive(final String ledgerName) throws InterruptedException {
           log.info("Waiting for ledger to become active...");
           while (true) {
               DescribeLedgerResult result = DescribeLedger.describe(ledgerName);
               if (result.getState().equals(LedgerState.ACTIVE.name())) {
                   log.info("Success. Ledger is active and ready to use.");
                   return result;
               }
               log.info("The ledger is still creating. Please wait...");
               Thread.sleep(LEDGER_CREATION_POLL_PERIOD_MS);
           }
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.AmazonQLDBClientBuilder;
   import com.amazonaws.services.qldb.model.CreateLedgerRequest;
   import com.amazonaws.services.qldb.model.CreateLedgerResult;
   import com.amazonaws.services.qldb.model.DescribeLedgerResult;
   import com.amazonaws.services.qldb.model.LedgerState;
   import com.amazonaws.services.qldb.model.PermissionsMode;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Create a ledger and wait for it to be active.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateLedger {
       public static final Logger log = LoggerFactory.getLogger(CreateLedger.class);
       public static final Long LEDGER_CREATION_POLL_PERIOD_MS = 10_000L;
       public static AmazonQLDB client = getClient();
   
       private CreateLedger() { }
   
       /**
        * Build a low-level QLDB client.
        *
        * @return {@link AmazonQLDB} control plane client.
        */
       public static AmazonQLDB getClient() {
           return AmazonQLDBClientBuilder.standard().build();
       }
   
       public static void main(final String... args) throws Exception {
           try {
   
               create(Constants.LEDGER_NAME);
   
               waitForActive(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to create the ledger!", e);
               throw e;
           }
       }
   
       /**
        * Create a new ledger with the specified ledger name.
        *
        * @param ledgerName
        *              Name of the ledger to be created.
        * @return {@link CreateLedgerResult} from QLDB.
        */
       public static CreateLedgerResult create(final String ledgerName) {
           log.info("Let's create the ledger with name: {}...", ledgerName);
           CreateLedgerRequest request = new CreateLedgerRequest()
                   .withName(ledgerName)
                   .withPermissionsMode(PermissionsMode.ALLOW_ALL);
           CreateLedgerResult result = client.createLedger(request);
           log.info("Success. Ledger state: {}.", result.getState());
           return result;
       }
   
       /**
        * Wait for a newly created ledger to become active.
        *
        * @param ledgerName
        *              Name of the ledger to wait on.
        * @return {@link DescribeLedgerResult} from QLDB.
        * @throws InterruptedException if thread is being interrupted.
        */
       public static DescribeLedgerResult waitForActive(final String ledgerName) throws InterruptedException {
           log.info("Waiting for ledger to become active...");
           while (true) {
               DescribeLedgerResult result = DescribeLedger.describe(ledgerName);
               if (result.getState().equals(LedgerState.ACTIVE.name())) {
                   log.info("Success. Ledger is active and ready to use.");
                   return result;
               }
               log.info("The ledger is still creating. Please wait...");
               Thread.sleep(LEDGER_CREATION_POLL_PERIOD_MS);
           }
       }
   }
   ```

------
**참고**  
`createLedger` 호출 시 원장 이름과 권한 모드를 지정해야 합니다. 원장 데이터의 보안을 극대화하려면 `STANDARD` 권한 모드를 사용할 것을 권장합니다.
원장을 생성할 때 기본적으로 *삭제 방지*가 활성화됩니다. 이 기능은 사용자가 원장을 삭제하는 것을 방지하는 QLDB의 기능입니다. QLDB API 또는 AWS Command Line Interface ()를 사용하여 원장 생성 시 삭제 방지 기능을 비활성화할 수 있습니다AWS CLI.
선택적으로, 원장에 첨부할 태그를 지정할 수도 있습니다.

새 원장과의 연결을 확인하려면 [2단계: 원장과의 연결을 테스트](getting-started.java.step-2.md)로 이동하세요.

# 2단계: 원장과의 연결을 테스트
<a name="getting-started.java.step-2"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 단계에서는 트랜잭션 데이터 API 엔드포인트를 사용하여 Amazon QLDB의 `vehicle-registration` 원장에 연결할 수 있는지 확인합니다.

**원장과의 연결을 테스트하려면**

1. `vehicle-registration` 원장에 대한 데이터 세션 연결을 생성하는 다음 프로그램(`ConnectToLedger.java`)을 검토합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.net.URI;
   import java.net.URISyntaxException;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
   import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
   import software.amazon.awssdk.services.qldbsession.QldbSessionClientBuilder;
   import software.amazon.qldb.QldbDriver;
   import software.amazon.qldb.RetryPolicy;
   
   /**
    * Connect to a session for a given ledger using default settings.
    * <p>
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class ConnectToLedger {
       public static final Logger log = LoggerFactory.getLogger(ConnectToLedger.class);
       public static AwsCredentialsProvider credentialsProvider;
       public static String endpoint = null;
       public static String ledgerName = Constants.LEDGER_NAME;
       public static String region = null;
       public static QldbDriver driver;
   
       private ConnectToLedger() {
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @param retryAttempts How many times the transaction will be retried in
        * case of a retryable issue happens like Optimistic Concurrency Control exception,
        * server side failures or network issues.
        * @return The pooled driver for creating sessions.
        */
       public static QldbDriver createQldbDriver(int retryAttempts) {
           QldbSessionClientBuilder builder = getAmazonQldbSessionClientBuilder();
           return QldbDriver.builder()
                                .ledger(ledgerName)
                                .transactionRetryPolicy(RetryPolicy
                                      .builder()
                                      .maxRetries(retryAttempts)
                                      .build())
                                .sessionClientBuilder(builder)
                                .build();
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static QldbDriver createQldbDriver() {
           QldbSessionClientBuilder builder = getAmazonQldbSessionClientBuilder();
           return QldbDriver.builder()
               .ledger(ledgerName)
               .transactionRetryPolicy(RetryPolicy.builder()
                                                  .maxRetries(Constants.RETRY_LIMIT).build())
               .sessionClientBuilder(builder)
               .build();
       }
   
       /**
        * Creates a QldbSession builder that is passed to the QldbDriver to connect to the Ledger.
        *
        * @return An instance of the AmazonQLDBSessionClientBuilder
        */
       public static QldbSessionClientBuilder getAmazonQldbSessionClientBuilder() {
           QldbSessionClientBuilder builder = QldbSessionClient.builder();
           if (null != endpoint && null != region) {
               try {
                   builder.endpointOverride(new URI(endpoint));
               } catch (URISyntaxException e) {
                   throw new IllegalArgumentException(e);
               }
           }
           if (null != credentialsProvider) {
               builder.credentialsProvider(credentialsProvider);
           }
           return builder;
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static QldbDriver getDriver() {
           if (driver == null) {
               driver = createQldbDriver();
           }
           return driver;
       }
   
   
       public static void main(final String... args) {
           Iterable<String> tables = ConnectToLedger.getDriver().getTableNames();
           log.info("Existing tables in the ledger:");
           for (String table : tables) {
               log.info("- {} ", table);
           }
       }
   }
   ```

**참고**  
원장에서 데이터 작업을 실행하려면 특정 원장에 연결할 `QldbDriver` 클래스 인스턴스를 생성해야 합니다. 이는 이전 단계에서 원장을 생성할 때 사용한 `AmazonQLDB` 클라이언트와는 다른 클라이언트 객체입니다. 이전 클라이언트는 [Amazon QLDB API 참조](api-reference.md)에 나열된 관리 API 작업을 실행하는 데만 사용됩니다.
먼저 `QldbDriver` 객체를 생성해야 합니다. 이 드라이버를 생성할 때 원장 이름을 지정해야 합니다.  
그런 다음 이 드라이버의 `execute` 메서드를 사용하여 PartiQL 명령문을 실행할 수 있습니다.
선택적으로 트랜잭션 예외에 대한 최대 재시도 횟수를 지정할 수 있습니다. 이 `execute` 메서드는 OCC(낙관적 동시성 제어) 충돌 및 기타 일반적인 일시적 예외를 이 구성 가능한 한도까지 자동으로 재시도합니다. 기본값은 `4`입니다.  
한도에 도달한 후에도 트랜잭션이 여전히 실패하면 드라이버에서 예외가 발생합니다. 자세한 내용은 [Amazon QLDB의 드라이버를 사용한 재시도 정책에 대한 이해](driver-retry-policy.md)을 참조하십시오.

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.auth.AWSCredentialsProvider;
   import com.amazonaws.client.builder.AwsClientBuilder;
   import com.amazonaws.services.qldbsession.AmazonQLDBSessionClientBuilder;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.PooledQldbDriver;
   import software.amazon.qldb.QldbDriver;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.exceptions.QldbClientException;
   
   /**
    * Connect to a session for a given ledger using default settings.
    * <p>
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class ConnectToLedger {
       public static final Logger log = LoggerFactory.getLogger(ConnectToLedger.class);
       public static AWSCredentialsProvider credentialsProvider;
       public static String endpoint = null;
       public static String ledgerName = Constants.LEDGER_NAME;
       public static String region = null;
       private static PooledQldbDriver driver;
   
       private ConnectToLedger() {
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static PooledQldbDriver createQldbDriver() {
           AmazonQLDBSessionClientBuilder builder = AmazonQLDBSessionClientBuilder.standard();
           if (null != endpoint && null != region) {
               builder.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region));
           }
           if (null != credentialsProvider) {
               builder.setCredentials(credentialsProvider);
           }
           return PooledQldbDriver.builder()
                   .withLedger(ledgerName)
                   .withRetryLimit(Constants.RETRY_LIMIT)
                   .withSessionClientBuilder(builder)
                   .build();
       }
   
       /**
        * Create a pooled driver for creating sessions.
        *
        * @return The pooled driver for creating sessions.
        */
       public static PooledQldbDriver getDriver() {
           if (driver == null) {
               driver = createQldbDriver();
           }
           return driver;
       }
   
       /**
        * Connect to a ledger through a {@link QldbDriver}.
        *
        * @return {@link QldbSession}.
        */
       public static QldbSession createQldbSession() {
           return getDriver().getSession();
       }
   
       public static void main(final String... args) {
           try (QldbSession qldbSession = createQldbSession()) {
               log.info("Listing table names ");
               for (String tableName : qldbSession.getTableNames()) {
                   log.info(tableName);
               }
           } catch (QldbClientException e) {
               log.error("Unable to create session.", e);
           }
       }
   }
   ```

**참고**  
원장에서 데이터 작업을 실행하려면 특정 원장에 연결할 `PooledQldbDriver` 또는 `QldbDriver` 클래스 인스턴스를 생성해야 합니다. 이는 이전 단계에서 원장을 생성할 때 사용한 `AmazonQLDB` 클라이언트와는 다른 클라이언트 객체입니다. 이전 클라이언트는 [Amazon QLDB API 참조](api-reference.md)에 나열된 관리 API 작업을 실행하는 데만 사용됩니다.  
`QldbDriver`로 사용자 지정 세션 풀을 구현해야 하는 경우가 아니라면 `PooledQldbDriver`를 사용하는 것이 좋습니다. `PooledQldbDriver`의 기본 풀 크기는 세션 클라이언트가 허용하는 [최대 열린 HTTP 연결 수](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html#getMaxConnections--)입니다.
먼저 `PooledQldbDriver` 객체를 생성해야 합니다. 이 드라이버를 생성할 때 원장 이름을 지정해야 합니다.  
그런 다음 이 드라이버의 `execute` 메서드를 사용하여 PartiQL 명령문을 실행할 수 있습니다. 또는 이 풀링된 드라이버 객체에서 세션을 수동으로 만들고 세션의 `execute` 메서드를 사용할 수 있습니다. 세션은 원장과의 단일 연결을 나타냅니다.
선택적으로 트랜잭션 예외에 대한 최대 재시도 횟수를 지정할 수 있습니다. 이 `execute` 메서드는 OCC(낙관적 동시성 제어) 충돌 및 기타 일반적인 일시적 예외를 이 구성 가능한 한도까지 자동으로 재시도합니다. 기본값은 `4`입니다.  
한도에 도달한 후에도 트랜잭션이 여전히 실패하면 드라이버에서 예외가 발생합니다. 자세한 내용은 [Amazon QLDB의 드라이버를 사용한 재시도 정책에 대한 이해](driver-retry-policy.md)을 참조하십시오.

------

1. `ConnectToLedger.java` 프로그램을 컴파일하고 실행하여 `vehicle-registration` 원장에 대한 데이터 세션 연결을 테스트하세요.

**재정의 AWS 리전**

샘플 애플리케이션은 기본값의 QLDB에 연결되며 AWS 리전, 사전 조건 단계에 설명된 대로 설정할 수 있습니다[기본 AWS 보안 인증 정보 및 리전 설정](getting-started.java.md#getting-started.java.credentials). 또한 QLDB 세션 클라이언트 빌더 속성을 수정하여 리전을 변경할 수 있습니다.

------
#### [ 2.x ]

다음 코드 예제에서는 새 `QldbSessionClientBuilder` 객체를 인스턴스화합니다.

```
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.qldbsession.QldbSessionClientBuilder;

// This client builder will default to US East (Ohio)
QldbSessionClientBuilder builder = QldbSessionClient.builder()
    .region(Region.US_EAST_2);
```

`region` 메서드를 사용하면 사용할 수 있는 어떤 리전의 QLDB에 대해서도 코드를 실행할 수 있습니다. 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

------
#### [ 1.x ]

다음 코드 예제에서는 새 `AmazonQLDBSessionClientBuilder` 객체를 인스턴스화합니다.

```
import com.amazonaws.regions.Regions;
import com.amazonaws.services.qldbsession.AmazonQLDBSessionClientBuilder;

// This client builder will default to US East (Ohio)
AmazonQLDBSessionClientBuilder builder = AmazonQLDBSessionClientBuilder.standard()
    .withRegion(Regions.US_EAST_2);
```

`withRegion` 메서드를 사용하면 사용할 수 있는 어떤 리전의 QLDB에 대해서도 코드를 실행할 수 있습니다. 전체 목록은 *AWS 일반 참조*에서 [Amazon QLDB 엔드포인트 및 할당량](https://docs.aws.amazon.com/general/latest/gr/qldb.html)을 참조하세요.

------

`vehicle-registration` 원장에 테이블을 생성하려면 [3단계: 테이블, 인덱스 및 샘플 데이터 생성](getting-started.java.step-3.md)로 진행하세요.

# 3단계: 테이블, 인덱스 및 샘플 데이터 생성
<a name="getting-started.java.step-3"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 원장이 활성화되고 연결을 수락하면 차량, 소유자 및 등록 정보에 대한 데이터 테이블 생성을 시작할 수 있습니다. 테이블과 인덱스를 생성한 후 데이터를 로드할 수 있습니다.

이 단계에서는 `vehicle-registration` 원장에 4개의 테이블을 생성합니다.
+ `VehicleRegistration`
+ `Vehicle`
+ `Person`
+ `DriversLicense`

또한 다음과 같은 인덱스를 생성합니다.


****  

| 테이블 이름 | 필드 | 
| --- | --- | 
| VehicleRegistration | VIN | 
| VehicleRegistration | LicensePlateNumber | 
| Vehicle | VIN | 
| Person | GovId | 
| DriversLicense | LicenseNumber | 
| DriversLicense | PersonId | 

샘플 데이터를 삽입할 때는 먼저 `Person` 테이블에 문서를 삽입합니다. 그런 다음 각 `Person` 문서에서 시스템이 할당한 `id`를 사용하여 적절한 `VehicleRegistration` 및 `DriversLicense` 문서의 해당 필드를 채웁니다.

**작은 정보**  
가장 좋은 방법은 문서의 시스템 할당 `id`를 외래 키로 사용하는 것입니다. 고유 식별자(예: 차량의 VIN)로 사용되는 필드를 정의할 수 있지만 문서의 실제  고유 식별자는 `id`입니다. 이 필드는 문서의 메타데이터에 포함되며 *커밋된 뷰*(테이블의 시스텀 정의 뷰)에서 쿼리할 수 있습니다.  
QLDB 뷰에 대한 자세한 내용은 [핵심 개념](ledger-structure.md) 섹션을 참조하세요. 메타데이터에 대해 자세히 알아보려면 [문서 메타데이터 쿼리](working.metadata.md) 섹션을 참조하세요.

**샘플 데이터를 설정하려면**

1. 다음 `.java` 파일을 검토합니다. 이러한 모델 클래스는 `vehicle-registration` 테이블에 저장하는 문서를 나타냅니다. Amazon Ion 형식으로 또는 Amazon Ion 형식에서 직렬화할 수 있습니다.
**참고**  
[Amazon QLDB 문서](ql-reference.docs.md)는 JSON의 상위 집합인 Ion 형식으로 저장됩니다. 따라서 FasterXML Jackson 라이브러리를 사용하여 데이터를 JSON으로 모델링할 수 있습니다.

   1. `DriversLicense.java`

      ```
      /*
       * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      import java.time.LocalDate;
      
      /**
       * Represents a driver's license, serializable to (and from) Ion.
       */
      public final class DriversLicense implements RevisionData {
          private final String personId;
          private final String licenseNumber;
          private final String licenseType;
      
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          private final LocalDate validFromDate;
      
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          private final LocalDate validToDate;
      
          @JsonCreator
          public DriversLicense(@JsonProperty("PersonId") final String personId,
                                @JsonProperty("LicenseNumber") final String licenseNumber,
                                @JsonProperty("LicenseType") final String licenseType,
                                @JsonProperty("ValidFromDate") final LocalDate validFromDate,
                                @JsonProperty("ValidToDate") final LocalDate validToDate) {
              this.personId = personId;
              this.licenseNumber = licenseNumber;
              this.licenseType = licenseType;
              this.validFromDate = validFromDate;
              this.validToDate = validToDate;
          }
      
          @JsonProperty("PersonId")
          public String getPersonId() {
              return personId;
          }
      
          @JsonProperty("LicenseNumber")
          public String getLicenseNumber() {
              return licenseNumber;
          }
      
          @JsonProperty("LicenseType")
          public String getLicenseType() {
              return licenseType;
          }
      
          @JsonProperty("ValidFromDate")
          public LocalDate getValidFromDate() {
              return  validFromDate;
          }
      
          @JsonProperty("ValidToDate")
          public LocalDate getValidToDate() {
              return  validToDate;
          }
      
          @Override
          public String toString() {
              return "DriversLicense{" +
                      "personId='" + personId + '\'' +
                      ", licenseNumber='" + licenseNumber + '\'' +
                      ", licenseType='" + licenseType + '\'' +
                      ", validFromDate=" + validFromDate +
                      ", validToDate=" + validToDate +
                      '}';
          }
      }
      ```

   1. `Person.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import java.time.LocalDate;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      
      import software.amazon.qldb.TransactionExecutor;
      import software.amazon.qldb.tutorial.Constants;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      /**
       * Represents a person, serializable to (and from) Ion.
       */ 
      public final class Person implements RevisionData {
          private final String firstName;
          private final String lastName;
      
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          private final LocalDate dob;
          private final String govId;
          private final String govIdType;
          private final String address;
      
          @JsonCreator
          public Person(@JsonProperty("FirstName") final String firstName,
                        @JsonProperty("LastName") final String lastName,
                        @JsonProperty("DOB") final LocalDate dob,
                        @JsonProperty("GovId") final String govId,
                        @JsonProperty("GovIdType") final String govIdType,
                        @JsonProperty("Address") final String address) {
              this.firstName = firstName;
              this.lastName = lastName;
              this.dob = dob;
              this.govId = govId;
              this.govIdType = govIdType;
              this.address = address;
          }
      
          @JsonProperty("Address")
          public String getAddress() {
              return address;
          }
      
          @JsonProperty("DOB")
          public LocalDate getDob() {
              return dob;
          }
      
          @JsonProperty("FirstName")
          public String getFirstName() {
              return firstName;
          }
      
          @JsonProperty("LastName")
          public String getLastName() {
              return lastName;
          }
      
          @JsonProperty("GovId")
          public String getGovId() {
              return govId;
          }
      
          @JsonProperty("GovIdType")
          public String getGovIdType() {
              return govIdType;
          }
      
          /**
           * This returns the unique document ID given a specific government ID.
           *
           * @param txn
           *              A transaction executor object.
           * @param govId
           *              The government ID of a driver.
           * @return the unique document ID.
           */
          public static String getDocumentIdByGovId(final TransactionExecutor txn, final String govId) {
              return SampleData.getDocumentId(txn, Constants.PERSON_TABLE_NAME, "GovId", govId);
          }
      
          @Override
          public String toString() {
              return "Person{" +
                      "firstName='" + firstName + '\'' +
                      ", lastName='" + lastName + '\'' +
                      ", dob=" + dob +
                      ", govId='" + govId + '\'' +
                      ", govIdType='" + govIdType + '\'' +
                      ", address='" + address + '\'' +
                      '}';
          }
      }
      ```

   1. `VehicleRegistration.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      import software.amazon.qldb.TransactionExecutor;
      import software.amazon.qldb.tutorial.Constants;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      import java.math.BigDecimal;
      import java.time.LocalDate;
      
      /**
       * Represents a vehicle registration, serializable to (and from) Ion.
       */ 
      public final class VehicleRegistration implements RevisionData {
      
          private final String vin;
          private final String licensePlateNumber;
          private final String state;
          private final String city;
          private final BigDecimal pendingPenaltyTicketAmount;
          private final LocalDate validFromDate;
          private final LocalDate validToDate;
          private final Owners owners;
      
          @JsonCreator
          public VehicleRegistration(@JsonProperty("VIN") final String vin,
                                     @JsonProperty("LicensePlateNumber") final String licensePlateNumber,
                                     @JsonProperty("State") final String state,
                                     @JsonProperty("City") final String city,
                                     @JsonProperty("PendingPenaltyTicketAmount") final BigDecimal pendingPenaltyTicketAmount,
                                     @JsonProperty("ValidFromDate") final LocalDate validFromDate,
                                     @JsonProperty("ValidToDate") final LocalDate validToDate,
                                     @JsonProperty("Owners") final Owners owners) {
              this.vin = vin;
              this.licensePlateNumber = licensePlateNumber;
              this.state = state;
              this.city = city;
              this.pendingPenaltyTicketAmount = pendingPenaltyTicketAmount;
              this.validFromDate = validFromDate;
              this.validToDate = validToDate;
              this.owners = owners;
          }
      
          @JsonProperty("City")
          public String getCity() {
              return city;
          }
      
          @JsonProperty("LicensePlateNumber")
          public String getLicensePlateNumber() {
              return licensePlateNumber;
          }
      
          @JsonProperty("Owners")
          public Owners getOwners() {
              return owners;
          }
      
          @JsonProperty("PendingPenaltyTicketAmount")
          public BigDecimal getPendingPenaltyTicketAmount() {
              return pendingPenaltyTicketAmount;
          }
      
          @JsonProperty("State")
          public String getState() {
              return state;
          }
      
          @JsonProperty("ValidFromDate")
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          public LocalDate getValidFromDate() {
              return validFromDate;
          }
      
          @JsonProperty("ValidToDate")
          @JsonSerialize(using = IonLocalDateSerializer.class)
          @JsonDeserialize(using = IonLocalDateDeserializer.class)
          public LocalDate getValidToDate() {
              return validToDate;
          }
      
          @JsonProperty("VIN")
          public String getVin() {
              return vin;
          }
      
          /**
           * Returns the unique document ID of a vehicle given a specific VIN.
           *
           * @param txn
           *              A transaction executor object.
           * @param vin
           *              The VIN of a vehicle.
           * @return the unique document ID of the specified vehicle.
           */
          public static String getDocumentIdByVin(final TransactionExecutor txn, final String vin) {
              return SampleData.getDocumentId(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME, "VIN", vin);
          }
      
          @Override
          public String toString() {
              return "VehicleRegistration{" +
                      "vin='" + vin + '\'' +
                      ", licensePlateNumber='" + licensePlateNumber + '\'' +
                      ", state='" + state + '\'' +
                      ", city='" + city + '\'' +
                      ", pendingPenaltyTicketAmount=" + pendingPenaltyTicketAmount +
                      ", validFromDate=" + validFromDate +
                      ", validToDate=" + validToDate +
                      ", owners=" + owners +
                      '}';
          }
      }
      ```

   1. `Vehicle.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import software.amazon.qldb.tutorial.model.streams.RevisionData;
      
      /**
       * Represents a vehicle, serializable to (and from) Ion.
       */
      public final class Vehicle implements RevisionData {
          private final String vin;
          private final String type;
          private final int year;
          private final String make;
          private final String model;
          private final String color;
      
          @JsonCreator
          public Vehicle(@JsonProperty("VIN") final String vin,
                         @JsonProperty("Type") final String type,
                         @JsonProperty("Year") final int year,
                         @JsonProperty("Make") final String make,
                         @JsonProperty("Model") final String model,
                         @JsonProperty("Color") final String color) {
              this.vin = vin;
              this.type = type;
              this.year = year;
              this.make = make;
              this.model = model;
              this.color = color;
          }
      
          @JsonProperty("Color")
          public String getColor() {
              return color;
          }
      
          @JsonProperty("Make")
          public String getMake() {
              return make;
          }
      
          @JsonProperty("Model")
          public String getModel() {
              return model;
          }
      
          @JsonProperty("Type")
          public String getType() {
              return type;
          }
      
          @JsonProperty("VIN")
          public String getVin() {
              return vin;
          }
      
          @JsonProperty("Year")
          public int getYear() {
              return year;
          }
      
          @Override
          public String toString() {
              return "Vehicle{" +
                      "vin='" + vin + '\'' +
                      ", type='" + type + '\'' +
                      ", year=" + year +
                      ", make='" + make + '\'' +
                      ", model='" + model + '\'' +
                      ", color='" + color + '\'' +
                      '}';
          }
      }
      ```

   1. `Owner.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      /**
       * Represents a vehicle owner, serializable to (and from) Ion.
       */ 
      public final class Owner {
          private final String personId;
      
          public Owner(@JsonProperty("PersonId") final String personId) {
              this.personId = personId;
          }
      
          @JsonProperty("PersonId")
          public String getPersonId() {
              return personId;
          }
      
          @Override
          public String toString() {
              return "Owner{" +
                      "personId='" + personId + '\'' +
                      '}';
          }
      }
      ```

   1. `Owners.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      import java.util.List;
      
      /**
       * Represents a set of owners for a given vehicle, serializable to (and from) Ion.
       */ 
      public final class Owners {
          private final Owner primaryOwner;
          private final List<Owner> secondaryOwners;
      
          public Owners(@JsonProperty("PrimaryOwner") final Owner primaryOwner,
                        @JsonProperty("SecondaryOwners") final List<Owner> secondaryOwners) {
              this.primaryOwner = primaryOwner;
              this.secondaryOwners = secondaryOwners;
          }
      
          @JsonProperty("PrimaryOwner")
          public Owner getPrimaryOwner() {
              return primaryOwner;
          }
      
          @JsonProperty("SecondaryOwners")
          public List<Owner> getSecondaryOwners() {
              return secondaryOwners;
          }
      
          @Override
          public String toString() {
              return "Owners{" +
                      "primaryOwner=" + primaryOwner +
                      ", secondaryOwners=" + secondaryOwners +
                      '}';
          }
      }
      ```

   1. `DmlResultDocument.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      /**
       * Contains information about an individual document inserted or modified
       * as a result of DML.
       */
      public class DmlResultDocument {
      
          private String documentId;
      
          @JsonCreator
          public DmlResultDocument(@JsonProperty("documentId") final String documentId) {
              this.documentId = documentId;
          }
      
          public String getDocumentId() {
              return documentId;
          }
      
          @Override
          public String toString() {
              return "DmlResultDocument{"
                  + "documentId='" + documentId + '\''
                  + '}';
          }
      }
      ```

   1. `RevisionData.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model.streams;
      
      /**
       * Allows modeling the content of all revisions as a generic revision data. Used
       * in the {@link Revision} and extended by domain models in {@link
       * software.amazon.qldb.tutorial.model} to make it easier to write the {@link
       * Revision.RevisionDataDeserializer} that must deserialize the {@link
       * Revision#data} from different domain models.
       */
      public interface RevisionData { }
      ```

   1. `RevisionMetadata.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonInt;
      import com.amazon.ion.IonString;
      import com.amazon.ion.IonStruct;
      import com.amazon.ion.IonTimestamp;
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import com.fasterxml.jackson.databind.annotation.JsonSerialize;
      import com.fasterxml.jackson.dataformat.ion.IonTimestampSerializers;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import java.util.Date;
      import java.util.Objects;
      
      /**
       * Represents the metadata field of a QLDB Document
       */
      public class RevisionMetadata {
          private static final Logger log = LoggerFactory.getLogger(RevisionMetadata.class);
          private final String id;
          private final long version;
          @JsonSerialize(using = IonTimestampSerializers.IonTimestampJavaDateSerializer.class)
          private final Date txTime;
          private final String txId;
      
          @JsonCreator
          public RevisionMetadata(@JsonProperty("id") final String id,
                                  @JsonProperty("version") final long version,
                                  @JsonProperty("txTime") final Date txTime,
                                  @JsonProperty("txId") final String txId) {
              this.id = id;
              this.version = version;
              this.txTime = txTime;
              this.txId = txId;
          }
      
          /**
           * Gets the unique ID of a QLDB document.
           *
           * @return the document ID.
           */
          public String getId() {
              return id;
          }
      
          /**
           * Gets the version number of the document in the document's modification history.
           * @return the version number.
           */
          public long getVersion() {
              return version;
          }
      
          /**
           * Gets the time during which the document was modified.
           *
           * @return the transaction time.
           */
          public Date getTxTime() {
              return txTime;
          }
      
          /**
           * Gets the transaction ID associated with this document.
           *
           * @return the transaction ID.
           */
          public String getTxId() {
              return txId;
          }
      
          public static RevisionMetadata fromIon(final IonStruct ionStruct) {
              if (ionStruct == null) {
                  throw new IllegalArgumentException("Metadata cannot be null");
              }
              try {
                  IonString id = (IonString) ionStruct.get("id");
                  IonInt version = (IonInt) ionStruct.get("version");
                  IonTimestamp txTime = (IonTimestamp) ionStruct.get("txTime");
                  IonString txId = (IonString) ionStruct.get("txId");
                  if (id == null || version == null || txTime == null || txId == null) {
                      throw new IllegalArgumentException("Document is missing required fields");
                  }
                  return new RevisionMetadata(id.stringValue(), version.longValue(), new Date(txTime.getMillis()), txId.stringValue());
              } catch (ClassCastException e) {
                  log.error("Failed to parse ion document");
                  throw new IllegalArgumentException("Document members are not of the correct type", e);
              }
          }
      
          /**
           * Converts a {@link RevisionMetadata} object to a string.
           *
           * @return the string representation of the {@link QldbRevision} object.
           */
          @Override
          public String toString() {
              return "Metadata{"
                      + "id='" + id + '\''
                      + ", version=" + version
                      + ", txTime=" + txTime
                      + ", txId='" + txId
                      + '\''
                      + '}';
          }
      
          /**
           * Check whether two {@link RevisionMetadata} objects are equivalent.
           *
           * @return {@code true} if the two objects are equal, {@code false} otherwise.
           */
          @Override
          public boolean equals(Object o) {
              if (this == o) { return true; }
              if (o == null || getClass() != o.getClass()) { return false; }
              RevisionMetadata metadata = (RevisionMetadata) o;
              return version == metadata.version
                      && id.equals(metadata.id)
                      && txTime.equals(metadata.txTime)
                      && txId.equals(metadata.txId);
          }
      
          /**
           * Generate a hash code for the {@link RevisionMetadata} object.
           *
           * @return the hash code.
           */
          @Override
          public int hashCode() {
              // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties.
              return Objects.hash(id, version, txTime, txId);
              // CHECKSTYLE:ON
          }
      }
      ```

   1. `QldbRevision.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonBlob;
      import com.amazon.ion.IonStruct;
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import software.amazon.qldb.tutorial.Constants;
      import software.amazon.qldb.tutorial.Verifier;
      
      import java.io.IOException;
      import java.util.Arrays;
      import java.util.Objects;
      
      /**
       * Represents a QldbRevision including both user data and metadata.
       */
      public final class QldbRevision {
          private static final Logger log = LoggerFactory.getLogger(QldbRevision.class);
      
          private final BlockAddress blockAddress;
          private final RevisionMetadata metadata;
          private final byte[] hash;
          private final byte[] dataHash;
          private final IonStruct data;
      
          @JsonCreator
          public QldbRevision(@JsonProperty("blockAddress") final BlockAddress blockAddress,
                              @JsonProperty("metadata") final RevisionMetadata metadata,
                              @JsonProperty("hash") final byte[] hash,
                              @JsonProperty("dataHash") final byte[] dataHash,
                              @JsonProperty("data") final IonStruct data) {
              this.blockAddress = blockAddress;
              this.metadata = metadata;
              this.hash = hash;
              this.dataHash = dataHash;
              this.data = data;
          }
      
          /**
           * Gets the unique ID of a QLDB document.
           *
           * @return the {@link BlockAddress} object.
           */
          public BlockAddress getBlockAddress() {
              return blockAddress;
          }
      
          /**
           * Gets the metadata of the revision.
           *
           * @return the {@link RevisionMetadata} object.
           */
          public RevisionMetadata getMetadata() {
              return metadata;
          }
      
          /**
           * Gets the SHA-256 hash value of the revision.
           * This is equivalent to the hash of the revision metadata and data.
           *
           * @return the byte array representing the hash.
           */
          public byte[] getHash() {
              return hash;
          }
      
          /**
           * Gets the SHA-256 hash value of the data portion of the revision.
           * This is only present if the revision is redacted.
           *
           * @return the byte array representing the hash.
           */
          public byte[] getDataHash() {
              return dataHash;
          }
      
          /**
           * Gets the revision data.
           *
           * @return the revision data.
           */
          public IonStruct getData() {
              return data;
          }
      
          /**
           * Returns true if the revision has been redacted.
           * @return a boolean value representing the redaction status
           * of this revision.
           */
          public Boolean isRedacted() {
              return dataHash != null;
          }
      
          /**
           * Constructs a new {@link QldbRevision} from an {@link IonStruct}.
           *
           * The specified {@link IonStruct} must include the following fields
           *
           * - blockAddress -- a {@link BlockAddress},
           * - metadata -- a {@link RevisionMetadata},
           * - hash -- the revision's hash calculated by QLDB,
           * - dataHash -- the user data's hash calculated by QLDB (only present if revision is redacted),
           * - data -- an {@link IonStruct} containing user data in the document.
           *
           * If any of these fields are missing or are malformed, then throws {@link IllegalArgumentException}.
           *
           * If the document hash calculated from the members of the specified {@link IonStruct} does not match
           * the hash member of the {@link IonStruct} then throws {@link IllegalArgumentException}.
           *
           * @param ionStruct
           *              The {@link IonStruct} that contains a {@link QldbRevision} object.
           * @return the converted {@link QldbRevision} object.
           * @throws IOException if failed to parse parameter {@link IonStruct}.
           */
          public static QldbRevision fromIon(final IonStruct ionStruct) throws IOException {
              try {
                  BlockAddress blockAddress = Constants.MAPPER.readValue(ionStruct.get("blockAddress"), BlockAddress.class);
                  IonBlob revisionHash = (IonBlob) ionStruct.get("hash");
                  IonStruct metadataStruct = (IonStruct) ionStruct.get("metadata");
                  IonStruct data = ionStruct.get("data") == null || ionStruct.get("data").isNullValue() ?
                      null : (IonStruct) ionStruct.get("data");
                  IonBlob dataHash = ionStruct.get("dataHash") == null || ionStruct.get("dataHash").isNullValue() ?
                      null : (IonBlob) ionStruct.get("dataHash");
                  if (revisionHash == null || metadataStruct == null) {
                      throw new IllegalArgumentException("Document is missing required fields");
                  }
                  byte[] dataHashBytes = dataHash != null ? dataHash.getBytes() : QldbIonUtils.hashIonValue(data);
                  verifyRevisionHash(metadataStruct, dataHashBytes, revisionHash.getBytes());
                  RevisionMetadata metadata = RevisionMetadata.fromIon(metadataStruct);
                  return new QldbRevision(
                          blockAddress,
                          metadata,
                          revisionHash.getBytes(),
                          dataHash != null ? dataHash.getBytes() : null,
                          data
                  );
              } catch (ClassCastException e) {
                  log.error("Failed to parse ion document");
                  throw new IllegalArgumentException("Document members are not of the correct type", e);
              }
          }
      
          /**
           * Converts a {@link QldbRevision} object to string.
           *
           * @return the string representation of the {@link QldbRevision} object.
           */
          @Override
          public String toString() {
              return "QldbRevision{" +
                      "blockAddress=" + blockAddress +
                      ", metadata=" + metadata +
                      ", hash=" + Arrays.toString(hash) +
                      ", dataHash=" + Arrays.toString(dataHash) +
                      ", data=" + data +
                      '}';
          }
      
          /**
           * Check whether two {@link QldbRevision} objects are equivalent.
           *
           * @return {@code true} if the two objects are equal, {@code false} otherwise.
           */
          @Override
          public boolean equals(final Object o) {
              if (this == o) {
                  return true;
              }
              if (!(o instanceof QldbRevision)) {
                  return false;
              }
              final QldbRevision that = (QldbRevision) o;
              return Objects.equals(getBlockAddress(), that.getBlockAddress())
                      && Objects.equals(getMetadata(), that.getMetadata())
                      && Arrays.equals(getHash(), that.getHash())
                      && Arrays.equals(getDataHash(), that.getDataHash())
                      && Objects.equals(getData(), that.getData());
          }
      
          /**
           * Create a hash code for the {@link QldbRevision} object.
           *
           * @return the hash code.
           */
          @Override
          public int hashCode() {
              // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties.
              int result = Objects.hash(blockAddress, metadata, data);
              // CHECKSTYLE:ON
              result = 31 * result + Arrays.hashCode(hash);
              return result;
          }
      
          /**
           * Throws an IllegalArgumentException if the hash of the revision data and metadata
           * does not match the hash provided by QLDB with the revision.
           */
          public void verifyRevisionHash() {
              // Certain internal-only system revisions only contain a hash which cannot be
              // further computed. However, these system hashes still participate to validate
              // the journal block. User revisions will always contain values for all fields
              // and can therefore have their hash computed.
              if (blockAddress == null && metadata == null && data == null && dataHash == null) {
                  return;
              }
      
              try {
                  IonStruct metadataIon = (IonStruct) Constants.MAPPER.writeValueAsIonValue(metadata);
                  byte[] dataHashBytes = isRedacted() ? dataHash : QldbIonUtils.hashIonValue(data);
                  verifyRevisionHash(metadataIon, dataHashBytes, hash);
              } catch (IOException e) {
                  throw new IllegalArgumentException("Could not encode revision metadata to ion.", e);
              }
          }
      
          private static void verifyRevisionHash(IonStruct metadata, byte[] dataHash, byte[] expectedHash) {
              byte[] metadataHash = QldbIonUtils.hashIonValue(metadata);
              byte[] candidateHash = Verifier.dot(metadataHash, dataHash);
              if (!Arrays.equals(candidateHash, expectedHash)) {
                  throw new IllegalArgumentException("Hash entry of QLDB revision and computed hash "
                          + "of QLDB revision do not match");
              }
          }
      }
      ```

   1. `IonLocalDateDeserializer.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.amazon.ion.Timestamp;
      import com.fasterxml.jackson.core.JsonParser;
      import com.fasterxml.jackson.databind.DeserializationContext;
      import com.fasterxml.jackson.databind.JsonDeserializer;
      
      import java.io.IOException;
      import java.time.LocalDate;
      
      /**
       * Deserializes [java.time.LocalDate] from Ion.
       */
      public class IonLocalDateDeserializer extends JsonDeserializer<LocalDate> {
      
          @Override
          public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
              return timestampToLocalDate((Timestamp) jp.getEmbeddedObject());
          }
      
          private LocalDate timestampToLocalDate(Timestamp timestamp) {
              return LocalDate.of(timestamp.getYear(), timestamp.getMonth(), timestamp.getDay());
          }
      }
      ```

   1. `IonLocalDateSerializer.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.model;
      
      import com.amazon.ion.Timestamp;
      import com.fasterxml.jackson.core.JsonGenerator;
      import com.fasterxml.jackson.databind.SerializerProvider;
      import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
      import com.fasterxml.jackson.dataformat.ion.IonGenerator;
      
      import java.io.IOException;
      import java.time.LocalDate;
      
      /**
       * Serializes [java.time.LocalDate] to Ion.
       */
      public class IonLocalDateSerializer extends StdScalarSerializer<LocalDate> {
      
          public IonLocalDateSerializer() {
              super(LocalDate.class);
          }
      
          @Override
          public void serialize(LocalDate date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
              Timestamp timestamp = Timestamp.forDay(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
              ((IonGenerator) jsonGenerator).writeValue(timestamp);
          }
      }
      ```

1. `vehicle-registration` 테이블에 삽입한 샘플 데이터를 나타내는 다음 파일(`SampleData.java`)을 검토하세요.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial.model;
   
   import com.amazon.ion.IonString;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import java.io.IOException;
   import java.math.BigDecimal;
   import java.text.ParseException;
   import java.time.LocalDate;
   import java.time.format.DateTimeFormatter;
   import java.util.ArrayList;
   import java.util.Arrays;
   import java.util.Collections;
   import java.util.List;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.ConnectToLedger;
   import software.amazon.qldb.tutorial.Constants;
   import software.amazon.qldb.tutorial.qldb.DmlResultDocument;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   
   /**
    * Sample domain objects for use throughout this tutorial.
    */
   public final class SampleData {
       public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
   
       public static final List<VehicleRegistration> REGISTRATIONS = Collections.unmodifiableList(Arrays.asList(
               new VehicleRegistration("1N4AL11D75C109151", "LEWISR261LL", "WA", "Seattle",
                       BigDecimal.valueOf(90.25), convertToLocalDate("2017-08-21"), convertToLocalDate("2020-05-11"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("KM8SRDHF6EU074761", "CA762X", "WA", "Kent",
                       BigDecimal.valueOf(130.75), convertToLocalDate("2017-09-14"), convertToLocalDate("2020-06-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("3HGGK5G53FM761765", "CD820Z", "WA", "Everett",
                       BigDecimal.valueOf(442.30), convertToLocalDate("2011-03-17"), convertToLocalDate("2021-03-24"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1HVBBAANXWH544237", "LS477D", "WA", "Tacoma",
                       BigDecimal.valueOf(42.20), convertToLocalDate("2011-10-26"), convertToLocalDate("2023-09-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1C4RJFAG0FC625797", "TH393F", "WA", "Olympia",
                       BigDecimal.valueOf(30.45), convertToLocalDate("2013-09-02"), convertToLocalDate("2024-03-19"),
                       new Owners(new Owner(null), Collections.emptyList()))
       ));
   
       public static final List<Vehicle> VEHICLES = Collections.unmodifiableList(Arrays.asList(
               new Vehicle("1N4AL11D75C109151", "Sedan", 2011,  "Audi", "A5", "Silver"),
               new Vehicle("KM8SRDHF6EU074761", "Sedan", 2015, "Tesla", "Model S", "Blue"),
               new Vehicle("3HGGK5G53FM761765", "Motorcycle", 2011, "Ducati", "Monster 1200", "Yellow"),
               new Vehicle("1HVBBAANXWH544237", "Semi", 2009, "Ford", "F 150", "Black"),
               new Vehicle("1C4RJFAG0FC625797", "Sedan", 2019, "Mercedes", "CLK 350", "White")
       ));
   
       public static final List<Person> PEOPLE = Collections.unmodifiableList(Arrays.asList(
               new Person("Raul", "Lewis", convertToLocalDate("1963-08-19"),
                       "LEWISR261LL", "Driver License",  "1719 University Street, Seattle, WA, 98109"),
               new Person("Brent", "Logan", convertToLocalDate("1967-07-03"),
                       "LOGANB486CG", "Driver License", "43 Stockert Hollow Road, Everett, WA, 98203"),
               new Person("Alexis", "Pena", convertToLocalDate("1974-02-10"),
                       "744 849 301", "SSN", "4058 Melrose Street, Spokane Valley, WA, 99206"),
               new Person("Melvin", "Parker", convertToLocalDate("1976-05-22"),
                       "P626-168-229-765", "Passport", "4362 Ryder Avenue, Seattle, WA, 98101"),
               new Person("Salvatore", "Spencer", convertToLocalDate("1997-11-15"),
                       "S152-780-97-415-0", "Passport", "4450 Honeysuckle Lane, Seattle, WA, 98101")
       ));
   
       public static final List<DriversLicense> LICENSES = Collections.unmodifiableList(Arrays.asList(
               new DriversLicense(null, "LEWISR261LL", "Learner",
                       convertToLocalDate("2016-12-20"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "LOGANB486CG", "Probationary",
                       convertToLocalDate("2016-04-06"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "744 849 301", "Full",
                       convertToLocalDate("2017-12-06"), convertToLocalDate("2022-10-15")),
               new DriversLicense(null, "P626-168-229-765", "Learner",
                       convertToLocalDate("2017-08-16"), convertToLocalDate("2021-11-15")),
               new DriversLicense(null, "S152-780-97-415-0", "Probationary",
                       convertToLocalDate("2015-08-15"), convertToLocalDate("2021-08-21"))
       ));
   
       private SampleData() { }
   
       /**
        * Converts a date string with the format 'yyyy-MM-dd' into a {@link java.util.Date} object.
        *
        * @param date
        *              The date string to convert.
        * @return {@link java.time.LocalDate} or null if there is a {@link ParseException}
        */
       public static synchronized LocalDate convertToLocalDate(String date) {
           return LocalDate.parse(date, DATE_TIME_FORMAT);
       }
   
       /**
        * Convert the result set into a list of IonValues.
        *
        * @param result
        *              The result set to convert.
        * @return a list of IonValues.
        */
       public static List<IonValue> toIonValues(Result result) {
           final List<IonValue> valueList = new ArrayList<>();
           result.iterator().forEachRemaining(valueList::add);
           return valueList;
       }
   
       /**
        * Get the document ID of a particular document.
        *
        * @param txn
        *              A transaction executor object.
        * @param tableName
        *              Name of the table containing the document.
        * @param identifier
        *              The identifier used to narrow down the search.
        * @param value
        *              Value of the identifier.
        * @return the list of document IDs in the result set.
        */
       public static String getDocumentId(final TransactionExecutor txn, final String tableName,
                                          final String identifier, final String value) {
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(value));
               final String query = String.format("SELECT metadata.id FROM _ql_committed_%s AS p WHERE p.data.%s = ?",
                       tableName, identifier);
               Result result = txn.execute(query, parameters);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document ID using " + value);
               }
               return getStringValueOfStructField((IonStruct) result.iterator().next(), "id");
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the document by ID.
        *
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link QldbRevision} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static QldbRevision getDocumentById(String tableName, String documentId) {
           try {
               final IonValue ionValue = Constants.MAPPER.writeValueAsIonValue(documentId);
               Result result = ConnectToLedger.getDriver().execute(txn -> {
                   return txn.execute("SELECT c.* FROM _ql_committed_" + tableName + " AS c BY docId "
                                      + "WHERE docId = ?", ionValue);
               });
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document by id " + documentId + " in table " + tableName);
               }
               return Constants.MAPPER.readValue(result.iterator().next(), QldbRevision.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Return a list of modified document IDs as strings from a DML {@link Result}.
        *
        * @param result
        *              The result set from a DML operation.
        * @return the list of document IDs modified by the operation.
        */
       public static List<String> getDocumentIdsFromDmlResult(final Result result) {
           final List<String> strings = new ArrayList<>();
           result.iterator().forEachRemaining(row -> strings.add(getDocumentIdFromDmlResultDocument(row)));
           return strings;
       }
   
       /**
        * Convert the given DML result row's document ID to string.
        *
        * @param dmlResultDocument
        *              The {@link IonValue} representing the results of a DML operation.
        * @return a string of document ID.
        */
       public static String getDocumentIdFromDmlResultDocument(final IonValue dmlResultDocument) {
           try {
               DmlResultDocument result = Constants.MAPPER.readValue(dmlResultDocument, DmlResultDocument.class);
               return result.getDocumentId();
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the String value of a given {@link IonStruct} field name.
        * @param struct the {@link IonStruct} from which to get the value.
        * @param fieldName the name of the field from which to get the value.
        * @return the String value of the field within the given {@link IonStruct}.
        */
       public static String getStringValueOfStructField(final IonStruct struct, final String fieldName) {
           return ((IonString) struct.get(fieldName)).stringValue();
       }
   
       /**
        * Return a copy of the given driver's license with updated person Id.
        *
        * @param oldLicense
        *              The old driver's license to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link DriversLicense}.
        */
       public static DriversLicense updatePersonIdDriversLicense(final DriversLicense oldLicense, final String personId) {
           return new DriversLicense(personId, oldLicense.getLicenseNumber(), oldLicense.getLicenseType(),
                   oldLicense.getValidFromDate(), oldLicense.getValidToDate());
       }
   
       /**
        * Return a copy of the given vehicle registration with updated person Id.
        *
        * @param oldRegistration
        *              The old vehicle registration to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link VehicleRegistration}.
        */
       public static VehicleRegistration updateOwnerVehicleRegistration(final VehicleRegistration oldRegistration,
                                                                        final String personId) {
           return new VehicleRegistration(oldRegistration.getVin(), oldRegistration.getLicensePlateNumber(),
                   oldRegistration.getState(), oldRegistration.getCity(), oldRegistration.getPendingPenaltyTicketAmount(),
                   oldRegistration.getValidFromDate(), oldRegistration.getValidToDate(),
                   new Owners(new Owner(personId), Collections.emptyList()));
       }
   }
   ```

------
#### [ 1.x ]

**중요**  
Amazon Ion 패키지의 경우 애플리케이션에서 네임스페이스 `com.amazon.ion`을 사용해야 합니다. 는 네임스페이스 아래의 다른 Ion 패키지에 AWS SDK for Java 종속되지만 `software.amazon.ion`QLDB 드라이버와 호환되지 않는 레거시 패키지입니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial.model;
   
   import com.amazon.ion.IonString;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.Constants;
   import software.amazon.qldb.tutorial.qldb.DmlResultDocument;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   
   import java.io.IOException;
   
   import java.math.BigDecimal;
   import java.text.ParseException;
   import java.time.LocalDate;
   import java.time.format.DateTimeFormatter;
   import java.util.ArrayList;
   import java.util.Arrays;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Sample domain objects for use throughout this tutorial.
    */
   public final class SampleData {
       public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
   
       public static final List<VehicleRegistration> REGISTRATIONS = Collections.unmodifiableList(Arrays.asList(
               new VehicleRegistration("1N4AL11D75C109151", "LEWISR261LL", "WA", "Seattle",
                       BigDecimal.valueOf(90.25), convertToLocalDate("2017-08-21"), convertToLocalDate("2020-05-11"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("KM8SRDHF6EU074761", "CA762X", "WA", "Kent",
                       BigDecimal.valueOf(130.75), convertToLocalDate("2017-09-14"), convertToLocalDate("2020-06-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("3HGGK5G53FM761765", "CD820Z", "WA", "Everett",
                       BigDecimal.valueOf(442.30), convertToLocalDate("2011-03-17"), convertToLocalDate("2021-03-24"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1HVBBAANXWH544237", "LS477D", "WA", "Tacoma",
                       BigDecimal.valueOf(42.20), convertToLocalDate("2011-10-26"), convertToLocalDate("2023-09-25"),
                       new Owners(new Owner(null), Collections.emptyList())),
               new VehicleRegistration("1C4RJFAG0FC625797", "TH393F", "WA", "Olympia",
                       BigDecimal.valueOf(30.45), convertToLocalDate("2013-09-02"), convertToLocalDate("2024-03-19"),
                       new Owners(new Owner(null), Collections.emptyList()))
       ));
   
       public static final List<Vehicle> VEHICLES = Collections.unmodifiableList(Arrays.asList(
               new Vehicle("1N4AL11D75C109151", "Sedan", 2011,  "Audi", "A5", "Silver"),
               new Vehicle("KM8SRDHF6EU074761", "Sedan", 2015, "Tesla", "Model S", "Blue"),
               new Vehicle("3HGGK5G53FM761765", "Motorcycle", 2011, "Ducati", "Monster 1200", "Yellow"),
               new Vehicle("1HVBBAANXWH544237", "Semi", 2009, "Ford", "F 150", "Black"),
               new Vehicle("1C4RJFAG0FC625797", "Sedan", 2019, "Mercedes", "CLK 350", "White")
       ));
   
       public static final List<Person> PEOPLE = Collections.unmodifiableList(Arrays.asList(
               new Person("Raul", "Lewis", convertToLocalDate("1963-08-19"),
                       "LEWISR261LL", "Driver License",  "1719 University Street, Seattle, WA, 98109"),
               new Person("Brent", "Logan", convertToLocalDate("1967-07-03"),
                       "LOGANB486CG", "Driver License", "43 Stockert Hollow Road, Everett, WA, 98203"),
               new Person("Alexis", "Pena", convertToLocalDate("1974-02-10"),
                       "744 849 301", "SSN", "4058 Melrose Street, Spokane Valley, WA, 99206"),
               new Person("Melvin", "Parker", convertToLocalDate("1976-05-22"),
                       "P626-168-229-765", "Passport", "4362 Ryder Avenue, Seattle, WA, 98101"),
               new Person("Salvatore", "Spencer", convertToLocalDate("1997-11-15"),
                       "S152-780-97-415-0", "Passport", "4450 Honeysuckle Lane, Seattle, WA, 98101")
       ));
   
       public static final List<DriversLicense> LICENSES = Collections.unmodifiableList(Arrays.asList(
               new DriversLicense(null, "LEWISR261LL", "Learner",
                       convertToLocalDate("2016-12-20"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "LOGANB486CG", "Probationary",
                       convertToLocalDate("2016-04-06"), convertToLocalDate("2020-11-15")),
               new DriversLicense(null, "744 849 301", "Full",
                       convertToLocalDate("2017-12-06"), convertToLocalDate("2022-10-15")),
               new DriversLicense(null, "P626-168-229-765", "Learner",
                       convertToLocalDate("2017-08-16"), convertToLocalDate("2021-11-15")),
               new DriversLicense(null, "S152-780-97-415-0", "Probationary",
                       convertToLocalDate("2015-08-15"), convertToLocalDate("2021-08-21"))
       ));
   
       private SampleData() { }
   
       /**
        * Converts a date string with the format 'yyyy-MM-dd' into a {@link java.util.Date} object.
        *
        * @param date
        *              The date string to convert.
        * @return {@link LocalDate} or null if there is a {@link ParseException}
        */
       public static synchronized LocalDate convertToLocalDate(String date) {
           return LocalDate.parse(date, DATE_TIME_FORMAT);
       }
   
       /**
        * Convert the result set into a list of IonValues.
        *
        * @param result
        *              The result set to convert.
        * @return a list of IonValues.
        */
       public static List<IonValue> toIonValues(Result result) {
           final List<IonValue> valueList = new ArrayList<>();
           result.iterator().forEachRemaining(valueList::add);
           return valueList;
       }
   
       /**
        * Get the document ID of a particular document.
        *
        * @param txn
        *              A transaction executor object.
        * @param tableName
        *              Name of the table containing the document.
        * @param identifier
        *              The identifier used to narrow down the search.
        * @param value
        *              Value of the identifier.
        * @return the list of document IDs in the result set.
        */
       public static String getDocumentId(final TransactionExecutor txn, final String tableName,
                                          final String identifier, final String value) {
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(value));
               final String query = String.format("SELECT metadata.id FROM _ql_committed_%s AS p WHERE p.data.%s = ?",
                       tableName, identifier);
               Result result = txn.execute(query, parameters);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document ID using " + value);
               }
               return getStringValueOfStructField((IonStruct) result.iterator().next(), "id");
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the document by ID.
        *
        * @param qldbSession
        *              A QLDB session.
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link QldbRevision} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static QldbRevision getDocumentById(QldbSession qldbSession, String tableName, String documentId) {
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(documentId));
               final String query = String.format("SELECT c.* FROM _ql_committed_%s AS c BY docId WHERE docId = ?", tableName);
               Result result = qldbSession.execute(query, parameters);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to retrieve document by id " + documentId + " in table " + tableName);
               }
               return Constants.MAPPER.readValue(result.iterator().next(), QldbRevision.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Return a list of modified document IDs as strings from a DML {@link Result}.
        *
        * @param result
        *              The result set from a DML operation.
        * @return the list of document IDs modified by the operation.
        */
       public static List<String> getDocumentIdsFromDmlResult(final Result result) {
           final List<String> strings = new ArrayList<>();
           result.iterator().forEachRemaining(row -> strings.add(getDocumentIdFromDmlResultDocument(row)));
           return strings;
       }
   
       /**
        * Convert the given DML result row's document ID to string.
        *
        * @param dmlResultDocument
        *              The {@link IonValue} representing the results of a DML operation.
        * @return a string of document ID.
        */
       public static String getDocumentIdFromDmlResultDocument(final IonValue dmlResultDocument) {
           try {
               DmlResultDocument result = Constants.MAPPER.readValue(dmlResultDocument, DmlResultDocument.class);
               return result.getDocumentId();
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Get the String value of a given {@link IonStruct} field name.
        * @param struct the {@link IonStruct} from which to get the value.
        * @param fieldName the name of the field from which to get the value.
        * @return the String value of the field within the given {@link IonStruct}.
        */
       public static String getStringValueOfStructField(final IonStruct struct, final String fieldName) {
           return ((IonString) struct.get(fieldName)).stringValue();
       }
   
       /**
        * Return a copy of the given driver's license with updated person Id.
        *
        * @param oldLicense
        *              The old driver's license to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link DriversLicense}.
        */
       public static DriversLicense updatePersonIdDriversLicense(final DriversLicense oldLicense, final String personId) {
           return new DriversLicense(personId, oldLicense.getLicenseNumber(), oldLicense.getLicenseType(),
                   oldLicense.getValidFromDate(), oldLicense.getValidToDate());
       }
   
       /**
        * Return a copy of the given vehicle registration with updated person Id.
        *
        * @param oldRegistration
        *              The old vehicle registration to update.
        * @param personId
        *              The PersonId of the driver.
        * @return the updated {@link VehicleRegistration}.
        */
       public static VehicleRegistration updateOwnerVehicleRegistration(final VehicleRegistration oldRegistration,
                                                                        final String personId) {
           return new VehicleRegistration(oldRegistration.getVin(), oldRegistration.getLicensePlateNumber(),
                   oldRegistration.getState(), oldRegistration.getCity(), oldRegistration.getPendingPenaltyTicketAmount(),
                   oldRegistration.getValidFromDate(), oldRegistration.getValidToDate(),
                   new Owners(new Owner(personId), Collections.emptyList()));
       }
   }
   ```

------
**참고**  
이 클래스는 Ion 라이브러리를 사용하여 데이터를 Ion 형식으로 또는 Ion 형식에서 변환하는 도우미 메서드를 제공합니다.
이 `getDocumentId` 메서드는 접두사 `_ql_committed_`가 있는 테이블에 대한 쿼리를 실행합니다. 이는 테이블의 *커밋된 보기*를 쿼리하려는 것을 나타내는 예약된 접두사입니다. 이 뷰에서는 데이터가 `data` 필드에 중첩되고 메타데이터는 `metadata` 필드에 중첩됩니다.

1. 다음 프로그램(`CreateTable.java`)을 컴파일하고 실행하여 앞서 언급한 테이블을 생성합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create tables in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateTable {
       public static final Logger log = LoggerFactory.getLogger(CreateTable.class);
   
       private CreateTable() { }
   
       /**
        * Registrations, vehicles, owners, and licenses tables being created in a single transaction.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @return the number of tables created.
        */
       public static int createTable(final TransactionExecutor txn, final String tableName) {
           log.info("Creating the '{}' table...", tableName);
           final String createTable = String.format("CREATE TABLE %s", tableName);
           final Result result = txn.execute(createTable);
           log.info("{} table created successfully.", tableName);
           return SampleData.toIonValues(result).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createTable(txn, Constants.DRIVERS_LICENSE_TABLE_NAME);
               createTable(txn, Constants.PERSON_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           });
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create tables in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateTable {
       public static final Logger log = LoggerFactory.getLogger(CreateTable.class);
   
       private CreateTable() { }
   
       /**
        * Registrations, vehicles, owners, and licenses tables being created in a single transaction.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @return the number of tables created.
        */
       public static int createTable(final TransactionExecutor txn, final String tableName) {
           log.info("Creating the '{}' table...", tableName);
           final String createTable = String.format("CREATE TABLE %s", tableName);
           final Result result = txn.execute(createTable);
           log.info("{} table created successfully.", tableName);
           return SampleData.toIonValues(result).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createTable(txn, Constants.DRIVERS_LICENSE_TABLE_NAME);
               createTable(txn, Constants.PERSON_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_TABLE_NAME);
               createTable(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
       }
   }
   ```

------
**참고**  
이 프로그램은 `TransactionExecutor` Lambda를 `execute` 메서드에 전달하는 방법을 보여줍니다. 이 예제에서는 Lambda 표현식을 사용하여 단일 트랜잭션에서 여러 `CREATE TABLE` PartiQL 명령문을 실행합니다.  
`execute` 메서드는 암시적으로 트랜잭션을 시작하고 Lambda에서 모든 문을 실행한 다음 트랜잭션을 자동 커밋합니다.

1. 앞에서 설명한 대로 다음 프로그램(`CreateIndex.java`)을 컴파일하고 실행하여 테이블에 인덱스를 생성합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create indexes on tables in a particular ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateIndex {
       public static final Logger log = LoggerFactory.getLogger(CreateIndex.class);
   
       private CreateIndex() { }
   
       /**
        * In this example, create indexes for registrations and vehicles tables.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @param indexAttribute
        *              The index attribute to use.
        * @return the number of tables created.
        */
       public static int createIndex(final TransactionExecutor txn, final String tableName, final String indexAttribute) {
           log.info("Creating an index on {}...", indexAttribute);
           final String createIndex = String.format("CREATE INDEX ON %s (%s)", tableName, indexAttribute);
           final Result r = txn.execute(createIndex);
           return SampleData.toIonValues(r).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createIndex(txn, Constants.PERSON_TABLE_NAME, Constants.PERSON_GOV_ID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_NUMBER_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_PERSONID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Constants.VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME);
           });
           log.info("Indexes created successfully!");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Create indexes on tables in a particular ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class CreateIndex {
       public static final Logger log = LoggerFactory.getLogger(CreateIndex.class);
   
       private CreateIndex() { }
   
       /**
        * In this example, create indexes for registrations and vehicles tables.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to be created.
        * @param indexAttribute
        *              The index attribute to use.
        * @return the number of tables created.
        */
       public static int createIndex(final TransactionExecutor txn, final String tableName, final String indexAttribute) {
           log.info("Creating an index on {}...", indexAttribute);
           final String createIndex = String.format("CREATE INDEX ON %s (%s)", tableName, indexAttribute);
           final Result r = txn.execute(createIndex);
           return SampleData.toIonValues(r).size();
       }
   
       public static void main(final String... args) {
           ConnectToLedger.getDriver().execute(txn -> {
               createIndex(txn, Constants.PERSON_TABLE_NAME, Constants.PERSON_GOV_ID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_NUMBER_INDEX_NAME);
               createIndex(txn, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.DRIVER_LICENSE_PERSONID_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VIN_INDEX_NAME);
               createIndex(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Constants.VEHICLE_REGISTRATION_LICENSE_PLATE_NUMBER_INDEX_NAME);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Indexes created successfully!");
       }
   }
   ```

------

1. 다음 프로그램(`InsertDocument.java`)을 컴파일하고 실행하여 샘플 데이터를 테이블에 삽입합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.DriversLicense;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   /**
    * Insert documents into a table in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class InsertDocument {
       public static final Logger log = LoggerFactory.getLogger(InsertDocument.class);
   
       private InsertDocument() { }
   
       /**
        * Insert the given list of documents into the specified table and return the document IDs of the inserted documents.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documents
        *              List of documents to insert into the specified table.
        * @return a list of document IDs.
        * @throws IllegalStateException if failed to convert documents into an {@link IonValue}.
        */
       public static List<String> insertDocuments(final TransactionExecutor txn, final String tableName,
                                                  final List documents) {
           log.info("Inserting some documents in the {} table...", tableName);
           try {
               final String query = String.format("INSERT INTO %s ?", tableName);
               final IonValue ionDocuments = Constants.MAPPER.writeValueAsIonValue(documents);
   
               return SampleData.getDocumentIdsFromDmlResult(txn.execute(query, ionDocuments));
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update PersonIds in driver's licenses and in vehicle registrations using document IDs.
        *
        * @param documentIds
        *              List of document IDs representing the PersonIds in DriversLicense and PrimaryOwners in VehicleRegistration.
        * @param licenses
        *              List of driver's licenses to update.
        * @param registrations
        *              List of registrations to update.
        */
       public static void updatePersonId(final List<String> documentIds, final List<DriversLicense> licenses,
                                         final List<VehicleRegistration> registrations) {
           for (int i = 0; i < documentIds.size(); ++i) {
               DriversLicense license = SampleData.LICENSES.get(i);
               VehicleRegistration registration = SampleData.REGISTRATIONS.get(i);
               licenses.add(SampleData.updatePersonIdDriversLicense(license, documentIds.get(i)));
               registrations.add(SampleData.updateOwnerVehicleRegistration(registration, documentIds.get(i)));
           }
       }
   
       public static void main(final String... args) {
           final List<DriversLicense> newDriversLicenses = new ArrayList<>();
           final List<VehicleRegistration> newVehicleRegistrations = new ArrayList<>();
           ConnectToLedger.getDriver().execute(txn -> {
               List<String> documentIds = insertDocuments(txn, Constants.PERSON_TABLE_NAME, SampleData.PEOPLE);
               updatePersonId(documentIds, newDriversLicenses, newVehicleRegistrations);
               insertDocuments(txn, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLES);
               insertDocuments(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Collections.unmodifiableList(newVehicleRegistrations));
               insertDocuments(txn, Constants.DRIVERS_LICENSE_TABLE_NAME,
                       Collections.unmodifiableList(newDriversLicenses));
           });
           log.info("Documents inserted successfully!");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonValue;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.DriversLicense;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Insert documents into a table in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class InsertDocument {
       public static final Logger log = LoggerFactory.getLogger(InsertDocument.class);
   
       private InsertDocument() { }
   
       /**
        * Insert the given list of documents into the specified table and return the document IDs of the inserted documents.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param tableName
        *              Name of the table to insert documents into.
        * @param documents
        *              List of documents to insert into the specified table.
        * @return a list of document IDs.
        * @throws IllegalStateException if failed to convert documents into an {@link IonValue}.
        */
       public static List<String> insertDocuments(final TransactionExecutor txn, final String tableName,
                                                  final List documents) {
           log.info("Inserting some documents in the {} table...", tableName);
           try {
               final String statement = String.format("INSERT INTO %s ?", tableName);
               final IonValue ionDocuments = Constants.MAPPER.writeValueAsIonValue(documents);
               final List<IonValue> parameters = Collections.singletonList(ionDocuments);
               return SampleData.getDocumentIdsFromDmlResult(txn.execute(statement, parameters));
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update PersonIds in driver's licenses and in vehicle registrations using document IDs.
        *
        * @param documentIds
        *              List of document IDs representing the PersonIds in DriversLicense and PrimaryOwners in VehicleRegistration.
        * @param licenses
        *              List of driver's licenses to update.
        * @param registrations
        *              List of registrations to update.
        */
       public static void updatePersonId(final List<String> documentIds, final List<DriversLicense> licenses,
                                         final List<VehicleRegistration> registrations) {
           for (int i = 0; i < documentIds.size(); ++i) {
               DriversLicense license = SampleData.LICENSES.get(i);
               VehicleRegistration registration = SampleData.REGISTRATIONS.get(i);
               licenses.add(SampleData.updatePersonIdDriversLicense(license, documentIds.get(i)));
               registrations.add(SampleData.updateOwnerVehicleRegistration(registration, documentIds.get(i)));
           }
       }
   
       public static void main(final String... args) {
           final List<DriversLicense> newDriversLicenses = new ArrayList<>();
           final List<VehicleRegistration> newVehicleRegistrations = new ArrayList<>();
           ConnectToLedger.getDriver().execute(txn -> {
               List<String> documentIds = insertDocuments(txn, Constants.PERSON_TABLE_NAME, SampleData.PEOPLE);
               updatePersonId(documentIds, newDriversLicenses, newVehicleRegistrations);
               insertDocuments(txn, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLES);
               insertDocuments(txn, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                       Collections.unmodifiableList(newVehicleRegistrations));
               insertDocuments(txn, Constants.DRIVERS_LICENSE_TABLE_NAME,
                       Collections.unmodifiableList(newDriversLicenses));
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Documents inserted successfully!");
       }
   }
   ```

------
**참고**  
이 프로그램은 파라미터화된 값을 사용하여 `execute` 메서드를 호출하는 방법을 보여줍니다. 실행하려는 PartiQL 문 외에도 `IonValue` 유형의 데이터 파라미터를 전달할 수 있습니다. 명령문 문자열에서 물음표(`?`)를 변수 자리 표시자로 사용하세요.
`INSERT` 문이 성공하면 삽입된 각 문서의 `id`이 반환됩니다.

다음으로, `SELECT` 문을 사용하여 `vehicle-registration` 원장의 테이블에서 데이터를 읽을 수 있습니다. [4단계: 원장에서 테이블 쿼리](getting-started.java.step-4.md)로 이동합니다.

# 4단계: 원장에서 테이블 쿼리
<a name="getting-started.java.step-4"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 원장에 테이블을 생성하고 데이터를 로드한 후 쿼리를 실행하여 방금 삽입한 차량 등록 데이터를 검토할 수 있습니다. QLDB는 [PartiQL](ql-reference.md)을 쿼리 언어로 사용하고 [Amazon Ion](ion.md)을 문서 지향 데이터 모델로 사용합니다.

PartiQL은 Ion과 함께 작동하도록 확장된 오픈 소스 SQL 호환 쿼리 언어입니다. PartiQL을 사용하면 익숙한 SQL 연산자를 사용하여 데이터를 삽입, 쿼리 및 관리할 수 있습니다. Amazon Ion은 JSON의 상위 집합입니다. Ion은 정형, 반정형 및 중첩 데이터를 저장하고 처리할 수 있는 유연성을 제공하는 오픈 소스 문서 기반 데이터 형식입니다.

이 단계에서는 `SELECT` 명령문을 사용하여 `vehicle-registration` 원장의 테이블에서 데이터를 읽습니다.

**주의**  
인덱싱된 조회 없이 QLDB에서 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. PartiQL은 SQL과 호환되므로 이러한 쿼리를 지원합니다. 하지만 QLDB의 프로덕션 사용 사례에 대해서는 테이블 스캔을 실행하지 *마세요.*. 테이블 스캔은 동시성 충돌 및 트랜잭션 시간 초과를 포함하여 대규모 테이블에서 성능 문제를 일으킬 수 있습니다.  
인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 자세한 내용은 [쿼리 성능 최적화](working.optimize.md)을 참조하세요.

**테이블을 쿼리하려면**
+ 다음 프로그램(`FindVehicles.java`)을 컴파일하고 실행하여 원장에 있는 사람의 이름으로 등록된 모든 차량을 쿼리하세요.

------
#### [ 2.x ]

  ```
  /*
   * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   * SPDX-License-Identifier: MIT-0
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy of this
   * software and associated documentation files (the "Software"), to deal in the Software
   * without restriction, including without limitation the rights to use, copy, modify,
   * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   * permit persons to whom the Software is furnished to do so.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   */
  
  package software.amazon.qldb.tutorial;
  
  import java.io.IOException;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.amazon.ion.IonValue;
  
  import software.amazon.qldb.Result;
  import software.amazon.qldb.TransactionExecutor;
  import software.amazon.qldb.tutorial.model.Person;
  import software.amazon.qldb.tutorial.model.SampleData;
  
  /**
   * Find all vehicles registered under a person.
   *
   * This code expects that you have AWS credentials setup per:
   * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
   */
  public final class FindVehicles {
      public static final Logger log = LoggerFactory.getLogger(FindVehicles.class);
  
      private FindVehicles() { }
  
      /**
       * Find vehicles registered under a driver using their government ID.
       *
       * @param txn
       *              The {@link TransactionExecutor} for lambda execute.
       * @param govId
       *              The government ID of the owner.
       * @throws IllegalStateException if failed to convert parameters into {@link IonValue}.
       */
      public static void findVehiclesForOwner(final TransactionExecutor txn, final String govId) {
          try {
              final String documentId = Person.getDocumentIdByGovId(txn, govId);
              final String query = "SELECT v FROM Vehicle AS v INNER JOIN VehicleRegistration AS r "
                      + "ON v.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?";
  
              final Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
              log.info("List of Vehicles for owner with GovId: {}...", govId);
              ScanTable.printDocuments(result);
          } catch (IOException ioe) {
              throw new IllegalStateException(ioe);
          }
      }
  
      public static void main(final String... args) {
          final Person person = SampleData.PEOPLE.get(0);
          ConnectToLedger.getDriver().execute(txn -> {
              findVehiclesForOwner(txn, person.getGovId());
          });
      }
  }
  ```

------
#### [ 1.x ]

  ```
  /*
   * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
   * SPDX-License-Identifier: MIT-0
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy of this
   * software and associated documentation files (the "Software"), to deal in the Software
   * without restriction, including without limitation the rights to use, copy, modify,
   * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   * permit persons to whom the Software is furnished to do so.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   */
  
  package software.amazon.qldb.tutorial;
  
  import java.io.IOException;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.amazon.ion.IonValue;
  
  import software.amazon.qldb.Result;
  import software.amazon.qldb.TransactionExecutor;
  import software.amazon.qldb.tutorial.model.Person;
  import software.amazon.qldb.tutorial.model.SampleData;
  
  /**
   * Find all vehicles registered under a person.
   *
   * This code expects that you have AWS credentials setup per:
   * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
   */
  public final class FindVehicles {
      public static final Logger log = LoggerFactory.getLogger(FindVehicles.class);
  
      private FindVehicles() { }
  
      /**
       * Find vehicles registered under a driver using their government ID.
       *
       * @param txn
       *              The {@link TransactionExecutor} for lambda execute.
       * @param govId
       *              The government ID of the owner.
       * @throws IllegalStateException if failed to convert parameters into {@link IonValue}.
       */
      public static void findVehiclesForOwner(final TransactionExecutor txn, final String govId) {
          try {
              final String documentId = Person.getDocumentIdByGovId(txn, govId);
              final String query = "SELECT v FROM Vehicle AS v INNER JOIN VehicleRegistration AS r "
                      + "ON v.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?";
  
              final Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
              log.info("List of Vehicles for owner with GovId: {}...", govId);
              ScanTable.printDocuments(result);
          } catch (IOException ioe) {
              throw new IllegalStateException(ioe);
          }
      }
  
      public static void main(final String... args) {
          final Person person = SampleData.PEOPLE.get(0);
          ConnectToLedger.getDriver().execute(txn -> {
              findVehiclesForOwner(txn, person.getGovId());
          }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
      }
  }
  ```

------
**참고**  
먼저 이 프로그램은 이 문서에 대한 `Person` 테이블을 `GovId LEWISR261LL`로 쿼리하여 `id` 메타데이터 필드를 가져옵니다.  
그런 다음 이 문서 `id`를 외래 키로 사용하여 `VehicleRegistration` 테이블을 `PrimaryOwner.PersonId`로 쿼리합니다. 또한 `VIN` 필드의 `Vehicle` 테이블과 `VehicleRegistration`를 조인합니다.

`vehicle-registration` 원장의 테이블에 있는 문서를 수정하는 방법에 대한 자세한 내용은 [5단계: 원장의 문서 수정](getting-started.java.step-5.md)을 참조하세요.

# 5단계: 원장의 문서 수정
<a name="getting-started.java.step-5"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이제 작업할 데이터가 있으므로 Amazon QLDB에서 `vehicle-registration` 원장에 있는 문서를 변경할 수 있습니다. 이 단계에서 다음 코드 예제는 DML(데이터 조작 언어) 문을 실행하는 방법을 보여줍니다. 이러한 명령문은 한 차량의 주 소유자를 업데이트하고 다른 차량에 보조 소유자를 추가합니다.

**문서를 수정하려면**

1. 다음 프로그램(`TransferVehicleOwnership.java`)을 컴파일하고 실행하여 차량의 기본 소유주를 원장의 VIN `1N4AL11D75C109151`으로 업데이트합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.system.IonReaderBuilder;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.LinkedHashMap;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Find primary owner for a particular vehicle's VIN.
    * Transfer to another primary owner for a particular vehicle's VIN.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class TransferVehicleOwnership {
       public static final Logger log = LoggerFactory.getLogger(TransferVehicleOwnership.class);
   
       private TransferVehicleOwnership() { }
   
       /**
        * Query a driver's information using the given ID.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPersonFromDocumentId(final TransactionExecutor txn, final String documentId) {
           try {
               log.info("Finding person for documentId: {}...", documentId);
               final String query = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?";
   
               Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to find person with ID: " + documentId);
               }
   
               return Constants.MAPPER.readValue(result.iterator().next(), Person.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Find the primary owner for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPrimaryOwnerForVehicle(final TransactionExecutor txn, final String vin) {
           try {
               log.info("Finding primary owner for vehicle with Vin: {}...", vin);
               final String query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               Result result = txn.execute(query, parameters);
               final List<IonStruct> documents = ScanTable.toIonStructs(result);
               ScanTable.printDocuments(documents);
               if (documents.isEmpty()) {
                   throw new IllegalStateException("Unable to find registrations with VIN: " + vin);
               }
   
               final IonReader reader = IonReaderBuilder.standard().build(documents.get(0));
               final String personId = Constants.MAPPER.readValue(reader, LinkedHashMap.class).get("PersonId").toString();
               return findPersonFromDocumentId(txn, personId);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update the primary owner for a vehicle registration with the given documentId.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param documentId
        *              New PersonId for the primary owner.
        * @throws IllegalStateException if no vehicle registration was found using the given document ID and VIN, or if failed
        * to convert parameters into {@link IonValue}.
        */
       public static void updateVehicleRegistration(final TransactionExecutor txn, final String vin, final String documentId) {
           try {
               log.info("Updating primary owner for vehicle with Vin: {}...", vin);
               final String query = "UPDATE VehicleRegistration AS v SET v.Owners.PrimaryOwner = ? WHERE v.VIN = ?";
   
               final List<IonValue> parameters = new ArrayList<>();
               parameters.add(Constants.MAPPER.writeValueAsIonValue(new Owner(documentId)));
               parameters.add(Constants.MAPPER.writeValueAsIonValue(vin));
   
               Result result = txn.execute(query, parameters);
               ScanTable.printDocuments(result);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to transfer vehicle, could not find registration.");
               } else {
                   log.info("Successfully transferred vehicle with VIN '{}' to new owner.", vin);
               }
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(0).getVin();
           final String primaryOwnerGovId = SampleData.PEOPLE.get(0).getGovId();
           final String newPrimaryOwnerGovId = SampleData.PEOPLE.get(1).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final Person primaryOwner = findPrimaryOwnerForVehicle(txn, vin);
               if (!primaryOwner.getGovId().equals(primaryOwnerGovId)) {
                   // Verify the primary owner.
                   throw new IllegalStateException("Incorrect primary owner identified for vehicle, unable to transfer.");
               }
   
               final String newOwner = Person.getDocumentIdByGovId(txn, newPrimaryOwnerGovId);
               updateVehicleRegistration(txn, vin, newOwner);
           });
           log.info("Successfully transferred vehicle ownership!");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.system.IonReaderBuilder;
   
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.LinkedHashMap;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Find primary owner for a particular vehicle's VIN.
    * Transfer to another primary owner for a particular vehicle's VIN.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class TransferVehicleOwnership {
       public static final Logger log = LoggerFactory.getLogger(TransferVehicleOwnership.class);
   
       private TransferVehicleOwnership() { }
   
       /**
        * Query a driver's information using the given ID.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param documentId
        *              The unique ID of a document in the Person table.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPersonFromDocumentId(final TransactionExecutor txn, final String documentId) {
           try {
               log.info("Finding person for documentId: {}...", documentId);
               final String query = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?";
   
               Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(documentId));
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to find person with ID: " + documentId);
               }
   
               return Constants.MAPPER.readValue(result.iterator().next(), Person.class);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Find the primary owner for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @return a {@link Person} object.
        * @throws IllegalStateException if failed to convert parameter into {@link IonValue}.
        */
       public static Person findPrimaryOwnerForVehicle(final TransactionExecutor txn, final String vin) {
           try {
               log.info("Finding primary owner for vehicle with Vin: {}...", vin);
               final String query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               Result result = txn.execute(query, parameters);
               final List<IonStruct> documents = ScanTable.toIonStructs(result);
               ScanTable.printDocuments(documents);
               if (documents.isEmpty()) {
                   throw new IllegalStateException("Unable to find registrations with VIN: " + vin);
               }
   
               final IonReader reader = IonReaderBuilder.standard().build(documents.get(0));
               final String personId = Constants.MAPPER.readValue(reader, LinkedHashMap.class).get("PersonId").toString();
               return findPersonFromDocumentId(txn, personId);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Update the primary owner for a vehicle registration with the given documentId.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param documentId
        *              New PersonId for the primary owner.
        * @throws IllegalStateException if no vehicle registration was found using the given document ID and VIN, or if failed
        * to convert parameters into {@link IonValue}.
        */
       public static void updateVehicleRegistration(final TransactionExecutor txn, final String vin, final String documentId) {
           try {
               log.info("Updating primary owner for vehicle with Vin: {}...", vin);
               final String query = "UPDATE VehicleRegistration AS v SET v.Owners.PrimaryOwner = ? WHERE v.VIN = ?";
   
               final List<IonValue> parameters = new ArrayList<>();
               parameters.add(Constants.MAPPER.writeValueAsIonValue(new Owner(documentId)));
               parameters.add(Constants.MAPPER.writeValueAsIonValue(vin));
   
               Result result = txn.execute(query, parameters);
               ScanTable.printDocuments(result);
               if (result.isEmpty()) {
                   throw new IllegalStateException("Unable to transfer vehicle, could not find registration.");
               } else {
                   log.info("Successfully transferred vehicle with VIN '{}' to new owner.", vin);
               }
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(0).getVin();
           final String primaryOwnerGovId = SampleData.PEOPLE.get(0).getGovId();
           final String newPrimaryOwnerGovId = SampleData.PEOPLE.get(1).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final Person primaryOwner = findPrimaryOwnerForVehicle(txn, vin);
               if (!primaryOwner.getGovId().equals(primaryOwnerGovId)) {
                   // Verify the primary owner.
                   throw new IllegalStateException("Incorrect primary owner identified for vehicle, unable to transfer.");
               }
   
               final String newOwner = Person.getDocumentIdByGovId(txn, newPrimaryOwnerGovId);
               updateVehicleRegistration(txn, vin, newOwner);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Successfully transferred vehicle ownership!");
       }
   }
   ```

------

1. 다음 프로그램(`AddSecondaryOwner.java`)을 컴파일하고 실행하여 보조 소유자를 원장의 VIN `KM8SRDHF6EU074761`로 차량에 추가합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.util.Collections;
   import java.util.Iterator;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Owners;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Finds and adds secondary owners for a vehicle.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class AddSecondaryOwner {
       public static final Logger log = LoggerFactory.getLogger(AddSecondaryOwner.class);
   
       private AddSecondaryOwner() { }
   
       /**
        * Check whether a secondary owner has already been registered for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwnerId
        *              The secondary owner to add.
        * @return {@code true} if the given secondary owner has already been registered, {@code false} otherwise.
        * @throws IllegalStateException if failed to convert VIN to an {@link IonValue}.
        */
       public static boolean isSecondaryOwnerForVehicle(final TransactionExecutor txn, final String vin,
                                                        final String secondaryOwnerId) {
           try {
               log.info("Finding secondary owners for vehicle with VIN: {}...", vin);
               final String query = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               final Iterator<IonValue> itr = result.iterator();
               if (!itr.hasNext()) {
                   return false;
               }
   
               final Owners owners = Constants.MAPPER.readValue(itr.next(), Owners.class);
               if (null != owners.getSecondaryOwners()) {
                   for (Owner owner : owners.getSecondaryOwners()) {
                       if (secondaryOwnerId.equalsIgnoreCase(owner.getPersonId())) {
                           return true;
                       }
                   }
               }
   
               return false;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Adds a secondary owner for the specified VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwner
        *              The secondary owner to add.
        * @throws IllegalStateException if failed to convert parameter into an {@link IonValue}.
        */
       public static void addSecondaryOwnerForVin(final TransactionExecutor txn, final String vin,
                                                  final String secondaryOwner) {
           try {
               log.info("Inserting secondary owner for vehicle with VIN: {}...", vin);
               final String query = String.format("FROM VehicleRegistration AS v WHERE v.VIN = ?" +
                       "INSERT INTO v.Owners.SecondaryOwners VALUE ?");
               final IonValue newOwner = Constants.MAPPER.writeValueAsIonValue(new Owner(secondaryOwner));
               final IonValue vinAsIonValue = Constants.MAPPER.writeValueAsIonValue(vin);
               Result result = txn.execute(query, vinAsIonValue, newOwner);
               log.info("VehicleRegistration Document IDs which had secondary owners added: ");
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(1).getVin();
           final String govId = SampleData.PEOPLE.get(0).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final String documentId = Person.getDocumentIdByGovId(txn, govId);
               if (isSecondaryOwnerForVehicle(txn, vin, documentId)) {
                   log.info("Person with ID {} has already been added as a secondary owner of this vehicle.", govId);
               } else {
                   addSecondaryOwnerForVin(txn, vin, documentId);
               }
           });
           log.info("Secondary owners successfully updated.");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.util.Collections;
   import java.util.Iterator;
   import java.util.List;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.Owner;
   import software.amazon.qldb.tutorial.model.Owners;
   import software.amazon.qldb.tutorial.model.Person;
   import software.amazon.qldb.tutorial.model.SampleData;
   
   /**
    * Finds and adds secondary owners for a vehicle.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class AddSecondaryOwner {
       public static final Logger log = LoggerFactory.getLogger(AddSecondaryOwner.class);
   
       private AddSecondaryOwner() { }
   
       /**
        * Check whether a secondary owner has already been registered for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwnerId
        *              The secondary owner to add.
        * @return {@code true} if the given secondary owner has already been registered, {@code false} otherwise.
        * @throws IllegalStateException if failed to convert VIN to an {@link IonValue}.
        */
       public static boolean isSecondaryOwnerForVehicle(final TransactionExecutor txn, final String vin,
                                                        final String secondaryOwnerId) {
           try {
               log.info("Finding secondary owners for vehicle with VIN: {}...", vin);
               final String query = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?";
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               final Iterator<IonValue> itr = result.iterator();
               if (!itr.hasNext()) {
                   return false;
               }
   
               final Owners owners = Constants.MAPPER.readValue(itr.next(), Owners.class);
               if (null != owners.getSecondaryOwners()) {
                   for (Owner owner : owners.getSecondaryOwners()) {
                       if (secondaryOwnerId.equalsIgnoreCase(owner.getPersonId())) {
                           return true;
                       }
                   }
               }
   
               return false;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Adds a secondary owner for the specified VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              Unique VIN for a vehicle.
        * @param secondaryOwner
        *              The secondary owner to add.
        * @throws IllegalStateException if failed to convert parameter into an {@link IonValue}.
        */
       public static void addSecondaryOwnerForVin(final TransactionExecutor txn, final String vin,
                                                  final String secondaryOwner) {
           try {
               log.info("Inserting secondary owner for vehicle with VIN: {}...", vin);
               final String query = String.format("FROM VehicleRegistration AS v WHERE v.VIN = '%s' " +
                       "INSERT INTO v.Owners.SecondaryOwners VALUE ?", vin);
               final IonValue newOwner = Constants.MAPPER.writeValueAsIonValue(new Owner(secondaryOwner));
               Result result = txn.execute(query, newOwner);
               log.info("VehicleRegistration Document IDs which had secondary owners added: ");
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String vin = SampleData.VEHICLES.get(1).getVin();
           final String govId = SampleData.PEOPLE.get(0).getGovId();
   
           ConnectToLedger.getDriver().execute(txn -> {
               final String documentId = Person.getDocumentIdByGovId(txn, govId);
               if (isSecondaryOwnerForVehicle(txn, vin, documentId)) {
                   log.info("Person with ID {} has already been added as a secondary owner of this vehicle.", govId);
               } else {
                   addSecondaryOwnerForVin(txn, vin, documentId);
               }
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Secondary owners successfully updated.");
       }
   }
   ```

------

`vehicle-registration` 원장의 이러한 변경 사항을 검토하려면 [6단계: 문서의 개정 기록 보기](getting-started.java.step-6.md)을 참조하세요.

# 6단계: 문서의 개정 기록 보기
<a name="getting-started.java.step-6"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이전 단계에서 차량의 등록 데이터를 수정한 후 등록된 모든 소유자의 기록과 기타 업데이트된 필드를 쿼리할 수 있습니다. 이 단계에서는 `vehicle-registration` 원장의 `VehicleRegistration` 테이블에 있는 문서의 개정 기록을 쿼리합니다.

**개정 기록을 보려면**

1. 다음 프로그램(`QueryHistory.java`)을 검토합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import java.io.IOException;
   import java.time.Instant;
   import java.time.temporal.ChronoUnit;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import com.amazon.ion.IonValue;
   
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   /**
    * Query a table's history for a particular set of documents.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class QueryHistory {
       public static final Logger log = LoggerFactory.getLogger(QueryHistory.class);
       private static final int THREE_MONTHS = 90;
   
       private QueryHistory() { }
   
       /**
        * In this example, query the 'VehicleRegistration' history table to find all previous primary owners for a VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              VIN to find previous primary owners for.
        * @param query
        *              The query to find previous primary owners.
        * @throws IllegalStateException if failed to convert document ID to an {@link IonValue}.
        */
       public static void previousPrimaryOwners(final TransactionExecutor txn, final String vin, final String query) {
           try {
               final String docId = VehicleRegistration.getDocumentIdByVin(txn, vin);
   
               log.info("Querying the 'VehicleRegistration' table's history using VIN: {}...", vin);
               final Result result = txn.execute(query, Constants.MAPPER.writeValueAsIonValue(docId));
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String threeMonthsAgo = Instant.now().minus(THREE_MONTHS, ChronoUnit.DAYS).toString();
           final String query = String.format("SELECT data.Owners.PrimaryOwner, metadata.version "
                                              + "FROM history(VehicleRegistration, `%s`) "
                                              + "AS h WHERE h.metadata.id = ?", threeMonthsAgo);
           ConnectToLedger.getDriver().execute(txn -> {
               final String vin = SampleData.VEHICLES.get(0).getVin();
               previousPrimaryOwners(txn, vin, query);
           });
           log.info("Successfully queried history.");
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonValue;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.model.VehicleRegistration;
   
   import java.io.IOException;
   import java.time.Instant;
   import java.time.temporal.ChronoUnit;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Query a table's history for a particular set of documents.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class QueryHistory {
       public static final Logger log = LoggerFactory.getLogger(QueryHistory.class);
       private static final int THREE_MONTHS = 90;
   
       private QueryHistory() { }
   
       /**
        * In this example, query the 'VehicleRegistration' history table to find all previous primary owners for a VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              VIN to find previous primary owners for.
        * @param query
        *              The query to find previous primary owners.
        * @throws IllegalStateException if failed to convert document ID to an {@link IonValue}.
        */
       public static void previousPrimaryOwners(final TransactionExecutor txn, final String vin, final String query) {
           try {
               final String docId = VehicleRegistration.getDocumentIdByVin(txn, vin);
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(docId));
               log.info("Querying the 'VehicleRegistration' table's history using VIN: {}...", vin);
               final Result result = txn.execute(query, parameters);
               ScanTable.printDocuments(result);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       public static void main(final String... args) {
           final String threeMonthsAgo = Instant.now().minus(THREE_MONTHS, ChronoUnit.DAYS).toString();
           final String query = String.format("SELECT data.Owners.PrimaryOwner, metadata.version "
                                              + "FROM history(VehicleRegistration, `%s`) "
                                              + "AS h WHERE h.metadata.id = ?", threeMonthsAgo);
           ConnectToLedger.getDriver().execute(txn -> {
               final String vin = SampleData.VEHICLES.get(0).getVin();
               previousPrimaryOwners(txn, vin, query);
           }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
           log.info("Successfully queried history.");
       }
   }
   ```

------
**참고**  
다음 구문에 내장된 [기록 함수](working.history.md#working.history.function)를 쿼리하여 문서의 개정 기록을 볼 수 있습니다  

     ```
     SELECT * FROM history( table_name [, `start-time` [, `end-time` ] ] ) AS h
     [ WHERE h.metadata.id = 'id' ]
     ```
*시작 시간*과 *종료 시간*은 모두 선택 사항입니다. 이 값은 백틱(``...``)으로 표시할 수 있는 Amazon Ion 리터럴 값입니다. 자세한 내용은 [Amazon QLDB에서 PartiQL을 사용한 Ion 쿼리](ql-reference.query.md) 섹션을 참조하세요.
가장 좋은 방법은 날짜 범위(*시작 시간* 및 *종료 시간*)와 문서 ID(`metadata.id`)를 모두 사용하여 기록 쿼리를 한정하는 것입니다. QLDB는 트랜잭션에서 `SELECT` 쿼리를 처리하며, 이 쿼리에는 [트랜잭션 시간 초과 제한](limits.md#limits.fixed)이 적용됩니다.  
QLDB 기록은 문서 ID로 인덱싱되며, 이번에는 추가 기록 인덱스를 만들 수 없습니다. 시작 시간과 종료 시간을 포함하는 기록 쿼리는 날짜 범위 한정이라는 이점을 얻습니다.

1. `QueryHistory.java` 프로그램을 컴파일하고 실행하여 VIN `1N4AL11D75C109151`으로 `VehicleRegistration` 문서의 개정 기록을 쿼리합니다.

`vehicle-registration` 원장의 문서 개정을 암호화 방식으로 검증하려면 [7단계: 원장에 있는 문서 검증](getting-started.java.step-7.md)으로 진행하세요.

# 7단계: 원장에 있는 문서 검증
<a name="getting-started.java.step-7"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB를 사용하면 SHA-256 암호화 해싱을 사용하여 원장 저널에 있는 문서의 무결성을 효율적으로 검증할 수 있습니다. QLDB에서 검증 및 암호화 해싱이 작동하는 방식에 대한 자세한 내용은 [Amazon QLDB에서의 데이터 확인](verification.md)을 참조하세요.

이 단계에서는 `vehicle-registration` 원장의 `VehicleRegistration` 테이블에 있는 문서 개정을 검증합니다. 먼저 다이제스트를 요청합니다. 다이제스트는 출력 파일로 반환되며 원장의 전체 변경 내역에 대한 서명 역할을 합니다. 그런 다음 해당 다이제스트와 관련된 개정 증거를 요청합니다. 이 증거를 사용하면 모든 유효성 검사를 통과한 경우 개정 내용의 무결성을 확인할 수 있습니다.

**문서 개정을 검증하려면**

1. 검증에 필요한 QLDB 객체와 Ion 및 문자열 값에 대한 도우미 메서드가 포함된 유틸리티 클래스를 나타내는 다음 `.java` 파일을 검토하세요.

   1. `BlockAddress.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import java.util.Objects;
      
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import com.fasterxml.jackson.annotation.JsonCreator;
      import com.fasterxml.jackson.annotation.JsonProperty;
      
      /**
       * Represents the BlockAddress field of a QLDB document.
       */
      public final class BlockAddress {
      
          private static final Logger log = LoggerFactory.getLogger(BlockAddress.class);
      
          private final String strandId;
          private final long sequenceNo;
      
          @JsonCreator
          public BlockAddress(@JsonProperty("strandId") final String strandId,
                              @JsonProperty("sequenceNo") final long sequenceNo) {
              this.strandId = strandId;
              this.sequenceNo = sequenceNo;
          }
      
          public long getSequenceNo() {
              return sequenceNo;
          }
      
          public String getStrandId() {
              return strandId;
          }
      
          @Override
          public String toString() {
              return "BlockAddress{"
                      + "strandId='" + strandId + '\''
                      + ", sequenceNo=" + sequenceNo
                      + '}';
          }
      
          @Override
          public boolean equals(final Object o) {
              if (this == o) {
                  return true;
              }
              if (o == null || getClass() != o.getClass()) {
                  return false;
              }
              BlockAddress that = (BlockAddress) o;
              return sequenceNo == that.sequenceNo
                      && strandId.equals(that.strandId);
          }
      
          @Override
          public int hashCode() {
              // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties.
              return Objects.hash(strandId, sequenceNo);
              // CHECKSTYLE:ON
          }
      }
      ```

   1. `Proof.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonReader;
      import com.amazon.ion.IonSystem;
      import com.amazon.ion.system.IonSystemBuilder;
      import com.amazonaws.services.qldb.model.GetRevisionRequest;
      import com.amazonaws.services.qldb.model.GetRevisionResult;
      
      import java.util.ArrayList;
      import java.util.List;
      
      /**
       * A Java representation of the {@link Proof} object.
       * Returned from the {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision(GetRevisionRequest)} api.
       */
      public final class Proof {
          private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
      
          private List<byte[]> internalHashes;
      
          public Proof(final List<byte[]> internalHashes) {
              this.internalHashes = internalHashes;
          }
      
          public List<byte[]> getInternalHashes() {
              return internalHashes;
          }
      
          /**
           * Decodes a {@link Proof} from an ion text String. This ion text is returned in
           * a {@link GetRevisionResult#getProof()}
           *
           * @param ionText
           *              The ion text representing a {@link Proof} object.
           * @return {@link JournalBlock} parsed from the ion text.
           * @throws IllegalStateException if failed to parse the {@link Proof} object from the given ion text.
           */
          public static Proof fromBlob(final String ionText) {
              try {
                  IonReader reader = SYSTEM.newReader(ionText);
                  List<byte[]> list = new ArrayList<>();
                  reader.next();
                  reader.stepIn();
                  while (reader.next() != null) {
                      list.add(reader.newBytes());
                  }
                  return new Proof(list);
              } catch (Exception e) {
                  throw new IllegalStateException("Failed to parse a Proof from byte array");
              }
          }
      }
      ```

   1. `QldbIonUtils.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonReader;
      import com.amazon.ion.IonValue;
      import com.amazon.ionhash.IonHashReader;
      import com.amazon.ionhash.IonHashReaderBuilder;
      import com.amazon.ionhash.MessageDigestIonHasherProvider;
      import software.amazon.qldb.tutorial.Constants;
      
      public class QldbIonUtils {
      
          private static MessageDigestIonHasherProvider ionHasherProvider = new MessageDigestIonHasherProvider("SHA-256");
      
          private QldbIonUtils() {}
      
          /**
           * Builds a hash value from the given {@link IonValue}.
           *
           * @param ionValue
           *              The {@link IonValue} to hash.
           * @return a byte array representing the hash value.
           */
          public static byte[] hashIonValue(final IonValue ionValue) {
              IonReader reader = Constants.SYSTEM.newReader(ionValue);
              IonHashReader hashReader = IonHashReaderBuilder.standard()
                      .withHasherProvider(ionHasherProvider)
                      .withReader(reader)
                      .build();
              while (hashReader.next() != null) {  }
              return hashReader.digest();
          }
      
      }
      ```

   1. `QldbStringUtils.java`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial.qldb;
      
      import com.amazon.ion.IonWriter;
      import com.amazon.ion.system.IonReaderBuilder;
      import com.amazon.ion.system.IonTextWriterBuilder;
      import com.amazonaws.services.qldb.model.GetBlockResult;
      import com.amazonaws.services.qldb.model.GetDigestResult;
      import com.amazonaws.services.qldb.model.ValueHolder;
      
      import java.io.IOException;
      
      /**
       * Helper methods to pretty-print certain QLDB response types.
       */
      public class QldbStringUtils {
      
          private QldbStringUtils() {}
      
          /**
           * Returns the string representation of a given {@link ValueHolder}.
           * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted.
           * Additionally, this method pretty-prints any IonText included in the {@link ValueHolder}.
           *
           * @param valueHolder the {@link ValueHolder} to convert to a String.
           * @return the String representation of the supplied {@link ValueHolder}.
           */
          public static String toUnredactedString(ValueHolder valueHolder) {
              StringBuilder sb = new StringBuilder();
              sb.append("{");
              if (valueHolder.getIonText() != null) {
      
                  sb.append("IonText: ");
                  IonWriter prettyWriter = IonTextWriterBuilder.pretty().build(sb);
                  try {
                      prettyWriter.writeValues(IonReaderBuilder.standard().build(valueHolder.getIonText()));
                  } catch (IOException ioe) {
                      sb.append("**Exception while printing this IonText**");
                  }
              }
      
              sb.append("}");
              return sb.toString();
          }
      
          /**
           * Returns the string representation of a given {@link GetBlockResult}.
           * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted.
           *
           * @param getBlockResult the {@link GetBlockResult} to convert to a String.
           * @return the String representation of the supplied {@link GetBlockResult}.
           */
          public static String toUnredactedString(GetBlockResult getBlockResult) {
              StringBuilder sb = new StringBuilder();
              sb.append("{");
              if (getBlockResult.getBlock() != null) {
                  sb.append("Block: ").append(toUnredactedString(getBlockResult.getBlock())).append(",");
              }
      
              if (getBlockResult.getProof() != null) {
                  sb.append("Proof: ").append(toUnredactedString(getBlockResult.getProof()));
              }
      
              sb.append("}");
              return sb.toString();
          }
      
          /**
           * Returns the string representation of a given {@link GetDigestResult}.
           * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted.
           *
           * @param getDigestResult the {@link GetDigestResult} to convert to a String.
           * @return the String representation of the supplied {@link GetDigestResult}.
           */
          public static String toUnredactedString(GetDigestResult getDigestResult) {
              StringBuilder sb = new StringBuilder();
              sb.append("{");
              if (getDigestResult.getDigest() != null) {
                  sb.append("Digest: ").append(getDigestResult.getDigest()).append(",");
              }
      
              if (getDigestResult.getDigestTipAddress() != null) {
                  sb.append("DigestTipAddress: ").append(toUnredactedString(getDigestResult.getDigestTipAddress()));
              }
      
              sb.append("}");
              return sb.toString();
          }
      }
      ```

   1. `Verifier.java`

------
#### [ 2.x ]

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial;
      
      import java.nio.ByteBuffer;
      import java.nio.charset.StandardCharsets;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.util.ArrayList;
      import java.util.Arrays;
      import java.util.Comparator;
      import java.util.Iterator;
      import java.util.List;
      import java.util.concurrent.ThreadLocalRandom;
      
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import com.amazonaws.util.Base64;
      
      import software.amazon.qldb.tutorial.qldb.Proof;
      
      /**
       * Encapsulates the logic to verify the integrity of revisions or blocks in a QLDB ledger.
       *
       * The main entry point is {@link #verify(byte[], byte[], String)}.
       *
       * This code expects that you have AWS credentials setup per:
       * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
       */
      public final class Verifier {
          public static final Logger log = LoggerFactory.getLogger(Verifier.class);
          private static final int HASH_LENGTH = 32;
          private static final int UPPER_BOUND = 8;
      
          /**
           * Compares two hashes by their <em>signed</em> byte values in little-endian order.
           */
          private static Comparator<byte[]> hashComparator = (h1, h2) -> {
              if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
                  throw new IllegalArgumentException("Invalid hash.");
              }
              for (int i = h1.length - 1; i >= 0; i--) {
                  int byteEqual = Byte.compare(h1[i], h2[i]);
                  if (byteEqual != 0) {
                      return byteEqual;
                  }
              }
      
              return 0;
          };
      
          private Verifier() { }
      
          /**
           * Verify the integrity of a document with respect to a QLDB ledger digest.
           *
           * The verification algorithm includes the following steps:
           *
           * 1. {@link #buildCandidateDigest(Proof, byte[])} build the candidate digest from the internal hashes
           * in the {@link Proof}.
           * 2. Check that the {@code candidateLedgerDigest} is equal to the {@code ledgerDigest}.
           *
           * @param documentHash
           *              The hash of the document to be verified.
           * @param digest
           *              The QLDB ledger digest. This digest should have been retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getDigest}
           * @param proofBlob
           *              The ion encoded bytes representing the {@link Proof} associated with the supplied
           *              {@code digestTipAddress} and {@code address} retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @return {@code true} if the record is verified or {@code false} if it is not verified.
           */
          public static boolean verify(
                  final byte[] documentHash,
                  final byte[] digest,
                  final String proofBlob
          ) {
              Proof proof = Proof.fromBlob(proofBlob);
      
              byte[] candidateDigest = buildCandidateDigest(proof, documentHash);
      
              return Arrays.equals(digest, candidateDigest);
          }
      
          /**
           * Build the candidate digest representing the entire ledger from the internal hashes of the {@link Proof}.
           *
           * @param proof
           *              A Java representation of {@link Proof}
           *              returned from {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @param leafHash
           *              Leaf hash to build the candidate digest with.
           * @return a byte array of the candidate digest.
           */
          private static byte[] buildCandidateDigest(final Proof proof, final byte[] leafHash) {
              return calculateRootHashFromInternalHashes(proof.getInternalHashes(), leafHash);
          }
      
          /**
           * Get a new instance of {@link MessageDigest} using the SHA-256 algorithm.
           *
           * @return an instance of {@link MessageDigest}.
           * @throws IllegalStateException if the algorithm is not available on the current JVM.
           */
          static MessageDigest newMessageDigest() {
              try {
                  return MessageDigest.getInstance("SHA-256");
              } catch (NoSuchAlgorithmException e) {
                  log.error("Failed to create SHA-256 MessageDigest", e);
                  throw new IllegalStateException("SHA-256 message digest is unavailable", e);
              }
          }
      
          /**
           * Takes two hashes, sorts them, concatenates them, and then returns the
           * hash of the concatenated array.
           *
           * @param h1
           *              Byte array containing one of the hashes to compare.
           * @param h2
           *              Byte array containing one of the hashes to compare.
           * @return the concatenated array of hashes.
           */
          public static byte[] dot(final byte[] h1, final byte[] h2) {
              if (h1.length == 0) {
                  return h2;
              }
              if (h2.length == 0) {
                  return h1;
              }
              byte[] concatenated = new byte[h1.length + h2.length];
              if (hashComparator.compare(h1, h2) < 0) {
                  System.arraycopy(h1, 0, concatenated, 0, h1.length);
                  System.arraycopy(h2, 0, concatenated, h1.length, h2.length);
              } else {
                  System.arraycopy(h2, 0, concatenated, 0, h2.length);
                  System.arraycopy(h1, 0, concatenated, h2.length, h1.length);
              }
              MessageDigest messageDigest = newMessageDigest();
              messageDigest.update(concatenated);
      
              return messageDigest.digest();
          }
      
          /**
           * Starting with the provided {@code leafHash} combined with the provided {@code internalHashes}
           * pairwise until only the root hash remains.
           *
           * @param internalHashes
           *              Internal hashes of Merkle tree.
           * @param leafHash
           *              Leaf hashes of Merkle tree.
           * @return the root hash.
           */
          private static byte[] calculateRootHashFromInternalHashes(final List<byte[]> internalHashes, final byte[] leafHash) {
              return internalHashes.stream().reduce(leafHash, Verifier::dot);
          }
      
          /**
           * Flip a single random bit in the given byte array. This method is used to demonstrate
           * QLDB's verification features.
           *
           * @param original
           *              The original byte array.
           * @return the altered byte array with a single random bit changed.
           */
          public static byte[] flipRandomBit(final byte[] original) {
              if (original.length == 0) {
                  throw new IllegalArgumentException("Array cannot be empty!");
              }
              int alteredPosition = ThreadLocalRandom.current().nextInt(original.length);
              int b = ThreadLocalRandom.current().nextInt(UPPER_BOUND);
              byte[] altered = new byte[original.length];
              System.arraycopy(original, 0, altered, 0, original.length);
              altered[alteredPosition] = (byte) (altered[alteredPosition] ^ (1 << b));
              return altered;
          }
      
          public static String toBase64(byte[] arr) {
              return new String(Base64.encode(arr), StandardCharsets.UTF_8);
          }
      
          /**
           * Convert a {@link ByteBuffer} into byte array.
           *
           * @param buffer
           *              The {@link ByteBuffer} to convert.
           * @return the converted byte array.
           */
          public static byte[] convertByteBufferToByteArray(final ByteBuffer buffer) {
              byte[] arr = new byte[buffer.remaining()];
              buffer.get(arr);
              return arr;
          }
      
          /**
           * Calculates the root hash from a list of hashes that represent the base of a Merkle tree.
           *
           * @param hashes
           *              The list of byte arrays representing hashes making up base of a Merkle tree.
           * @return a byte array that is the root hash of the given list of hashes.
           */
          public static byte[] calculateMerkleTreeRootHash(List<byte[]> hashes) {
              if (hashes.isEmpty()) {
                  return new byte[0];
              }
      
              List<byte[]> remaining = combineLeafHashes(hashes);
              while (remaining.size() > 1) {
                  remaining = combineLeafHashes(remaining);
              }
              return remaining.get(0);
          }
      
          private static List<byte[]> combineLeafHashes(List<byte[]> hashes) {
              List<byte[]> combinedHashes = new ArrayList<>();
              Iterator<byte[]> it = hashes.stream().iterator();
      
              while (it.hasNext()) {
                  byte[] left = it.next();
                  if (it.hasNext()) {
                      byte[] right = it.next();
                      byte[] combined = dot(left, right);
                      combinedHashes.add(combined);
                  } else {
                      combinedHashes.add(left);
                  }
              }
      
              return combinedHashes;
          }
      }
      ```

------
#### [ 1.x ]

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      package software.amazon.qldb.tutorial;
      
      import com.amazonaws.util.Base64;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import software.amazon.qldb.tutorial.qldb.Proof;
      
      import java.nio.ByteBuffer;
      import java.nio.charset.StandardCharsets;
      import java.security.MessageDigest;
      import java.security.NoSuchAlgorithmException;
      import java.util.*;
      import java.util.concurrent.ThreadLocalRandom;
      
      /**
       * Encapsulates the logic to verify the integrity of revisions or blocks in a QLDB ledger.
       *
       * The main entry point is {@link #verify(byte[], byte[], String)}.
       *
       * This code expects that you have AWS credentials setup per:
       * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
       */
      public final class Verifier {
          public static final Logger log = LoggerFactory.getLogger(Verifier.class);
          private static final int HASH_LENGTH = 32;
          private static final int UPPER_BOUND = 8;
      
          /**
           * Compares two hashes by their <em>signed</em> byte values in little-endian order.
           */
          private static Comparator<byte[]> hashComparator = (h1, h2) -> {
              if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
                  throw new IllegalArgumentException("Invalid hash.");
              }
              for (int i = h1.length - 1; i >= 0; i--) {
                  int byteEqual = Byte.compare(h1[i], h2[i]);
                  if (byteEqual != 0) {
                      return byteEqual;
                  }
              }
      
              return 0;
          };
      
          private Verifier() { }
      
          /**
           * Verify the integrity of a document with respect to a QLDB ledger digest.
           *
           * The verification algorithm includes the following steps:
           *
           * 1. {@link #buildCandidateDigest(Proof, byte[])} build the candidate digest from the internal hashes
           * in the {@link Proof}.
           * 2. Check that the {@code candidateLedgerDigest} is equal to the {@code ledgerDigest}.
           *
           * @param documentHash
           *              The hash of the document to be verified.
           * @param digest
           *              The QLDB ledger digest. This digest should have been retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getDigest}
           * @param proofBlob
           *              The ion encoded bytes representing the {@link Proof} associated with the supplied
           *              {@code digestTipAddress} and {@code address} retrieved using
           *              {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @return {@code true} if the record is verified or {@code false} if it is not verified.
           */
          public static boolean verify(
                  final byte[] documentHash,
                  final byte[] digest,
                  final String proofBlob
          ) {
              Proof proof = Proof.fromBlob(proofBlob);
      
              byte[] candidateDigest = buildCandidateDigest(proof, documentHash);
      
              return Arrays.equals(digest, candidateDigest);
          }
      
          /**
           * Build the candidate digest representing the entire ledger from the internal hashes of the {@link Proof}.
           *
           * @param proof
           *              A Java representation of {@link Proof}
           *              returned from {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}.
           * @param leafHash
           *              Leaf hash to build the candidate digest with.
           * @return a byte array of the candidate digest.
           */
          private static byte[] buildCandidateDigest(final Proof proof, final byte[] leafHash) {
              return calculateRootHashFromInternalHashes(proof.getInternalHashes(), leafHash);
          }
      
          /**
           * Get a new instance of {@link MessageDigest} using the SHA-256 algorithm.
           *
           * @return an instance of {@link MessageDigest}.
           * @throws IllegalStateException if the algorithm is not available on the current JVM.
           */
          static MessageDigest newMessageDigest() {
              try {
                  return MessageDigest.getInstance("SHA-256");
              } catch (NoSuchAlgorithmException e) {
                  log.error("Failed to create SHA-256 MessageDigest", e);
                  throw new IllegalStateException("SHA-256 message digest is unavailable", e);
              }
          }
      
          /**
           * Takes two hashes, sorts them, concatenates them, and then returns the
           * hash of the concatenated array.
           *
           * @param h1
           *              Byte array containing one of the hashes to compare.
           * @param h2
           *              Byte array containing one of the hashes to compare.
           * @return the concatenated array of hashes.
           */
          public static byte[] dot(final byte[] h1, final byte[] h2) {
              if (h1.length == 0) {
                  return h2;
              }
              if (h2.length == 0) {
                  return h1;
              }
              byte[] concatenated = new byte[h1.length + h2.length];
              if (hashComparator.compare(h1, h2) < 0) {
                  System.arraycopy(h1, 0, concatenated, 0, h1.length);
                  System.arraycopy(h2, 0, concatenated, h1.length, h2.length);
              } else {
                  System.arraycopy(h2, 0, concatenated, 0, h2.length);
                  System.arraycopy(h1, 0, concatenated, h2.length, h1.length);
              }
              MessageDigest messageDigest = newMessageDigest();
              messageDigest.update(concatenated);
      
              return messageDigest.digest();
          }
      
          /**
           * Starting with the provided {@code leafHash} combined with the provided {@code internalHashes}
           * pairwise until only the root hash remains.
           *
           * @param internalHashes
           *              Internal hashes of Merkle tree.
           * @param leafHash
           *              Leaf hashes of Merkle tree.
           * @return the root hash.
           */
          private static byte[] calculateRootHashFromInternalHashes(final List<byte[]> internalHashes, final byte[] leafHash) {
              return internalHashes.stream().reduce(leafHash, Verifier::dot);
          }
      
          /**
           * Flip a single random bit in the given byte array. This method is used to demonstrate
           * QLDB's verification features.
           *
           * @param original
           *              The original byte array.
           * @return the altered byte array with a single random bit changed.
           */
          public static byte[] flipRandomBit(final byte[] original) {
              if (original.length == 0) {
                  throw new IllegalArgumentException("Array cannot be empty!");
              }
              int alteredPosition = ThreadLocalRandom.current().nextInt(original.length);
              int b = ThreadLocalRandom.current().nextInt(UPPER_BOUND);
              byte[] altered = new byte[original.length];
              System.arraycopy(original, 0, altered, 0, original.length);
              altered[alteredPosition] = (byte) (altered[alteredPosition] ^ (1 << b));
              return altered;
          }
      
          public static String toBase64(byte[] arr) {
              return new String(Base64.encode(arr), StandardCharsets.UTF_8);
          }
      
          /**
           * Convert a {@link ByteBuffer} into byte array.
           *
           * @param buffer
           *              The {@link ByteBuffer} to convert.
           * @return the converted byte array.
           */
          public static byte[] convertByteBufferToByteArray(final ByteBuffer buffer) {
              byte[] arr = new byte[buffer.remaining()];
              buffer.get(arr);
              return arr;
          }
      
          /**
           * Calculates the root hash from a list of hashes that represent the base of a Merkle tree.
           *
           * @param hashes
           *              The list of byte arrays representing hashes making up base of a Merkle tree.
           * @return a byte array that is the root hash of the given list of hashes.
           */
          public static byte[] calculateMerkleTreeRootHash(List<byte[]> hashes) {
              if (hashes.isEmpty()) {
                  return new byte[0];
              }
      
              List<byte[]> remaining = combineLeafHashes(hashes);
              while (remaining.size() > 1) {
                  remaining = combineLeafHashes(remaining);
              }
              return remaining.get(0);
          }
      
          private static List<byte[]> combineLeafHashes(List<byte[]> hashes) {
              List<byte[]> combinedHashes = new ArrayList<>();
              Iterator<byte[]> it = hashes.stream().iterator();
      
              while (it.hasNext()) {
                  byte[] left = it.next();
                  if (it.hasNext()) {
                      byte[] right = it.next();
                      byte[] combined = dot(left, right);
                      combinedHashes.add(combined);
                  } else {
                      combinedHashes.add(left);
                  }
              }
      
              return combinedHashes;
          }
      }
      ```

------

1. 두 개의 `.java` 파일(`GetDigest.java` 및 `GetRevision.java`)을 사용하여 다음 단계를 수행하세요.
   + `vehicle-registration` 원장에 새 다이제스트를 요청하세요.
   + `VehicleRegistration` 테이블에 있는 문서의 각 개정에 대한 증거를 요청합니다.
   + 반환된 다이제스트와 증거를 사용하여 다이제스트를 다시 계산하여 개정 내용을 검증합니다.

   `GetDigest.java` 프로그램에는 다음 코드가 포함되어 있습니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.GetDigestRequest;
   import com.amazonaws.services.qldb.model.GetDigestResult;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.tutorial.qldb.QldbStringUtils;
   
   /**
    * This is an example for retrieving the digest of a particular ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class GetDigest {
       public static final Logger log = LoggerFactory.getLogger(GetDigest.class);
       public static AmazonQLDB client = CreateLedger.getClient();
   
       private GetDigest() { }
   
       /**
        * Calls {@link #getDigest(String)} for a ledger.
        *
        * @param args
        *              Arbitrary command-line arguments.
        * @throws Exception if failed to get a ledger digest.
        */
       public static void main(final String... args) throws Exception {
           try {
   
               getDigest(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to get a ledger digest!", e);
               throw e;
           }
       }
   
       /**
        * Get the digest for the specified ledger.
        *
        * @param ledgerName
        *              The ledger to get digest from.
        * @return {@link GetDigestResult}.
        */
       public static GetDigestResult getDigest(final String ledgerName) {
           log.info("Let's get the current digest of the ledger named {}.", ledgerName);
           GetDigestRequest request = new GetDigestRequest()
                   .withName(ledgerName);
           GetDigestResult result = client.getDigest(request);
           log.info("Success. LedgerDigest: {}.", QldbStringUtils.toUnredactedString(result));
           return result;
       }
   }
   ```
**참고**  
`getDigest` 메서드를 사용하여 원장에 있는 저널의 현재 *팁*을 포함하는 다이제스트를 요청합니다. 저널 팁은 QLDB가 요청을 수신한 시점을 기준으로 가장 최근에 커밋된 블록을 나타냅니다.

   `GetRevision.java` 프로그램에는 다음 코드가 포함되어 있습니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonSystem;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.IonWriter;
   import com.amazon.ion.system.IonReaderBuilder;
   import com.amazon.ion.system.IonSystemBuilder;
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.GetDigestResult;
   import com.amazonaws.services.qldb.model.GetRevisionRequest;
   import com.amazonaws.services.qldb.model.GetRevisionResult;
   import com.amazonaws.services.qldb.model.ValueHolder;
   import java.io.ByteArrayOutputStream;
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbDriver;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.qldb.BlockAddress;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   import software.amazon.qldb.tutorial.qldb.QldbStringUtils;
   
   /**
    * Verify the integrity of a document revision in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class GetRevision {
       public static final Logger log = LoggerFactory.getLogger(GetRevision.class);
       public static AmazonQLDB client = CreateLedger.getClient();
       private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
   
       private GetRevision() { }
   
       public static void main(String... args) throws Exception {
   
           final String vin = SampleData.REGISTRATIONS.get(0).getVin();
   
   
           verifyRegistration(ConnectToLedger.getDriver(), Constants.LEDGER_NAME, vin);
       }
   
       /**
        * Verify each version of the registration for the given VIN.
        *
        * @param driver
        *              A QLDB driver.
        * @param ledgerName
        *              The ledger to get digest from.
        * @param vin
        *              VIN to query the revision history of a specific registration with.
        * @throws Exception if failed to verify digests.
        * @throws AssertionError if document revision verification failed.
        */
       public static void verifyRegistration(final QldbDriver driver, final String ledgerName, final String vin)
               throws Exception {
           log.info(String.format("Let's verify the registration with VIN=%s, in ledger=%s.", vin, ledgerName));
   
           try {
               log.info("First, let's get a digest.");
               GetDigestResult digestResult = GetDigest.getDigest(ledgerName);
   
               ValueHolder digestTipAddress = digestResult.getDigestTipAddress();
               byte[] digestBytes = Verifier.convertByteBufferToByteArray(digestResult.getDigest());
   
               log.info("Got a ledger digest. Digest end address={}, digest={}.",
                   QldbStringUtils.toUnredactedString(digestTipAddress),
                   Verifier.toBase64(digestBytes));
   
               log.info(String.format("Next, let's query the registration with VIN=%s. "
                       + "Then we can verify each version of the registration.", vin));
               List<IonStruct> documentsWithMetadataList = new ArrayList<>();
               driver.execute(txn -> {
                   documentsWithMetadataList.addAll(queryRegistrationsByVin(txn, vin));
               });
               log.info("Registrations queried successfully!");
   
               log.info(String.format("Found %s revisions of the registration with VIN=%s.",
                       documentsWithMetadataList.size(), vin));
   
               for (IonStruct ionStruct : documentsWithMetadataList) {
   
                   QldbRevision document = QldbRevision.fromIon(ionStruct);
                   log.info(String.format("Let's verify the document: %s", document));
   
                   log.info("Let's get a proof for the document.");
                   GetRevisionResult proofResult = getRevision(
                           ledgerName,
                           document.getMetadata().getId(),
                           digestTipAddress,
                           document.getBlockAddress()
                   );
   
                   final IonValue proof = Constants.MAPPER.writeValueAsIonValue(proofResult.getProof());
                   final IonReader reader = IonReaderBuilder.standard().build(proof);
                   reader.next();
                   ByteArrayOutputStream baos = new ByteArrayOutputStream();
                   IonWriter writer = SYSTEM.newBinaryWriter(baos);
                   writer.writeValue(reader);
                   writer.close();
                   baos.flush();
                   baos.close();
                   byte[] byteProof = baos.toByteArray();
   
                   log.info(String.format("Got back a proof: %s", Verifier.toBase64(byteProof)));
   
                   boolean verified = Verifier.verify(
                           document.getHash(),
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (!verified) {
                       throw new AssertionError("Document revision is not verified!");
                   } else {
                       log.info("Success! The document is verified");
                   }
   
                   byte[] alteredDigest = Verifier.flipRandomBit(digestBytes);
                   log.info(String.format("Flipping one bit in the digest and assert that the document is NOT verified. "
                           + "The altered digest is: %s", Verifier.toBase64(alteredDigest)));
                   verified = Verifier.verify(
                           document.getHash(),
                           alteredDigest,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected document to not be verified against altered digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the digest causes verification to fail.");
                   }
   
                   byte[] alteredDocumentHash = Verifier.flipRandomBit(document.getHash());
                   log.info(String.format("Flipping one bit in the document's hash and assert that it is NOT verified. "
                           + "The altered document hash is: %s.", Verifier.toBase64(alteredDocumentHash)));
                   verified = Verifier.verify(
                           alteredDocumentHash,
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected altered document hash to not be verified against digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the document hash causes verification to fail.");
                   }
               }
   
           } catch (Exception e) {
               log.error("Failed to verify digests.", e);
               throw e;
           }
   
           log.info(String.format("Finished verifying the registration with VIN=%s in ledger=%s.", vin, ledgerName));
       }
   
       /**
        * Get the revision of a particular document specified by the given document ID and block address.
        *
        * @param ledgerName
        *              Name of the ledger containing the document.
        * @param documentId
        *              Unique ID for the document to be verified, contained in the committed view of the document.
        * @param digestTipAddress
        *              The latest block location covered by the digest.
        * @param blockAddress
        *              The location of the block to request.
        * @return the requested revision.
        */
       public static GetRevisionResult getRevision(final String ledgerName, final String documentId,
                                                   final ValueHolder digestTipAddress, final BlockAddress blockAddress) {
           try {
               GetRevisionRequest request = new GetRevisionRequest()
                       .withName(ledgerName)
                       .withDigestTipAddress(digestTipAddress)
                       .withBlockAddress(new ValueHolder().withIonText(Constants.MAPPER.writeValueAsIonValue(blockAddress)
                               .toString()))
                       .withDocumentId(documentId);
               return client.getRevision(request);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Query the registration history for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              The unique VIN to query.
        * @return a list of {@link IonStruct} representing the registration history.
        * @throws IllegalStateException if failed to convert parameters into {@link IonValue}
        */
       public static List<IonStruct> queryRegistrationsByVin(final TransactionExecutor txn, final String vin) {
           log.info(String.format("Let's query the 'VehicleRegistration' table for VIN: %s...", vin));
           log.info("Let's query the 'VehicleRegistration' table for VIN: {}...", vin);
           final String query = String.format("SELECT * FROM _ql_committed_%s WHERE data.VIN = ?",
                   Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               List<IonStruct> list = ScanTable.toIonStructs(result);
               log.info(String.format("Found %d document(s)!", list.size()));
               return list;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazon.ion.IonReader;
   import com.amazon.ion.IonStruct;
   import com.amazon.ion.IonValue;
   import com.amazon.ion.IonWriter;
   import com.amazon.ion.system.IonReaderBuilder;
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.GetDigestResult;
   import com.amazonaws.services.qldb.model.GetRevisionRequest;
   import com.amazonaws.services.qldb.model.GetRevisionResult;
   import com.amazonaws.services.qldb.model.ValueHolder;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.QldbSession;
   import software.amazon.qldb.Result;
   import software.amazon.qldb.TransactionExecutor;
   import software.amazon.qldb.tutorial.model.SampleData;
   import software.amazon.qldb.tutorial.qldb.BlockAddress;
   import software.amazon.qldb.tutorial.qldb.QldbRevision;
   import software.amazon.qldb.tutorial.qldb.QldbStringUtils;
   
   import java.io.ByteArrayOutputStream;
   import java.io.IOException;
   import java.util.ArrayList;
   import java.util.Collections;
   import java.util.List;
   
   /**
    * Verify the integrity of a document revision in a QLDB ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class GetRevision {
       public static final Logger log = LoggerFactory.getLogger(GetRevision.class);
       public static AmazonQLDB client = CreateLedger.getClient();
   
       private GetRevision() { }
   
       public static void main(String... args) throws Exception {
   
           final String vin = SampleData.REGISTRATIONS.get(0).getVin();
   
           try (QldbSession qldbSession = ConnectToLedger.createQldbSession()) {
               verifyRegistration(qldbSession, Constants.LEDGER_NAME, vin);
           }
       }
   
       /**
        * Verify each version of the registration for the given VIN.
        *
        * @param qldbSession
        *              A QLDB session.
        * @param ledgerName
        *              The ledger to get digest from.
        * @param vin
        *              VIN to query the revision history of a specific registration with.
        * @throws Exception if failed to verify digests.
        * @throws AssertionError if document revision verification failed.
        */
       public static void verifyRegistration(final QldbSession qldbSession, final String ledgerName, final String vin)
               throws Exception {
           log.info(String.format("Let's verify the registration with VIN=%s, in ledger=%s.", vin, ledgerName));
   
           try {
               log.info("First, let's get a digest.");
               GetDigestResult digestResult = GetDigest.getDigest(ledgerName);
   
               ValueHolder digestTipAddress = digestResult.getDigestTipAddress();
               byte[] digestBytes = Verifier.convertByteBufferToByteArray(digestResult.getDigest());
   
               log.info("Got a ledger digest. Digest end address={}, digest={}.",
                   QldbStringUtils.toUnredactedString(digestTipAddress),
                   Verifier.toBase64(digestBytes));
   
               log.info(String.format("Next, let's query the registration with VIN=%s. "
                       + "Then we can verify each version of the registration.", vin));
               List<IonStruct> documentsWithMetadataList = new ArrayList<>();
               qldbSession.execute(txn -> {
                   documentsWithMetadataList.addAll(queryRegistrationsByVin(txn, vin));
               }, (retryAttempt) -> log.info("Retrying due to OCC conflict..."));
               log.info("Registrations queried successfully!");
   
               log.info(String.format("Found %s revisions of the registration with VIN=%s.",
                       documentsWithMetadataList.size(), vin));
   
               for (IonStruct ionStruct : documentsWithMetadataList) {
   
                   QldbRevision document = QldbRevision.fromIon(ionStruct);
                   log.info(String.format("Let's verify the document: %s", document));
   
                   log.info("Let's get a proof for the document.");
                   GetRevisionResult proofResult = getRevision(
                           ledgerName,
                           document.getMetadata().getId(),
                           digestTipAddress,
                           document.getBlockAddress()
                   );
   
                   final IonValue proof = Constants.MAPPER.writeValueAsIonValue(proofResult.getProof());
                   final IonReader reader = IonReaderBuilder.standard().build(proof);
                   reader.next();
                   ByteArrayOutputStream baos = new ByteArrayOutputStream();
                   IonWriter writer = Constants.SYSTEM.newBinaryWriter(baos);
                   writer.writeValue(reader);
                   writer.close();
                   baos.flush();
                   baos.close();
                   byte[] byteProof = baos.toByteArray();
   
                   log.info(String.format("Got back a proof: %s", Verifier.toBase64(byteProof)));
   
                   boolean verified = Verifier.verify(
                           document.getHash(),
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (!verified) {
                       throw new AssertionError("Document revision is not verified!");
                   } else {
                       log.info("Success! The document is verified");
                   }
   
                   byte[] alteredDigest = Verifier.flipRandomBit(digestBytes);
                   log.info(String.format("Flipping one bit in the digest and assert that the document is NOT verified. "
                           + "The altered digest is: %s", Verifier.toBase64(alteredDigest)));
                   verified = Verifier.verify(
                           document.getHash(),
                           alteredDigest,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected document to not be verified against altered digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the digest causes verification to fail.");
                   }
   
                   byte[] alteredDocumentHash = Verifier.flipRandomBit(document.getHash());
                   log.info(String.format("Flipping one bit in the document's hash and assert that it is NOT verified. "
                           + "The altered document hash is: %s.", Verifier.toBase64(alteredDocumentHash)));
                   verified = Verifier.verify(
                           alteredDocumentHash,
                           digestBytes,
                           proofResult.getProof().getIonText()
                   );
   
                   if (verified) {
                       throw new AssertionError("Expected altered document hash to not be verified against digest.");
                   } else {
                       log.info("Success! As expected flipping a bit in the document hash causes verification to fail.");
                   }
               }
   
           } catch (Exception e) {
               log.error("Failed to verify digests.", e);
               throw e;
           }
   
           log.info(String.format("Finished verifying the registration with VIN=%s in ledger=%s.", vin, ledgerName));
       }
   
       /**
        * Get the revision of a particular document specified by the given document ID and block address.
        *
        * @param ledgerName
        *              Name of the ledger containing the document.
        * @param documentId
        *              Unique ID for the document to be verified, contained in the committed view of the document.
        * @param digestTipAddress
        *              The latest block location covered by the digest.
        * @param blockAddress
        *              The location of the block to request.
        * @return the requested revision.
        */
       public static GetRevisionResult getRevision(final String ledgerName, final String documentId,
                                                   final ValueHolder digestTipAddress, final BlockAddress blockAddress) {
           try {
               GetRevisionRequest request = new GetRevisionRequest()
                       .withName(ledgerName)
                       .withDigestTipAddress(digestTipAddress)
                       .withBlockAddress(new ValueHolder().withIonText(Constants.MAPPER.writeValueAsIonValue(blockAddress)
                               .toString()))
                       .withDocumentId(documentId);
               return client.getRevision(request);
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   
       /**
        * Query the registration history for the given VIN.
        *
        * @param txn
        *              The {@link TransactionExecutor} for lambda execute.
        * @param vin
        *              The unique VIN to query.
        * @return a list of {@link IonStruct} representing the registration history.
        * @throws IllegalStateException if failed to convert parameters into {@link IonValue}
        */
       public static List<IonStruct> queryRegistrationsByVin(final TransactionExecutor txn, final String vin) {
           log.info(String.format("Let's query the 'VehicleRegistration' table for VIN: %s...", vin));
           log.info("Let's query the 'VehicleRegistration' table for VIN: {}...", vin);
           final String query = String.format("SELECT * FROM _ql_committed_%s WHERE data.VIN = ?",
                   Constants.VEHICLE_REGISTRATION_TABLE_NAME);
           try {
               final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin));
               final Result result = txn.execute(query, parameters);
               List<IonStruct> list = ScanTable.toIonStructs(result);
               log.info(String.format("Found %d document(s)!", list.size()));
               return list;
           } catch (IOException ioe) {
               throw new IllegalStateException(ioe);
           }
       }
   }
   ```

------
**참고**  
`getRevision` 메서드가 지정된 문서 개정에 대한 증거를 반환하면 이 프로그램은 클라이언트 측 API를 사용하여 해당 개정을 검증합니다. 이 API에서 사용하는 알고리즘에 대한 개요는 [증명을 사용하여 다이제스트를 다시 계산하기](verification.results.md#verification.results.recalc) 섹션을 참조하세요.

1. `GetRevision.java` 프로그램을 컴파일하고 실행하여 VIN `1N4AL11D75C109151`으로 `VehicleRegistration` 문서를 암호학적으로 검증합니다.

`vehicle-registration` 원장의 저널 데이터를 내보내고 검증하려면 [8단계: 원장의 저널 데이터 내보내기 및 검증](getting-started.java.step-8.md)로 이동하세요.

# 8단계: 원장의 저널 데이터 내보내기 및 검증
<a name="getting-started.java.step-8"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB에서는 데이터 보존, 분석 및 감사와 같은 다양한 목적으로 원장의 저널 콘텐츠에 액세스할 수 있습니다. 자세한 내용은 [Amazon QLDB에서 저널 데이터 내보내기](export-journal.md) 단원을 참조하십시오.

이 단계에서는 `vehicle-registration` 원장의 [저널 블록](journal-contents.md)을 Amazon S3 버킷으로 내보냅니다. 그런 다음 내보낸 데이터를 사용하여 저널 블록과 각 블록 내 개별 해시 구성 요소 간의 해시 체인을 검증합니다.

사용하는 AWS Identity and Access Management (IAM) 보안 주체 엔터티에는에서 Amazon S3 버킷을 생성할 수 있는 충분한 IAM 권한이 있어야 합니다 AWS 계정. 자세한 내용은 *Amazon S3 사용 설명서*의 [Amazon S3의 정책 및 권한](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html)을 참조하세요. 또한 QLDB가 Amazon S3 버킷에 객체를 쓸 수 있도록 허용하는 권한 정책이 첨부된 IAM 역할을 생성할 권한이 있어야 합니다. 자세한 내용은 *IAM 사용 설명서*에서 [IAM 리소스에 액세스하는 데 필요한 권한](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_permissions-required.html)을 참조하세요.

**저널 데이터를 내보내고 검증하려면**

1. 저널 블록과 해당 데이터 내용을 나타내는 다음 파일(`JournalBlock.java`)을 검토하세요. 여기에는 블록 해시의 각 개별 구성 요소를 계산하는 방법을 보여주는 `verifyBlockHash()`라는 이름의 메서드가 포함되어 있습니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial.qldb;
   
   import com.fasterxml.jackson.annotation.JsonCreator;
   import com.fasterxml.jackson.annotation.JsonProperty;
   import com.fasterxml.jackson.databind.annotation.JsonSerialize;
   import com.fasterxml.jackson.dataformat.ion.IonTimestampSerializers;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import software.amazon.qldb.tutorial.Constants;
   import software.amazon.qldb.tutorial.Verifier;
   
   import java.io.IOException;
   import java.nio.ByteBuffer;
   import java.util.Arrays;
   import java.util.Date;
   import java.util.HashSet;
   import java.util.List;
   import java.util.Set;
   import java.util.stream.Collectors;
   
   import static java.nio.ByteBuffer.wrap;
   
   /**
    * Represents a JournalBlock that was recorded after executing a transaction
    * in the ledger.
    */
   public final class JournalBlock {
       private static final Logger log = LoggerFactory.getLogger(JournalBlock.class);
   
       private BlockAddress blockAddress;
       private String transactionId;
       @JsonSerialize(using = IonTimestampSerializers.IonTimestampJavaDateSerializer.class)
       private Date blockTimestamp;
       private byte[] blockHash;
       private byte[] entriesHash;
       private byte[] previousBlockHash;
       private byte[][] entriesHashList;
       private TransactionInfo transactionInfo;
       private RedactionInfo redactionInfo;
       private List<QldbRevision> revisions;
   
       @JsonCreator
       public JournalBlock(@JsonProperty("blockAddress") final BlockAddress blockAddress,
                           @JsonProperty("transactionId") final String transactionId,
                           @JsonProperty("blockTimestamp") final Date blockTimestamp,
                           @JsonProperty("blockHash") final byte[] blockHash,
                           @JsonProperty("entriesHash") final byte[] entriesHash,
                           @JsonProperty("previousBlockHash") final byte[] previousBlockHash,
                           @JsonProperty("entriesHashList") final byte[][] entriesHashList,
                           @JsonProperty("transactionInfo") final TransactionInfo transactionInfo,
                           @JsonProperty("redactionInfo") final RedactionInfo redactionInfo,
                           @JsonProperty("revisions") final List<QldbRevision> revisions) {
           this.blockAddress = blockAddress;
           this.transactionId = transactionId;
           this.blockTimestamp = blockTimestamp;
           this.blockHash = blockHash;
           this.entriesHash = entriesHash;
           this.previousBlockHash = previousBlockHash;
           this.entriesHashList = entriesHashList;
           this.transactionInfo = transactionInfo;
           this.redactionInfo = redactionInfo;
           this.revisions = revisions;
       }
   
       public BlockAddress getBlockAddress() {
           return blockAddress;
       }
   
       public String getTransactionId() {
           return transactionId;
       }
   
       public Date getBlockTimestamp() {
           return blockTimestamp;
       }
   
       public byte[][] getEntriesHashList() {
           return entriesHashList;
       }
   
       public TransactionInfo getTransactionInfo() {
           return transactionInfo;
       }
   
       public RedactionInfo getRedactionInfo() {
           return redactionInfo;
       }
   
       public List<QldbRevision> getRevisions() {
           return revisions;
       }
   
       public byte[] getEntriesHash() {
           return entriesHash;
       }
   
       public byte[] getBlockHash() {
           return blockHash;
       }
   
       public byte[] getPreviousBlockHash() {
           return previousBlockHash;
       }
   
       @Override
       public String toString() {
           return "JournalBlock{"
                   + "blockAddress=" + blockAddress
                   + ", transactionId='" + transactionId + '\''
                   + ", blockTimestamp=" + blockTimestamp
                   + ", blockHash=" + Arrays.toString(blockHash)
                   + ", entriesHash=" + Arrays.toString(entriesHash)
                   + ", previousBlockHash=" + Arrays.toString(previousBlockHash)
                   + ", entriesHashList=" + Arrays.toString(entriesHashList)
                   + ", transactionInfo=" + transactionInfo
                   + ", redactionInfo=" + redactionInfo
                   + ", revisions=" + revisions
                   + '}';
       }
   
       @Override
       public boolean equals(final Object o) {
           if (this == o) {
               return true;
           }
           if (!(o instanceof JournalBlock)) {
               return false;
           }
   
           final JournalBlock that = (JournalBlock) o;
   
           if (!getBlockAddress().equals(that.getBlockAddress())) {
               return false;
           }
           if (!getTransactionId().equals(that.getTransactionId())) {
               return false;
           }
           if (!getBlockTimestamp().equals(that.getBlockTimestamp())) {
               return false;
           }
           if (!Arrays.equals(getBlockHash(), that.getBlockHash())) {
               return false;
           }
           if (!Arrays.equals(getEntriesHash(), that.getEntriesHash())) {
               return false;
           }
           if (!Arrays.equals(getPreviousBlockHash(), that.getPreviousBlockHash())) {
               return false;
           }
           if (!Arrays.deepEquals(getEntriesHashList(), that.getEntriesHashList())) {
               return false;
           }
           if (!getTransactionInfo().equals(that.getTransactionInfo())) {
               return false;
           }
           if (getRedactionInfo() != null ? !getRedactionInfo().equals(that.getRedactionInfo()) : that.getRedactionInfo() != null) {
               return false;
           }
           return getRevisions() != null ? getRevisions().equals(that.getRevisions()) : that.getRevisions() == null;
       }
   
       @Override
       public int hashCode() {
           int result = getBlockAddress().hashCode();
           result = 31 * result + getTransactionId().hashCode();
           result = 31 * result + getBlockTimestamp().hashCode();
           result = 31 * result + Arrays.hashCode(getBlockHash());
           result = 31 * result + Arrays.hashCode(getEntriesHash());
           result = 31 * result + Arrays.hashCode(getPreviousBlockHash());
           result = 31 * result + Arrays.deepHashCode(getEntriesHashList());
           result = 31 * result + getTransactionInfo().hashCode();
           result = 31 * result + (getRedactionInfo() != null ? getRedactionInfo().hashCode() : 0);
           result = 31 * result + (getRevisions() != null ? getRevisions().hashCode() : 0);
           return result;
       }
   
       /**
        * This method validates that the hashes of the components of a journal block make up the block
        * hash that is provided with the block itself.
        *
        * The components that contribute to the hash of the journal block consist of the following:
        *   - user transaction information (contained in [transactionInfo])
        *   - user redaction information (contained in [redactionInfo])
        *   - user revisions (contained in [revisions])
        *   - hashes of internal-only system metadata (contained in [revisions] and in [entriesHashList])
        *   - the previous block hash
        *
        * If any of the computed hashes of user information cannot be validated or any of the system
        * hashes do not result in the correct computed values, this method will throw an IllegalArgumentException.
        *
        * Internal-only system metadata is represented by its hash, and can be present in the form of certain
        * items in the [revisions] list that only contain a hash and no user data, as well as some hashes
        * in [entriesHashList].
        *
        * To validate that the hashes of the user data are valid components of the [blockHash], this method
        * performs the following steps:
        *
        * 1. Compute the hash of the [transactionInfo] and validate that it is included in the [entriesHashList].
        * 2. Compute the hash of the [redactionInfo], if present, and validate that it is included in the [entriesHashList].
        * 3. Validate the hash of each user revision was correctly computed and matches the hash published
        * with that revision.
        * 4. Compute the hash of the [revisions] by treating the revision hashes as the leaf nodes of a Merkle tree
        * and calculating the root hash of that tree. Then validate that hash is included in the [entriesHashList].
        * 5. Compute the hash of the [entriesHashList] by treating the hashes as the leaf nodes of a Merkle tree
        * and calculating the root hash of that tree. Then validate that hash matches [entriesHash].
        * 6. Finally, compute the block hash by computing the hash resulting from concatenating the [entriesHash]
        * and previous block hash, and validate that the result matches the [blockHash] provided by QLDB with the block.
        *
        * This method is called by ValidateQldbHashChain::verify for each journal block to validate its
        * contents before verifying that the hash chain between consecutive blocks is correct.
        */
       public void verifyBlockHash() {
           Set<ByteBuffer> entriesHashSet = new HashSet<>();
           Arrays.stream(entriesHashList).forEach(hash -> entriesHashSet.add(wrap(hash).asReadOnlyBuffer()));
   
           byte[] computedTransactionInfoHash = computeTransactionInfoHash();
           if (!entriesHashSet.contains(wrap(computedTransactionInfoHash).asReadOnlyBuffer())) {
               throw new IllegalArgumentException(
                       "Block transactionInfo hash is not contained in the QLDB block entries hash list.");
           }
   
           if (redactionInfo != null) {
               byte[] computedRedactionInfoHash = computeRedactionInfoHash();
               if (!entriesHashSet.contains(wrap(computedRedactionInfoHash).asReadOnlyBuffer())) {
                   throw new IllegalArgumentException(
                           "Block redactionInfo hash is not contained in the QLDB block entries hash list.");
               }
           }
   
           if (revisions != null) {
               revisions.forEach(QldbRevision::verifyRevisionHash);
               byte[] computedRevisionsHash = computeRevisionsHash();
               if (!entriesHashSet.contains(wrap(computedRevisionsHash).asReadOnlyBuffer())) {
                   throw new IllegalArgumentException(
                           "Block revisions list hash is not contained in the QLDB block entries hash list.");
               }
           }
   
           byte[] computedEntriesHash = computeEntriesHash();
           if (!Arrays.equals(computedEntriesHash, entriesHash)) {
               throw new IllegalArgumentException("Computed entries hash does not match entries hash provided in the block.");
           }
   
           byte[] computedBlockHash = Verifier.dot(computedEntriesHash, previousBlockHash);
           if (!Arrays.equals(computedBlockHash, blockHash)) {
               throw new IllegalArgumentException("Computed block hash does not match block hash provided in the block.");
           }
       }
   
       private byte[] computeTransactionInfoHash() {
           try {
               return QldbIonUtils.hashIonValue(Constants.MAPPER.writeValueAsIonValue(transactionInfo));
           } catch (IOException e) {
               throw new IllegalArgumentException("Could not compute transactionInfo hash to verify block hash.", e);
           }
       }
   
       private byte[] computeRedactionInfoHash() {
           try {
               return QldbIonUtils.hashIonValue(Constants.MAPPER.writeValueAsIonValue(redactionInfo));
           } catch (IOException e) {
               throw new IllegalArgumentException("Could not compute redactionInfo hash to verify block hash.", e);
           }
       }
   
       private byte[] computeRevisionsHash() {
           return Verifier.calculateMerkleTreeRootHash(revisions.stream().map(QldbRevision::getHash).collect(Collectors.toList()));
       }
   
       private byte[] computeEntriesHash() {
           return Verifier.calculateMerkleTreeRootHash(Arrays.asList(entriesHashList));
       }
   }
   ```

1. 다음 프로그램(`ValidateQldbHashChain.java`)을 컴파일하고 실행하여 다음 단계를 수행합니다.

   1. `vehicle-registration` 원장의 저널 블록을 라는 Amazon S3 버킷으로 내보냅니다**qldb-tutorial-journal-export-*111122223333***( AWS 계정 숫자로 대체).

   1. `verifyBlockHash()`를 호출하여 각 블록 내의 개별 해시 구성 요소를 검증합니다.

   1. 저널 블록 간의 해시 체인을 검증합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.model.ExportJournalToS3Result;
   import com.amazonaws.services.qldb.model.S3EncryptionConfiguration;
   import com.amazonaws.services.qldb.model.S3ObjectEncryptionType;
   import com.amazonaws.services.s3.AmazonS3ClientBuilder;
   
   import java.time.Instant;
   import java.util.Arrays;
   import java.util.List;
   
   import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
   import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   import software.amazon.qldb.tutorial.qldb.JournalBlock;
   
   /**
    * Validate the hash chain of a QLDB ledger by stepping through its S3 export.
    *
    * This code accepts an exportId as an argument, if exportId is passed the code
    * will use that or request QLDB to generate a new export to perform QLDB hash
    * chain validation.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class ValidateQldbHashChain {
       public static final Logger log = LoggerFactory.getLogger(ValidateQldbHashChain.class);
       private static final int TIME_SKEW = 20;
   
       private ValidateQldbHashChain() { }
   
       /**
        * Export journal contents to a S3 bucket.
        *
        * @return the ExportId of the journal export.
        * @throws InterruptedException if the thread is interrupted while waiting for export to complete.
        */
       private static String createExport() throws InterruptedException {
           String accountId = AWSSecurityTokenServiceClientBuilder.defaultClient()
               .getCallerIdentity(new GetCallerIdentityRequest()).getAccount();
           String bucketName = Constants.JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX + "-" + accountId;
           String prefix = Constants.LEDGER_NAME + "-" + Instant.now().getEpochSecond() + "/";
   
           S3EncryptionConfiguration encryptionConfiguration = new S3EncryptionConfiguration()
                   .withObjectEncryptionType(S3ObjectEncryptionType.SSE_S3);
           ExportJournalToS3Result exportJournalToS3Result = 
               ExportJournal.createJournalExportAndAwaitCompletion(Constants.LEDGER_NAME, 
                       bucketName, prefix, null, encryptionConfiguration, ExportJournal.DEFAULT_EXPORT_TIMEOUT_MS);
   
           return exportJournalToS3Result.getExportId();
       }
   
       /**
        * Validates that the chain hash on the {@link JournalBlock} is valid.
        *
        * @param journalBlocks
        *              {@link JournalBlock} containing hashes to validate.
        * @throws IllegalStateException if previous block hash does not match.
        */
       public static void verify(final List<JournalBlock> journalBlocks) {
           if (journalBlocks.size() == 0) {
               return;
           }
   
           journalBlocks.stream().reduce(null, (previousJournalBlock, journalBlock) -> {
               journalBlock.verifyBlockHash();
               if (previousJournalBlock == null) { return journalBlock; }
               if (!Arrays.equals(previousJournalBlock.getBlockHash(), journalBlock.getPreviousBlockHash())) {
                   throw new IllegalStateException("Previous block hash doesn't match.");
               }
               byte[] blockHash = Verifier.dot(journalBlock.getEntriesHash(), previousJournalBlock.getBlockHash());
               if (!Arrays.equals(blockHash, journalBlock.getBlockHash())) {
                   throw new IllegalStateException("Block hash doesn't match entriesHash dot previousBlockHash, the chain is "
                           + "broken.");
               }
               return journalBlock;
           });
       }
   
       public static void main(final String... args) throws InterruptedException {
           try {
               String exportId;
               if (args.length == 1) {
                   exportId = args[0];
                   log.info("Validating QLDB hash chain for exportId: " + exportId);
               } else {
                   log.info("Requesting QLDB to create an export.");
                   exportId = createExport();
               }
               List<JournalBlock> journalBlocks =
                   JournalS3ExportReader.readExport(DescribeJournalExport.describeExport(Constants.LEDGER_NAME,
                       exportId), AmazonS3ClientBuilder.defaultClient());
               verify(journalBlocks);
           } catch (Exception e) {
               log.error("Unable to perform hash chain verification.", e);
               throw e;
           }
       }
   
   }
   ```

`vehicle-registration` 원장을 더 이상 사용할 필요가 없는 경우 [9단계(선택 사항): 리소스 정리](getting-started.java.step-9.md)로 진행하세요.

# 9단계(선택 사항): 리소스 정리
<a name="getting-started.java.step-9"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

`vehicle-registration` 원장을 계속 사용할 수 있습니다. 그러나 더 이상 필요하지 않은 경우 삭제해야 합니다.

**원장을 삭제하려면**

1. 다음 프로그램(`DeleteLedger.java`)을 컴파일하고 실행하여 `vehicle-registration` 원장 및 모든 콘텐츠를 삭제합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   package software.amazon.qldb.tutorial;
   
   import com.amazonaws.services.qldb.AmazonQLDB;
   import com.amazonaws.services.qldb.model.DeleteLedgerRequest;
   import com.amazonaws.services.qldb.model.DeleteLedgerResult;
   import com.amazonaws.services.qldb.model.ResourceNotFoundException;
   import com.amazonaws.services.qldb.model.UpdateLedgerRequest;
   import com.amazonaws.services.qldb.model.UpdateLedgerResult;
   
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   
   /**
    * Delete a ledger.
    *
    * This code expects that you have AWS credentials setup per:
    * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html
    */
   public final class DeleteLedger {
       public static final Logger log = LoggerFactory.getLogger(DeleteLedger.class);
       public static final Long LEDGER_DELETION_POLL_PERIOD_MS = 20_000L;
       public static AmazonQLDB client = CreateLedger.getClient();
   
       private DeleteLedger() { }
   
       public static void main(String... args) throws Exception {
           try {
               setDeletionProtection(Constants.LEDGER_NAME, false);
   
               delete(Constants.LEDGER_NAME);
   
               waitForDeleted(Constants.LEDGER_NAME);
   
           } catch (Exception e) {
               log.error("Unable to delete the ledger.", e);
               throw e;
           }
       }
   
       /**
        * Send a request to the QLDB database to delete the specified ledger.
        *
        * @param ledgerName
        *              Name of the ledger to be deleted.
        * @return DeleteLedgerResult.
        */
       public static DeleteLedgerResult delete(final String ledgerName) {
           log.info("Attempting to delete the ledger with name: {}...", ledgerName);
           DeleteLedgerRequest request = new DeleteLedgerRequest().withName(ledgerName);
           DeleteLedgerResult result = client.deleteLedger(request);
           log.info("Success.");
           return result;
       }
   
       /**
        * Wait for the ledger to be deleted.
        *
        * @param ledgerName
        *              Name of the ledger being deleted.
        * @throws InterruptedException if thread is being interrupted.
        */
       public static void waitForDeleted(final String ledgerName) throws InterruptedException {
           log.info("Waiting for the ledger to be deleted...");
           while (true) {
               try {
                   DescribeLedger.describe(ledgerName);
                   log.info("The ledger is still being deleted. Please wait...");
                   Thread.sleep(LEDGER_DELETION_POLL_PERIOD_MS);
               } catch (ResourceNotFoundException ex) {
                   log.info("Success. The ledger is deleted.");
                   break;
               }
           }
       }
       
       public static UpdateLedgerResult setDeletionProtection(String ledgerName, boolean deletionProtection) {
           log.info("Let's set deletionProtection to {} for the ledger with name {}", deletionProtection, ledgerName);
           UpdateLedgerRequest request = new UpdateLedgerRequest()
                   .withName(ledgerName)
                   .withDeletionProtection(deletionProtection);
   
           UpdateLedgerResult result = client.updateLedger(request);
           log.info("Success. Ledger updated: {}", result);
           return result;
       }
   }
   ```
**참고**  
원장에 대해 삭제 방지가 활성화된 경우 QLDB API를 사용하여 원장을 삭제하기 전에 먼저 이를 비활성화해야 합니다.

1. [이전 단계](getting-started.java.step-8.md)에서 저널 데이터를 내보냈는데 더 이상 필요하지 않은 경우, Amazon S3 콘솔을 사용하여 S3 버킷을 삭제하세요.

   [https://console.aws.amazon.com/s3/](https://console.aws.amazon.com/s3/)에서 Amazon S3 콘솔을 엽니다.

# Amazon QLDB Node.js 자습서
<a name="getting-started.nodejs.tutorial"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서 샘플 애플리케이션 구현에서는 Node.js의 AWS SDK for JavaScript와 함께 Amazon QLDB 드라이버를 사용하여 QLDB 원장을 생성하고 샘플 데이터로 채웁니다.

이 자습서로 학습하면서 관리 API 작업에 대해서 [AWS SDK for JavaScript API 참조](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/)를 참조할 수 있습니다. 트랜잭션 데이터 작업의 경우 [Node.js API용 QLDB 드라이버 참조](https://amazon-qldb-docs.s3.amazonaws.com/drivers/nodejs/3.1.0/index.html)를 참조할 수 있습니다.

**참고**  
해당하는 경우 일부 자습서 단계에는 지원되는 Node.js QLDB 드라이버의 각 주요 버전에 대해 서로 다른 명령 또는 코드 예제가 있습니다.

**Topics**
+ [

# Amazon QLDB Node.js 샘플 애플리케이션 설치
](sample-app.nodejs.md)
+ [

# 1단계: 새 원장 생성
](getting-started.nodejs.step-1.md)
+ [

# 2단계: 원장과의 연결을 테스트
](getting-started.nodejs.step-2.md)
+ [

# 3단계: 테이블, 인덱스 및 샘플 데이터 생성
](getting-started.nodejs.step-3.md)
+ [

# 4단계: 원장에서 테이블 쿼리
](getting-started.nodejs.step-4.md)
+ [

# 5단계: 원장의 문서 수정
](getting-started.nodejs.step-5.md)
+ [

# 6단계: 문서의 개정 기록 보기
](getting-started.nodejs.step-6.md)
+ [

# 7단계: 원장에 있는 문서 검증
](getting-started.nodejs.step-7.md)
+ [

# 8단계(선택 사항): 리소스 정리
](getting-started.nodejs.step-8.md)

# Amazon QLDB Node.js 샘플 애플리케이션 설치
<a name="sample-app.nodejs"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 섹션에서는 단계별 Node.js 자습서를 위해 제공된 Amazon QLDB 샘플 애플리케이션을 설치하고 실행하는 방법을 설명합니다. 이 샘플 애플리케이션의 사용 사례는 차량 등록에 대한 전체 기록 정보를 추적하는 자동차 부서(DMV) 데이터베이스입니다.

Node.js용 DMV 샘플 애플리케이션은 GitHub 리포지토리 [aws-samples/amazon-qldb-dmv-sample-nodejs](https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs)의 오픈 소스입니다.

## 사전 조건
<a name="sample-app.nodejs.prereqs"></a>

시작하기 전에 Node.js [사전 조건](getting-started.nodejs.md#getting-started.nodejs.prereqs)용 QLDB 드라이버를 완료했는지 확인합니다. 여기에는 Node.js 설치 및 다음 작업이 포함됩니다.

1. 가입합니다 AWS.

1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

이 자습서의 모든 단계를 완료하려면 QLDB API를 통해 원장 리소스에 대한 전체 관리 액세스 권한이 필요합니다.

## 설치
<a name="sample-app.nodejs.install"></a>

**샘플 애플리케이션을 설치하려면**

1. 다음 명령을 입력하여 GitHub에서 샘플 애플리케이션을 복제합니다.

------
#### [ 2.x ]

   ```
   git clone https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs.git
   ```

------
#### [ 1.x ]

   ```
   git clone -b v1.0.0 https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs.git
   ```

------

   샘플 애플리케이션은 Node.js 드라이버 및 [Node.js의 JavaScript용AWS SDK](https://aws.amazon.com/sdk-for-node-js)를 포함하여 이 자습서의 전체 소스 코드와 해당 종속성을 패키징합니다. 이 애플리케이션은 TypeScript로 작성되었습니다.

1. `amazon-qldb-dmv-sample-nodejs` 패키지가 복제된 디렉토리로 전환합니다.

   ```
   cd amazon-qldb-dmv-sample-nodejs
   ```

1. 종속성을 클린 설치합니다.

   ```
   npm ci
   ```

1. 패키지를 트랜스파일합니다.

   ```
   npm run build
   ```

   트랜스파일된 JavaScript 파일은 `./dist` 디렉터리에 작성됩니다.

1. [1단계: 새 원장 생성](getting-started.nodejs.step-1.md)로 진행하여 자습서를 시작하고 원장을 생성하세요.

# 1단계: 새 원장 생성
<a name="getting-started.nodejs.step-1"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 단계에서는 `vehicle-registration`라는 이름의 새 Amazon QLDB 원장을 생성합니다

**새 원장을 생성하려면**

1. 이 자습서의 다른 모든 프로그램에서 사용하는 상수 값이 들어 있는 다음 파일(`Constants.ts`)을 검토하세요.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   /**
    * Constant values used throughout this tutorial.
    */
   export const LEDGER_NAME = "vehicle-registration";
   export const LEDGER_NAME_WITH_TAGS = "tags";
   
   export const DRIVERS_LICENSE_TABLE_NAME = "DriversLicense";
   export const PERSON_TABLE_NAME = "Person";
   export const VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration";
   export const VEHICLE_TABLE_NAME = "Vehicle";
   
   export const GOV_ID_INDEX_NAME = "GovId";
   export const LICENSE_NUMBER_INDEX_NAME = "LicenseNumber";
   export const LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber";
   export const PERSON_ID_INDEX_NAME = "PersonId";
   export const VIN_INDEX_NAME = "VIN";
   
   export const RETRY_LIMIT = 4;
   
   export const JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export";
   export const USER_TABLES = "information_schema.user_tables";
   ```

1. 다음 프로그램(`CreateLedger.ts`)을 사용하여 이름이 `vehicle-registration`로 지정된 원장을 생성합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QLDB } from "aws-sdk";
   import {
       CreateLedgerRequest,
       CreateLedgerResponse,
       DescribeLedgerRequest,
       DescribeLedgerResponse
   } from "aws-sdk/clients/qldb";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { sleep } from "./qldb/Util";
   
   const LEDGER_CREATION_POLL_PERIOD_MS = 10000;
   const ACTIVE_STATE = "ACTIVE";
   
   /**
    * Create a new ledger with the specified name.
    * @param ledgerName Name of the ledger to be created.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a CreateLedgerResponse.
    */
   export async function createLedger(ledgerName: string, qldbClient: QLDB): Promise<CreateLedgerResponse> {
       log(`Creating a ledger named: ${ledgerName}...`);
       const request: CreateLedgerRequest = {
           Name: ledgerName,
           PermissionsMode: "ALLOW_ALL"
       }
       const result: CreateLedgerResponse = await qldbClient.createLedger(request).promise();
       log(`Success. Ledger state: ${result.State}.`);
       return result;
   }
   
   /**
    * Wait for the newly created ledger to become active.
    * @param ledgerName Name of the ledger to be checked on.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a DescribeLedgerResponse.
    */
   export async function waitForActive(ledgerName: string, qldbClient: QLDB): Promise<DescribeLedgerResponse> {
       log(`Waiting for ledger ${ledgerName} to become active...`);
       const request: DescribeLedgerRequest = {
           Name: ledgerName
       }
       while (true) {
           const result: DescribeLedgerResponse = await qldbClient.describeLedger(request).promise();
           if (result.State === ACTIVE_STATE) {
               log("Success. Ledger is active and ready to be used.");
               return result;
           }
           log("The ledger is still creating. Please wait...");
           await sleep(LEDGER_CREATION_POLL_PERIOD_MS);
       }
   }
   
   /**
    * Create a ledger and wait for it to be active.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           await createLedger(LEDGER_NAME, qldbClient);
           await waitForActive(LEDGER_NAME, qldbClient);
       } catch (e) {
           error(`Unable to create the ledger: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
`createLedger` 호출 시 원장 이름과 권한 모드를 지정해야 합니다. 원장 데이터의 보안을 극대화하려면 `STANDARD` 권한 모드를 사용할 것을 권장합니다.
원장을 생성할 때 기본적으로 *삭제 방지*가 활성화됩니다. 이 기능은 사용자가 원장을 삭제하는 것을 방지하는 QLDB의 기능입니다. QLDB API 또는 AWS Command Line Interface ()를 사용하여 원장 생성 시 삭제 방지 기능을 비활성화할 수 있습니다AWS CLI.
선택적으로, 원장에 첨부할 태그를 지정할 수도 있습니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/CreateLedger.js
   ```

새 원장과의 연결을 확인하려면 [2단계: 원장과의 연결을 테스트](getting-started.nodejs.step-2.md)로 이동하세요.

# 2단계: 원장과의 연결을 테스트
<a name="getting-started.nodejs.step-2"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 단계에서는 트랜잭션 데이터 API 엔드포인트를 사용하여 Amazon QLDB의 `vehicle-registration` 원장에 연결할 수 있는지 확인합니다.

**원장과의 연결을 테스트하려면**

1. 다음 프로그램(`ConnectToLedger.ts`)을 사용하여 `vehicle-registration` 원장에 대한 데이터 세션 연결을 생성합니다.

------
#### [ 2.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, RetryConfig  } from "amazon-qldb-driver-nodejs";
   import { ClientConfiguration } from "aws-sdk/clients/qldbsession";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   const qldbDriver: QldbDriver = createQldbDriver();
   
   /**
    * Create a driver for creating sessions.
    * @param ledgerName The name of the ledger to create the driver on.
    * @param serviceConfigurationOptions The configurations for the AWS SDK client that the driver uses.
    * @returns The driver for creating sessions.
    */
   export function createQldbDriver(
       ledgerName: string = LEDGER_NAME,
       serviceConfigurationOptions: ClientConfiguration = {}
   ): QldbDriver {
       const retryLimit = 4;
       const maxConcurrentTransactions = 10;
       //Use driver's default backoff function (and hence, no second parameter provided to RetryConfig)
       const retryConfig: RetryConfig = new RetryConfig(retryLimit);
       const qldbDriver: QldbDriver = new QldbDriver(ledgerName, serviceConfigurationOptions, maxConcurrentTransactions, retryConfig);
       return qldbDriver;
   }
   
   export function getQldbDriver(): QldbDriver {
       return qldbDriver;
   }
   
   /**
    * Connect to a session for a given ledger using default settings.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           log("Listing table names...");
           const tableNames: string[] = await qldbDriver.getTableNames();
           tableNames.forEach((tableName: string): void => {
               log(tableName);
           });
       } catch (e) {
           error(`Unable to create session: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```

------
#### [ 1.x ]

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver  } from "amazon-qldb-driver-nodejs";
   import { ClientConfiguration } from "aws-sdk/clients/qldbsession";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   const qldbDriver: QldbDriver = createQldbDriver();
   
   /**
    * Create a driver for creating sessions.
    * @param ledgerName The name of the ledger to create the driver on.
    * @param serviceConfigurationOptions The configurations for the AWS SDK client that the driver uses.
    * @returns The driver for creating sessions.
    */
   export function createQldbDriver(
       ledgerName: string = LEDGER_NAME,
       serviceConfigurationOptions: ClientConfiguration = {}
   ): QldbDriver {
       const qldbDriver: QldbDriver = new QldbDriver(ledgerName, serviceConfigurationOptions);
       return qldbDriver;
   }
   
   export function getQldbDriver(): QldbDriver {
       return qldbDriver;
   }
   
   /**
    * Connect to a session for a given ledger using default settings.
    * @returns Promise which fulfills with void.
    */
   var main = async function(): Promise<void> {
       try {
           log("Listing table names...");
           const tableNames: string[] = await qldbDriver.getTableNames();
           tableNames.forEach((tableName: string): void => {
               log(tableName);
           });
       } catch (e) {
           error(`Unable to create session: ${e}`);
       } 
   }
   
   if (require.main === module) {
       main();
   }
   ```

------
**참고**  
원장에서 데이터 트랜잭션을 실행하려면 지정된 원장에 연결할 QLDB 드라이버 객체를 만들어야 합니다. 이는 [이전 단계](getting-started.nodejs.step-1.md)에서 원장을 생성할 때 사용한 `qldbClient` 객체와는 다른 클라이언트 객체입니다. 이전 클라이언트는 [Amazon QLDB API 참조](api-reference.md)에 나열된 관리 API 작업을 실행하는 데만 사용됩니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/ConnectToLedger.js
   ```

`vehicle-registration` 원장에 테이블을 생성하려면 [3단계: 테이블, 인덱스 및 샘플 데이터 생성](getting-started.nodejs.step-3.md)로 진행하세요.

# 3단계: 테이블, 인덱스 및 샘플 데이터 생성
<a name="getting-started.nodejs.step-3"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 원장이 활성화되고 연결을 수락하면 차량, 소유자 및 등록 정보에 대한 데이터 테이블 생성을 시작할 수 있습니다. 테이블과 인덱스를 생성한 후 데이터를 로드할 수 있습니다.

이 단계에서는 `vehicle-registration` 원장에 4개의 테이블을 생성합니다.
+ `VehicleRegistration`
+ `Vehicle`
+ `Person`
+ `DriversLicense`

또한 다음과 같은 인덱스를 생성합니다.


****  

| 테이블 이름 | 필드 | 
| --- | --- | 
| VehicleRegistration | VIN | 
| VehicleRegistration | LicensePlateNumber | 
| Vehicle | VIN | 
| Person | GovId | 
| DriversLicense | LicenseNumber | 
| DriversLicense | PersonId | 

샘플 데이터를 삽입할 때는 먼저 `Person` 테이블에 문서를 삽입합니다. 그런 다음 각 `Person` 문서에서 시스템이 할당한 `id`를 사용하여 적절한 `VehicleRegistration` 및 `DriversLicense` 문서의 해당 필드를 채웁니다.

**작은 정보**  
가장 좋은 방법은 문서의 시스템 할당 `id`를 외래 키로 사용하는 것입니다. 고유 식별자(예: 차량의 VIN)로 사용되는 필드를 정의할 수 있지만 문서의 실제  고유 식별자는 `id`입니다. 이 필드는 문서의 메타데이터에 포함되며 *커밋된 뷰*(테이블의 시스텀 정의 뷰)에서 쿼리할 수 있습니다.  
QLDB 뷰에 대한 자세한 내용은 [핵심 개념](ledger-structure.md) 섹션을 참조하세요. 메타데이터에 대해 자세히 알아보려면 [문서 메타데이터 쿼리](working.metadata.md) 섹션을 참조하세요.

**테이블 및 인덱스를 생성하려면**

1. 다음 프로그램(`CreateTable.ts`)을 사용하여 앞서 언급한 테이블을 생성합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import {
       DRIVERS_LICENSE_TABLE_NAME,
       PERSON_TABLE_NAME,
       VEHICLE_REGISTRATION_TABLE_NAME,
       VEHICLE_TABLE_NAME
   } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   /**
    * Create multiple tables in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param tableName Name of the table to create.
    * @returns Promise which fulfills with the number of changes to the database.
    */
   export async function createTable(txn: TransactionExecutor, tableName: string): Promise<number> {
       const statement: string = `CREATE TABLE ${tableName}`;
       return await txn.execute(statement).then((result: Result) => {
           log(`Successfully created table ${tableName}.`);
           return result.getResultList().length;
       });
   }
   
   /**
    * Create tables in a QLDB ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               Promise.all([
                   createTable(txn, VEHICLE_REGISTRATION_TABLE_NAME),
                   createTable(txn, VEHICLE_TABLE_NAME),
                   createTable(txn, PERSON_TABLE_NAME),
                   createTable(txn, DRIVERS_LICENSE_TABLE_NAME)
               ]);
           });
       } catch (e) {
           error(`Unable to create tables: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
이 프로그램은 QLDB 드라이버 인스턴스에서 `executeLambda` 함수를 사용하는 방법을 보여줍니다. 이 예제에서는 단일 Lambda 표현식을 사용하여 여러 `CREATE TABLE` PartiQL 문을 실행합니다.  
이 실행 함수는 암시적으로 트랜잭션을 시작하고 Lambda에서 모든 문을 실행한 다음 트랜잭션을 자동 커밋합니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/CreateTable.js
   ```

1. 앞에서 설명한 대로 다음 프로그램(`CreateIndex.ts`)을 사용하여 테이블에 인덱스를 생성합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import {
       DRIVERS_LICENSE_TABLE_NAME,
       GOV_ID_INDEX_NAME,
       LICENSE_NUMBER_INDEX_NAME,
       LICENSE_PLATE_NUMBER_INDEX_NAME,
       PERSON_ID_INDEX_NAME,
       PERSON_TABLE_NAME,
       VEHICLE_REGISTRATION_TABLE_NAME,
       VEHICLE_TABLE_NAME,
       VIN_INDEX_NAME
   } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   /**
    * Create an index for a particular table.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param tableName Name of the table to add indexes for.
    * @param indexAttribute Index to create on a single attribute.
    * @returns Promise which fulfills with the number of changes to the database.
    */
   export async function createIndex(
       txn: TransactionExecutor, 
       tableName: string, 
       indexAttribute: string
   ): Promise<number> {
       const statement: string = `CREATE INDEX on ${tableName} (${indexAttribute})`;
       return await txn.execute(statement).then((result) => {
           log(`Successfully created index ${indexAttribute} on table ${tableName}.`);
           return result.getResultList().length;
       });
   }
   
   /**
    * Create indexes on tables in a particular ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               Promise.all([
                   createIndex(txn, PERSON_TABLE_NAME, GOV_ID_INDEX_NAME),
                   createIndex(txn, VEHICLE_TABLE_NAME, VIN_INDEX_NAME),
                   createIndex(txn, VEHICLE_REGISTRATION_TABLE_NAME, VIN_INDEX_NAME),
                   createIndex(txn, VEHICLE_REGISTRATION_TABLE_NAME, LICENSE_PLATE_NUMBER_INDEX_NAME),
                   createIndex(txn, DRIVERS_LICENSE_TABLE_NAME, PERSON_ID_INDEX_NAME),
                   createIndex(txn, DRIVERS_LICENSE_TABLE_NAME, LICENSE_NUMBER_INDEX_NAME)
               ]);
           });
       } catch (e) {
           error(`Unable to create indexes: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/CreateIndex.js
   ```

**데이터를 테이블로 로드하려면**

1. 다음 `.ts` 파일을 검토합니다.

   1. `SampleData.ts` - `vehicle-registration` 테이블에 삽입한 샘플 데이터를 포함합니다.

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { Decimal } from "ion-js";
      
      const EMPTY_SECONDARY_OWNERS: object[] = [];
      export const DRIVERS_LICENSE = [
          {
              PersonId: "",
              LicenseNumber: "LEWISR261LL",
              LicenseType: "Learner",
              ValidFromDate: new Date("2016-12-20"),
              ValidToDate: new Date("2020-11-15")
          },
          {
              PersonId: "",
              LicenseNumber : "LOGANB486CG",
              LicenseType: "Probationary",
              ValidFromDate : new Date("2016-04-06"),
              ValidToDate : new Date("2020-11-15")
          },
          {
              PersonId: "",
              LicenseNumber : "744 849 301",
              LicenseType: "Full",
              ValidFromDate : new Date("2017-12-06"),
              ValidToDate : new Date("2022-10-15")
          },
          {
              PersonId: "",
              LicenseNumber : "P626-168-229-765",
              LicenseType: "Learner",
              ValidFromDate : new Date("2017-08-16"),
              ValidToDate : new Date("2021-11-15")
          },
          {
              PersonId: "",
              LicenseNumber : "S152-780-97-415-0",
              LicenseType: "Probationary",
              ValidFromDate : new Date("2015-08-15"),
              ValidToDate : new Date("2021-08-21")
          }
      ];
      export const PERSON = [
          {
              FirstName : "Raul",
              LastName : "Lewis",
              DOB : new Date("1963-08-19"),
              Address : "1719 University Street, Seattle, WA, 98109",
              GovId : "LEWISR261LL",
              GovIdType : "Driver License"
          },
          {
              FirstName : "Brent",
              LastName : "Logan",
              DOB : new Date("1967-07-03"),
              Address : "43 Stockert Hollow Road, Everett, WA, 98203",
              GovId : "LOGANB486CG",
              GovIdType : "Driver License"
          },
          {
              FirstName : "Alexis",
              LastName : "Pena",
              DOB : new Date("1974-02-10"),
              Address : "4058 Melrose Street, Spokane Valley, WA, 99206",
              GovId : "744 849 301",
              GovIdType : "SSN"
          },
          {
              FirstName : "Melvin",
              LastName : "Parker",
              DOB : new Date("1976-05-22"),
              Address : "4362 Ryder Avenue, Seattle, WA, 98101",
              GovId : "P626-168-229-765",
              GovIdType : "Passport"
          },
          {
              FirstName : "Salvatore",
              LastName : "Spencer",
              DOB : new Date("1997-11-15"),
              Address : "4450 Honeysuckle Lane, Seattle, WA, 98101",
              GovId : "S152-780-97-415-0",
              GovIdType : "Passport"
          }
      ];
      export const VEHICLE = [
          {
              VIN : "1N4AL11D75C109151",
              Type : "Sedan",
              Year : 2011,
              Make : "Audi",
              Model : "A5",
              Color : "Silver"
          },
          {
              VIN : "KM8SRDHF6EU074761",
              Type : "Sedan",
              Year : 2015,
              Make : "Tesla",
              Model : "Model S",
              Color : "Blue"
          },
          {
              VIN : "3HGGK5G53FM761765",
              Type : "Motorcycle",
              Year : 2011,
              Make : "Ducati",
              Model : "Monster 1200",
              Color : "Yellow"
          },
          {
              VIN : "1HVBBAANXWH544237",
              Type : "Semi",
              Year : 2009,
              Make : "Ford",
              Model : "F 150",
              Color : "Black"
          },
          {
              VIN : "1C4RJFAG0FC625797",
              Type : "Sedan",
              Year : 2019,
              Make : "Mercedes",
              Model : "CLK 350",
              Color : "White"
          }
      ];
      export const VEHICLE_REGISTRATION = [
          {
              VIN : "1N4AL11D75C109151",
              LicensePlateNumber : "LEWISR261LL",
              State : "WA",
              City : "Seattle",
              ValidFromDate : new Date("2017-08-21"),
              ValidToDate : new Date("2020-05-11"),
              PendingPenaltyTicketAmount : new Decimal(9025, -2),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "KM8SRDHF6EU074761",
              LicensePlateNumber : "CA762X",
              State : "WA",
              City : "Kent",
              PendingPenaltyTicketAmount : new Decimal(13075, -2),
              ValidFromDate : new Date("2017-09-14"),
              ValidToDate : new Date("2020-06-25"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "3HGGK5G53FM761765",
              LicensePlateNumber : "CD820Z",
              State : "WA",
              City : "Everett",
              PendingPenaltyTicketAmount : new Decimal(44230, -2),
              ValidFromDate : new Date("2011-03-17"),
              ValidToDate : new Date("2021-03-24"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "1HVBBAANXWH544237",
              LicensePlateNumber : "LS477D",
              State : "WA",
              City : "Tacoma",
              PendingPenaltyTicketAmount : new Decimal(4220, -2),
              ValidFromDate : new Date("2011-10-26"),
              ValidToDate : new Date("2023-09-25"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          },
          {
              VIN : "1C4RJFAG0FC625797",
              LicensePlateNumber : "TH393F",
              State : "WA",
              City : "Olympia",
              PendingPenaltyTicketAmount : new Decimal(3045, -2),
              ValidFromDate : new Date("2013-09-02"),
              ValidToDate : new Date("2024-03-19"),
              Owners : {
                  PrimaryOwner : { PersonId : "" },
                  SecondaryOwners : EMPTY_SECONDARY_OWNERS
              }
          }
      ];
      ```

   1. `Util.ts` - [Amazon Ion](ion.md) 데이터를 변환, 구문 분석 및 인쇄하는 도우미 기능을 제공하기 위해 `ion-js` 패키지에서 가져오는 유틸리티 모듈입니다.

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
      import { GetBlockResponse, GetDigestResponse, ValueHolder } from "aws-sdk/clients/qldb";
      import { 
          Decimal, 
          decodeUtf8,
          dom,
          IonTypes, 
          makePrettyWriter, 
          makeReader, 
          Reader, 
          Timestamp, 
          toBase64, 
          Writer
      } from "ion-js";
      
      import { error } from "./LogUtil";
      
      
      
      /**
       * TODO: Replace this with json.stringify
       * Returns the string representation of a given BlockResponse.
       * @param blockResponse The BlockResponse to convert to string.
       * @returns The string representation of the supplied BlockResponse.
       */
      export function blockResponseToString(blockResponse: GetBlockResponse): string {
          let stringBuilder: string = "";
          if (blockResponse.Block.IonText) {
              stringBuilder = stringBuilder + "Block: " + blockResponse.Block.IonText + ", ";
          }
          if (blockResponse.Proof.IonText) {
              stringBuilder = stringBuilder + "Proof: " + blockResponse.Proof.IonText;
          }
          stringBuilder = "{" + stringBuilder + "}";
          const writer: Writer = makePrettyWriter();
          const reader: Reader = makeReader(stringBuilder);
          writer.writeValues(reader);
          return decodeUtf8(writer.getBytes());
      }
      
      /**
       * TODO: Replace this with json.stringify
       * Returns the string representation of a given GetDigestResponse.
       * @param digestResponse The GetDigestResponse to convert to string.
       * @returns The string representation of the supplied GetDigestResponse.
       */
      export function digestResponseToString(digestResponse: GetDigestResponse): string {
          let stringBuilder: string = "";
          if (digestResponse.Digest) {
              stringBuilder += "Digest: " + JSON.stringify(toBase64(<Uint8Array> digestResponse.Digest)) + ", ";
          }
          if (digestResponse.DigestTipAddress.IonText) {
              stringBuilder += "DigestTipAddress: " + digestResponse.DigestTipAddress.IonText;
          }
          stringBuilder = "{" + stringBuilder + "}";
          const writer: Writer = makePrettyWriter();
          const reader: Reader = makeReader(stringBuilder);
          writer.writeValues(reader);
          return decodeUtf8(writer.getBytes());
      }
      
      /**
       * Get the document IDs from the given table.
       * @param txn The {@linkcode TransactionExecutor} for lambda execute.
       * @param tableName The table name to query.
       * @param field A field to query.
       * @param value The key of the given field.
       * @returns Promise which fulfills with the document ID as a string.
       */
      export async function getDocumentId(
          txn: TransactionExecutor,
          tableName: string,
          field: string,
          value: string
      ): Promise<string> {
          const query: string = `SELECT id FROM ${tableName} AS t BY id WHERE t.${field} = ?`;
          let documentId: string = undefined;
          await txn.execute(query, value).then((result: Result) => {
              const resultList: dom.Value[] = result.getResultList();
              if (resultList.length === 0) {
                  throw new Error(`Unable to retrieve document ID using ${value}.`);
              }
              documentId = resultList[0].get("id").stringValue();
      
          }).catch((err: any) => {
              error(`Error getting documentId: ${err}`);
          });
          return documentId;
      }
      
      /**
       * Sleep for the specified amount of time.
       * @param ms The amount of time to sleep in milliseconds.
       * @returns Promise which fulfills with void.
       */
      export function sleep(ms: number): Promise<void> {
          return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      /**
       * Find the value of a given path in an Ion value. The path should contain a blob value.
       * @param value The Ion value that contains the journal block attributes.
       * @param path The path to a certain attribute.
       * @returns Uint8Array value of the blob, or null if the attribute cannot be found in the Ion value
       *                  or is not of type Blob
       */
      export function getBlobValue(value: dom.Value, path: string): Uint8Array | null {
          const attribute: dom.Value = value.get(path);
          if (attribute !== null && attribute.getType() === IonTypes.BLOB) {
              return attribute.uInt8ArrayValue();
          }
          return null;
      }
      
      /**
       * TODO: Replace this with json.stringify
       * Returns the string representation of a given ValueHolder.
       * @param valueHolder The ValueHolder to convert to string.
       * @returns The string representation of the supplied ValueHolder.
       */
      export function valueHolderToString(valueHolder: ValueHolder): string {
          const stringBuilder: string = `{ IonText: ${valueHolder.IonText}}`;
          const writer: Writer = makePrettyWriter();
          const reader: Reader = makeReader(stringBuilder);
          writer.writeValues(reader);
          return decodeUtf8(writer.getBytes());
      }
      
      /**
       * Converts a given value to Ion using the provided writer.
       * @param value The value to convert to Ion.
       * @param ionWriter The Writer to pass the value into.
       * @throws Error: If the given value cannot be converted to Ion.
       */
      export function writeValueAsIon(value: any, ionWriter: Writer): void {
          switch (typeof value) {
              case "string":
                  ionWriter.writeString(value);
                  break;
              case "boolean":
                  ionWriter.writeBoolean(value);
                  break;
              case "number":
                      ionWriter.writeInt(value);
                      break;
              case "object":
                  if (Array.isArray(value)) {
                      // Object is an array.
                      ionWriter.stepIn(IonTypes.LIST);
      
                      for (const element of value) {
                          writeValueAsIon(element, ionWriter);
                      }
      
                      ionWriter.stepOut();
                  } else if (value instanceof Date) {
                      // Object is a Date.
                      ionWriter.writeTimestamp(Timestamp.parse(value.toISOString()));
                  } else if (value instanceof Decimal) {
                      // Object is a Decimal.
                      ionWriter.writeDecimal(value);
                  } else if (value === null) {
                      ionWriter.writeNull(IonTypes.NULL);
                  } else {
                      // Object is a struct.
                      ionWriter.stepIn(IonTypes.STRUCT);
      
                      for (const key of Object.keys(value)) {
                          ionWriter.writeFieldName(key);
                          writeValueAsIon(value[key], ionWriter);
                      }
                      ionWriter.stepOut();
                  }
                  break;
              default:
                  throw new Error(`Cannot convert to Ion for type: ${(typeof value)}.`);
          }
      }
      ```
**참고**  
이 `getDocumentId` 함수는 테이블에서 시스템 할당 문서 ID를 반환하는 쿼리를 실행합니다. 자세한 내용은 [BY 절을 사용하여 문서 ID 쿼리하기](working.metadata.by-clause.md) 섹션을 참조하세요.

1. 다음 프로그램(`InsertDocument.ts`)을 사용하여 샘플 데이터를 테이블에 삽입합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { DRIVERS_LICENSE, PERSON, VEHICLE, VEHICLE_REGISTRATION } from "./model/SampleData";
   import {
       DRIVERS_LICENSE_TABLE_NAME,
       PERSON_TABLE_NAME,
       VEHICLE_REGISTRATION_TABLE_NAME,
       VEHICLE_TABLE_NAME
   } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   
   /**
    * Insert the given list of documents into a table in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param tableName Name of the table to insert documents into.
    * @param documents List of documents to insert.
    * @returns Promise which fulfills with a {@linkcode Result} object.
    */
   export async function insertDocument(
       txn: TransactionExecutor,
       tableName: string,
       documents: object[]
   ): Promise<Result> {
       const statement: string = `INSERT INTO ${tableName} ?`;
       const result: Result = await txn.execute(statement, documents);
       return result;
   }
   
   /**
    * Handle the insertion of documents and updating PersonIds all in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @returns Promise which fulfills with void.
    */
   async function updateAndInsertDocuments(txn: TransactionExecutor): Promise<void> {
       log("Inserting multiple documents into the 'Person' table...");
       const documentIds: Result = await insertDocument(txn, PERSON_TABLE_NAME, PERSON);
   
       const listOfDocumentIds: dom.Value[] = documentIds.getResultList();
       log("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...");
       updatePersonId(listOfDocumentIds);
   
       log("Inserting multiple documents into the remaining tables...");
       await Promise.all([
           insertDocument(txn, DRIVERS_LICENSE_TABLE_NAME, DRIVERS_LICENSE),
           insertDocument(txn, VEHICLE_REGISTRATION_TABLE_NAME, VEHICLE_REGISTRATION),
           insertDocument(txn, VEHICLE_TABLE_NAME, VEHICLE)
       ]);
   }
   
   /**
    * Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
    * @param documentIds List of document IDs.
    */
   export function updatePersonId(documentIds: dom.Value[]): void {
       documentIds.forEach((value: dom.Value, i: number) => {
           const documentId: string = value.get("documentId").stringValue();
           DRIVERS_LICENSE[i].PersonId = documentId;
           VEHICLE_REGISTRATION[i].Owners.PrimaryOwner.PersonId = documentId;
       });
   }
   
   /**
    * Insert documents into a table in a QLDB ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await updateAndInsertDocuments(txn);
           });
       } catch (e) {
           error(`Unable to insert documents: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
이 프로그램은 파라미터화된 값을 사용하여 `execute` 함수를 호출하는 방법을 보여줍니다. 실행하려는 PartiQL 문 외에도 데이터 파라미터를 전달할 수 있습니다. 명령문 문자열에서 물음표(`?`)를 변수 자리 표시자로 사용하세요.
`INSERT` 문이 성공하면 삽입된 각 문서의 `id`이 반환됩니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/InsertDocument.js
   ```

다음으로 `SELECT` 문을 사용하여 `vehicle-registration` 원장의 테이블에서 데이터를 읽을 수 있습니다. [4단계: 원장에서 테이블 쿼리](getting-started.nodejs.step-4.md)로 이동합니다.

# 4단계: 원장에서 테이블 쿼리
<a name="getting-started.nodejs.step-4"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 원장에 테이블을 생성하고 데이터를 로드한 후 쿼리를 실행하여 방금 삽입한 차량 등록 데이터를 검토할 수 있습니다. QLDB는 [PartiQL](ql-reference.md)을 쿼리 언어로 사용하고 [Amazon Ion](ion.md)을 문서 지향 데이터 모델로 사용합니다.

PartiQL은 Ion과 함께 작동하도록 확장된 오픈 소스 SQL 호환 쿼리 언어입니다. PartiQL을 사용하면 익숙한 SQL 연산자를 사용하여 데이터를 삽입, 쿼리 및 관리할 수 있습니다. Amazon Ion은 JSON의 상위 집합입니다. Ion은 정형, 반정형 및 중첩 데이터를 저장하고 처리할 수 있는 유연성을 제공하는 오픈 소스 문서 기반 데이터 형식입니다.

이 단계에서는 `SELECT` 명령문을 사용하여 `vehicle-registration` 원장의 테이블에서 데이터를 읽습니다.

**주의**  
인덱싱된 조회 없이 QLDB에서 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. PartiQL은 SQL과 호환되므로 이러한 쿼리를 지원합니다. 하지만 QLDB의 프로덕션 사용 사례에 대해서는 테이블 스캔을 실행하지 *마세요.*. 테이블 스캔은 동시성 충돌 및 트랜잭션 시간 초과를 포함하여 대규모 테이블에서 성능 문제를 일으킬 수 있습니다.  
인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 자세한 내용은 [쿼리 성능 최적화](working.optimize.md)을 참조하세요.

**테이블을 쿼리하려면**

1. 다음 프로그램(`FindVehicles.ts`)을 사용하여 원장에 있는 사람의 이름으로 등록된 모든 차량을 쿼리하세요.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { PERSON } from "./model/SampleData";
   import { PERSON_TABLE_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   import { prettyPrintResultList } from "./ScanTable";
   
   /**
    * Query 'Vehicle' and 'VehicleRegistration' tables using a unique document ID in one transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param govId The owner's government ID.
    * @returns Promise which fulfills with void.
    */
   async function findVehiclesForOwner(txn: TransactionExecutor, govId: string): Promise<void> {
       const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", govId);
       const query: string = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " +
                           "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?";
   
       await txn.execute(query, documentId).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           log(`List of vehicles for owner with GovId: ${govId}`);
           prettyPrintResultList(resultList);
       });
   }
   
   /**
    * Find all vehicles registered under a person.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await findVehiclesForOwner(txn, PERSON[0].GovId);
           });
       } catch (e) {
           error(`Error getting vehicles for owner: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
먼저 이 프로그램은 이 문서에 대한 `Person` 테이블을 `GovId LEWISR261LL`로 쿼리하여 `id` 메타데이터 필드를 가져옵니다.  
그런 다음 이 문서 `id`를 외래 키로 사용하여 `VehicleRegistration` 테이블을 `PrimaryOwner.PersonId`로 쿼리합니다. 또한 `VIN` 필드의 `Vehicle` 테이블과 `VehicleRegistration`를 조인합니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/FindVehicles.js
   ```

`vehicle-registration` 원장의 테이블에 있는 문서를 수정하는 방법에 대한 자세한 내용은 [5단계: 원장의 문서 수정](getting-started.nodejs.step-5.md)을 참조하세요.

# 5단계: 원장의 문서 수정
<a name="getting-started.nodejs.step-5"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이제 작업할 데이터가 있으므로 Amazon QLDB에서 `vehicle-registration` 원장에 있는 문서를 변경할 수 있습니다. 이 단계에서 다음 코드 예제는 DML(데이터 조작 언어) 문을 실행하는 방법을 보여줍니다. 이러한 명령문은 한 차량의 주 소유자를 업데이트하고 다른 차량에 보조 소유자를 추가합니다.

**문서를 수정하려면**

1. 다음 프로그램(`TransferVehicleOwnership.ts`)을 사용하여 차량의 주 소유자를 원장에 VIN `1N4AL11D75C109151`으로 업데이트하세요.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { PERSON, VEHICLE } from "./model/SampleData";
   import { PERSON_TABLE_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   
   /**
    * Query a driver's information using the given ID.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param documentId The unique ID of a document in the Person table.
    * @returns Promise which fulfills with an Ion value containing the person.
    */
   export async function findPersonFromDocumentId(txn: TransactionExecutor, documentId: string): Promise<dom.Value> {
       const query: string = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?";
   
       let personId: dom.Value;
       await txn.execute(query, documentId).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           if (resultList.length === 0) {
               throw new Error(`Unable to find person with ID: ${documentId}.`);
           }
           personId = resultList[0];
       });
       return personId;
   }
   
   /**
    * Find the primary owner for the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN to find primary owner for.
    * @returns Promise which fulfills with an Ion value containing the primary owner.
    */
   export async function findPrimaryOwnerForVehicle(txn: TransactionExecutor, vin: string): Promise<dom.Value> {
       log(`Finding primary owner for vehicle with VIN: ${vin}`);
       const query: string = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?";
   
       let documentId: string = undefined;
       await txn.execute(query, vin).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           if (resultList.length === 0) {
               throw new Error(`Unable to retrieve document ID using ${vin}.`);
           }
           const PersonIdValue: dom.Value = resultList[0].get("PersonId");
           if (PersonIdValue === null) {
               throw new Error(`Expected field name PersonId not found.`);
           }
           documentId = PersonIdValue.stringValue();
       });
       return findPersonFromDocumentId(txn, documentId);
   }
   
   /**
    * Update the primary owner for a vehicle using the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN for the vehicle to operate on.
    * @param documentId New PersonId for the primary owner.
    * @returns Promise which fulfills with void.
    */
   async function updateVehicleRegistration(txn: TransactionExecutor, vin: string, documentId: string): Promise<void> {
       const statement: string = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?";
   
       log(`Updating the primary owner for vehicle with VIN: ${vin}...`);
       await txn.execute(statement, documentId, vin).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           if (resultList.length === 0) {
               throw new Error("Unable to transfer vehicle, could not find registration.");
           }
           log(`Successfully transferred vehicle with VIN ${vin} to new owner.`);
       });
   }
   
   /**
    * Validate the current owner of the given vehicle and transfer its ownership to a new owner in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN of the vehicle to transfer ownership of.
    * @param currentOwner The GovId of the current owner of the vehicle.
    * @param newOwner The GovId of the new owner of the vehicle.
    */
   export async function validateAndUpdateRegistration(
       txn: TransactionExecutor,
       vin: string,
       currentOwner: string,
       newOwner: string
   ): Promise<void> {
       const primaryOwner: dom.Value = await findPrimaryOwnerForVehicle(txn, vin);
       const govIdValue: dom.Value = primaryOwner.get("GovId");
       if (govIdValue !== null && govIdValue.stringValue() !== currentOwner) {
           log("Incorrect primary owner identified for vehicle, unable to transfer.");
       }
       else {
           const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", newOwner);
           await updateVehicleRegistration(txn, vin, documentId);
           log("Successfully transferred vehicle ownership!");
       }
   }
   
   /**
    * Find primary owner for a particular vehicle's VIN.
    * Transfer to another primary owner for a particular vehicle's VIN.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
   
           const vin: string = VEHICLE[0].VIN;
           const previousOwnerGovId: string = PERSON[0].GovId;
           const newPrimaryOwnerGovId: string = PERSON[1].GovId;
   
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await validateAndUpdateRegistration(txn, vin, previousOwnerGovId,  newPrimaryOwnerGovId);
           });
       } catch (e) {
           error(`Unable to connect and run queries: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/TransferVehicleOwnership.js
   ```

1. 다음 프로그램(`AddSecondaryOwner.ts`)을 사용하여 차량의 보조 소유자를 원장에 VIN `KM8SRDHF6EU074761`으로 추가하세요.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { PERSON, VEHICLE_REGISTRATION } from "./model/SampleData";
   import { PERSON_TABLE_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   import { prettyPrintResultList } from "./ScanTable";
   
   /**
    * Add a secondary owner into 'VehicleRegistration' table for a particular VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin VIN of the vehicle to query.
    * @param secondaryOwnerId The secondary owner's person ID.
    * @returns Promise which fulfills with void.
    */
   export async function addSecondaryOwner(
       txn: TransactionExecutor, 
       vin: string, 
       secondaryOwnerId: string
   ): Promise<void> {
       log(`Inserting secondary owner for vehicle with VIN: ${vin}`);
       const query: string =
           `FROM VehicleRegistration AS v WHERE v.VIN = ? INSERT INTO v.Owners.SecondaryOwners VALUE ?`;
   
       const personToInsert = {PersonId: secondaryOwnerId};
       await txn.execute(query, vin, personToInsert).then(async (result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
           log("VehicleRegistration Document IDs which had secondary owners added: ");
           prettyPrintResultList(resultList);
       });
   }
   
   /**
    * Query for a document ID with a government ID.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param governmentId The government ID to query with.
    * @returns Promise which fulfills with the document ID as a string.
    */
   export async function getDocumentIdByGovId(txn: TransactionExecutor, governmentId: string): Promise<string> {
       const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", governmentId);
       return documentId;
   }
   
   /**
    * Check whether a driver has already been registered for the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin VIN of the vehicle to query.
    * @param secondaryOwnerId The secondary owner's person ID.
    * @returns Promise which fulfills with a boolean.
    */
   export async function isSecondaryOwnerForVehicle(
       txn: TransactionExecutor,
       vin: string,
       secondaryOwnerId: string
   ): Promise<boolean> {
       log(`Finding secondary owners for vehicle with VIN: ${vin}`);
       const query: string = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?";
   
       let doesExist: boolean = false;
   
       await txn.execute(query, vin).then((result: Result) => {
           const resultList: dom.Value[] = result.getResultList();
   
           resultList.forEach((value: dom.Value) => {
               const secondaryOwnersList: dom.Value[] = value.get("SecondaryOwners").elements();
   
               secondaryOwnersList.forEach((secondaryOwner) => {
                   const personId: dom.Value = secondaryOwner.get("PersonId");
                   if (personId !== null &&  personId.stringValue() === secondaryOwnerId) {
                       doesExist = true;
                   }
               });
           });
       });
       return doesExist;
   }
   
   /**
    * Finds and adds secondary owners for a vehicle.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           const vin: string = VEHICLE_REGISTRATION[1].VIN;
           const govId: string = PERSON[0].GovId;
   
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               const documentId: string = await getDocumentIdByGovId(txn, govId);
   
               if (await isSecondaryOwnerForVehicle(txn, vin, documentId)) {
                   log(`Person with ID ${documentId} has already been added as a secondary owner of this vehicle.`);
               } else {
                   await addSecondaryOwner(txn, vin, documentId);
               }
           });
   
           log("Secondary owners successfully updated.");
       } catch (e) {
           error(`Unable to add secondary owner: ${e}`);
       } 
   }
   
   if (require.main === module) {
       main();
   }
   ```

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/AddSecondaryOwner.js
   ```

`vehicle-registration` 원장의 이러한 변경 사항을 검토하려면 [6단계: 문서의 개정 기록 보기](getting-started.nodejs.step-6.md)을 참조하세요.

# 6단계: 문서의 개정 기록 보기
<a name="getting-started.nodejs.step-6"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

[이전 단계](getting-started.nodejs.step-5.md)에서 차량의 등록 데이터를 수정한 후, 등록된 모든 소유자 및 기타 업데이트된 필드의 기록을 쿼리할 수 있습니다. 이 단계에서는 `vehicle-registration` 원장의 `VehicleRegistration` 테이블에 있는 문서의 개정 기록을 쿼리합니다.

**개정 기록을 보려면**

1. 다음 프로그램(`QueryHistory.ts`)을 사용하여 VIN `1N4AL11D75C109151`로 `VehicleRegistration` 문서의 개정 기록을 쿼리합니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { dom } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { VEHICLE_REGISTRATION } from "./model/SampleData";
   import { VEHICLE_REGISTRATION_TABLE_NAME } from "./qldb/Constants";
   import { prettyPrintResultList } from "./ScanTable";
   import { error, log } from "./qldb/LogUtil";
   import { getDocumentId } from "./qldb/Util";
   
   /**
    * Find previous primary owners for the given VIN in a single transaction.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin The VIN to find previous primary owners for.
    * @returns Promise which fulfills with void.
    */
   async function previousPrimaryOwners(txn: TransactionExecutor, vin: string): Promise<void> {
       const documentId: string = await getDocumentId(txn, VEHICLE_REGISTRATION_TABLE_NAME, "VIN", vin);
       const todaysDate: Date = new Date();
       // set todaysDate back one minute to ensure end time is in the past
       // by the time the request reaches our backend
       todaysDate.setMinutes(todaysDate.getMinutes() - 1);
       const threeMonthsAgo: Date = new Date(todaysDate);
       threeMonthsAgo.setMonth(todaysDate.getMonth() - 3);
   
       const query: string =
           `SELECT data.Owners.PrimaryOwner, metadata.version FROM history ` +
           `(${VEHICLE_REGISTRATION_TABLE_NAME}, \`${threeMonthsAgo.toISOString()}\`, \`${todaysDate.toISOString()}\`) ` +
           `AS h WHERE h.metadata.id = ?`;
   
       await txn.execute(query, documentId).then((result: Result) => {
           log(`Querying the 'VehicleRegistration' table's history using VIN: ${vin}.`);
           const resultList: dom.Value[] = result.getResultList();
           prettyPrintResultList(resultList);
       });
   }
   
   /**
    * Query a table's history for a particular set of documents.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbDriver: QldbDriver = getQldbDriver();
           const vin: string = VEHICLE_REGISTRATION[0].VIN;
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await previousPrimaryOwners(txn, vin);
           });
       } catch (e) {
           error(`Unable to query history to find previous owners: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
다음 구문에 내장된 [기록 함수](working.history.md#working.history.function)를 쿼리하여 문서의 개정 기록을 볼 수 있습니다  

     ```
     SELECT * FROM history( table_name [, `start-time` [, `end-time` ] ] ) AS h
     [ WHERE h.metadata.id = 'id' ]
     ```
*시작 시간*과 *종료 시간*은 모두 선택 사항입니다. 이 값은 백틱(``...``)으로 표시할 수 있는 Amazon Ion 리터럴 값입니다. 자세한 내용은 [Amazon QLDB에서 PartiQL을 사용한 Ion 쿼리](ql-reference.query.md)을 참조하세요.
가장 좋은 방법은 날짜 범위(*시작 시간* 및 *종료 시간*)와 문서 ID(`metadata.id`)를 모두 사용하여 기록 쿼리를 한정하는 것입니다. QLDB는 트랜잭션에서 `SELECT` 쿼리를 처리하며, 이 쿼리에는 [트랜잭션 시간 초과 제한](limits.md#limits.fixed)이 적용됩니다.  
QLDB 기록은 문서 ID로 인덱싱되며, 이번에는 추가 기록 인덱스를 만들 수 없습니다. 시작 시간과 종료 시간을 포함하는 기록 쿼리는 날짜 범위 한정이라는 이점을 갖습니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/QueryHistory.js
   ```

`vehicle-registration` 원장의 문서 개정을 암호화 방식으로 검증하려면 [7단계: 원장에 있는 문서 검증](getting-started.nodejs.step-7.md)으로 진행하세요.

# 7단계: 원장에 있는 문서 검증
<a name="getting-started.nodejs.step-7"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB를 사용하면 SHA-256 암호화 해싱을 사용하여 원장 저널에 있는 문서의 무결성을 효율적으로 검증할 수 있습니다. QLDB에서 검증 및 암호화 해싱이 작동하는 방식에 대한 자세한 내용은 [Amazon QLDB에서의 데이터 확인](verification.md)을 참조하세요.

이 단계에서는 `vehicle-registration` 원장의 `VehicleRegistration` 테이블에 있는 문서 개정을 검증합니다. 먼저 다이제스트를 요청합니다. 다이제스트는 출력 파일로 반환되며 원장의 전체 변경 내역에 대한 서명 역할을 합니다. 그런 다음 해당 다이제스트와 관련된 개정 증거를 요청합니다. 이 증거를 사용하면 모든 유효성 검사를 통과한 경우 개정 내용의 무결성을 확인할 수 있습니다.

**문서 개정을 검증하려면**

1. 검증에 필요한 QLDB 객체가 들어 있는 다음 `.ts` 파일을 검토하세요.

   1. `BlockAddress.ts`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { ValueHolder } from "aws-sdk/clients/qldb";
      import { dom, IonTypes } from "ion-js";
      
      export class BlockAddress {
          _strandId: string;
          _sequenceNo: number;
      
          constructor(strandId: string, sequenceNo: number) {
              this._strandId = strandId;
              this._sequenceNo = sequenceNo;
          }
      }
      
      /**
       * Convert a block address from an Ion value into a ValueHolder.
       * Shape of the ValueHolder must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"}
       * @param value The Ion value that contains the block address values to convert.
       * @returns The ValueHolder that contains the strandId and sequenceNo.
       */
      export function blockAddressToValueHolder(value: dom.Value): ValueHolder {
          const blockAddressValue : dom.Value = getBlockAddressValue(value);
          const strandId: string = getStrandId(blockAddressValue);
          const sequenceNo: number = getSequenceNo(blockAddressValue);
          const valueHolder: string = `{strandId: "${strandId}", sequenceNo: ${sequenceNo}}`;
          const blockAddress: ValueHolder = {IonText: valueHolder};
          return blockAddress;
      }
      
      /**
       * Helper method that to get the Metadata ID.
       * @param value The Ion value.
       * @returns The Metadata ID.
       */
      export function getMetadataId(value: dom.Value): string {
          const metaDataId: dom.Value = value.get("id");
          if (metaDataId === null) {
              throw new Error(`Expected field name id, but not found.`);
          }
          return metaDataId.stringValue();
      }
      
      /**
       * Helper method to get the Sequence No.
       * @param value The Ion value.
       * @returns The Sequence No.
       */
      export function getSequenceNo(value : dom.Value): number {
          const sequenceNo: dom.Value = value.get("sequenceNo");
          if (sequenceNo === null) {
              throw new Error(`Expected field name sequenceNo, but not found.`);
          }
          return sequenceNo.numberValue();
      }
      
      /**
       * Helper method to get the Strand ID.
       * @param value The Ion value.
       * @returns The Strand ID.
       */
      export function getStrandId(value: dom.Value): string {
          const strandId: dom.Value = value.get("strandId");
          if (strandId === null) {
              throw new Error(`Expected field name strandId, but not found.`);
          }
          return strandId.stringValue();
      }
      
      export function getBlockAddressValue(value: dom.Value) : dom.Value {
          const type = value.getType();
          if (type !== IonTypes.STRUCT) {
              throw new Error(`Unexpected format: expected struct, but got IonType: ${type.name}`);
          }
          const blockAddress: dom.Value = value.get("blockAddress");
          if (blockAddress == null) {
              throw new Error(`Expected field name blockAddress, but not found.`);
          }
          return blockAddress;
      }
      ```

   1. `Verifier.ts`

      ```
      /*
       * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
       * SPDX-License-Identifier: MIT-0
       *
       * Permission is hereby granted, free of charge, to any person obtaining a copy of this
       * software and associated documentation files (the "Software"), to deal in the Software
       * without restriction, including without limitation the rights to use, copy, modify,
       * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
       * permit persons to whom the Software is furnished to do so.
       *
       * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
       * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
       * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
       * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
       * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
       * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
       */
      
      import { Digest, ValueHolder } from "aws-sdk/clients/qldb";
      import { createHash } from "crypto";
      import { dom, toBase64 } from "ion-js";
      
      import { getBlobValue } from "./Util";
      
      const HASH_LENGTH: number = 32;
      const UPPER_BOUND: number = 8;
      
      /**
       * Build the candidate digest representing the entire ledger from the Proof hashes.
       * @param proof The Proof object.
       * @param leafHash The revision hash to pair with the first hash in the Proof hashes list.
       * @returns The calculated root hash.
       */
      function buildCandidateDigest(proof: ValueHolder, leafHash: Uint8Array): Uint8Array {
          const parsedProof: Uint8Array[] = parseProof(proof);
          const rootHash: Uint8Array = calculateRootHashFromInternalHash(parsedProof, leafHash);
          return rootHash;
      }
      
      /**
       * Combine the internal hashes and the leaf hash until only one root hash remains.
       * @param internalHashes An array of hash values.
       * @param leafHash The revision hash to pair with the first hash in the Proof hashes list.
       * @returns The root hash constructed by combining internal hashes.
       */
      function calculateRootHashFromInternalHash(internalHashes: Uint8Array[], leafHash: Uint8Array): Uint8Array {
          const rootHash: Uint8Array = internalHashes.reduce(joinHashesPairwise, leafHash);
          return rootHash;
      }
      
      /**
       * Compare two hash values by converting each Uint8Array byte, which is unsigned by default,
       * into a signed byte, assuming they are little endian.
       * @param hash1 The hash value to compare.
       * @param hash2 The hash value to compare.
       * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes.
       */
      function compareHashValues(hash1: Uint8Array, hash2: Uint8Array): number {
          if (hash1.length !== HASH_LENGTH || hash2.length !== HASH_LENGTH) {
              throw new Error("Invalid hash.");
          }
          for (let i = hash1.length-1; i >= 0; i--) {
              const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24);
              if (difference !== 0) {
                  return difference;
              }
          }
          return 0;
      }
      
      /**
       * Helper method that concatenates two Uint8Array.
       * @param arrays List of array to concatenate, in the order provided.
       * @returns The concatenated array.
       */
      function concatenate(...arrays: Uint8Array[]): Uint8Array {
          let totalLength = 0;
          for (const arr of arrays) {
              totalLength += arr.length;
          }
          const result = new Uint8Array(totalLength);
          let offset = 0;
          for (const arr of arrays) {
              result.set(arr, offset);
              offset += arr.length;
          }
          return result;
      }
      
      /**
       * Flip a single random bit in the given hash value.
       * This method is intended to be used for purpose of demonstrating the QLDB verification features only.
       * @param original The hash value to alter.
       * @returns The altered hash with a single random bit changed.
       */
      export function flipRandomBit(original: any): Uint8Array {
          if (original.length === 0) {
              throw new Error("Array cannot be empty!");
          }
          const bytePos: number = Math.floor(Math.random() * original.length);
          const bitShift: number = Math.floor(Math.random() * UPPER_BOUND);
          const alteredHash: Uint8Array = original;
      
          alteredHash[bytePos] = alteredHash[bytePos] ^ (1 << bitShift);
          return alteredHash;
      }
      
      /**
       * Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values.
       * @param h1 Byte array containing one of the hashes to compare.
       * @param h2 Byte array containing one of the hashes to compare.
       * @returns The concatenated array of hashes.
       */
      export function joinHashesPairwise(h1: Uint8Array, h2: Uint8Array): Uint8Array {
          if (h1.length === 0) {
              return h2;
          }
          if (h2.length === 0) {
              return h1;
          }
          let concat: Uint8Array;
          if (compareHashValues(h1, h2) < 0) {
              concat = concatenate(h1, h2);
          } else {
              concat = concatenate(h2, h1);
          }
          const hash = createHash('sha256');
          hash.update(concat);
          const newDigest: Uint8Array = hash.digest();
          return newDigest;
      }
      
      /**
       * Parse the Block object returned by QLDB and retrieve block hash.
       * @param valueHolder A structure containing an Ion string value.
       * @returns The block hash.
       */
      export function parseBlock(valueHolder: ValueHolder): Uint8Array {
          const block: dom.Value = dom.load(valueHolder.IonText);
          const blockHash: Uint8Array = getBlobValue(block, "blockHash");
          return blockHash;
      }
      
      /**
       * Parse the Proof object returned by QLDB into an iterator.
       * The Proof object returned by QLDB is a dictionary like the following:
       * {'IonText': '[{{<hash>}},{{<hash>}}]'}
       * @param valueHolder A structure containing an Ion string value.
       * @returns A list of hash values.
       */
      function parseProof(valueHolder: ValueHolder): Uint8Array[] {
          const proofs : dom.Value = dom.load(valueHolder.IonText);
          return proofs.elements().map(proof => proof.uInt8ArrayValue());
      }
      
      /**
       *  Verify document revision against the provided digest.
       * @param documentHash The SHA-256 value representing the document revision to be verified.
       * @param digest The SHA-256 hash value representing the ledger digest.
       * @param proof The Proof object retrieved from GetRevision.getRevision.
       * @returns If the document revision verifies against the ledger digest.
       */
      export function verifyDocument(documentHash: Uint8Array, digest: Digest, proof: ValueHolder): boolean {
          const candidateDigest = buildCandidateDigest(proof, documentHash);
          return (toBase64(<Uint8Array> digest) === toBase64(candidateDigest));
      }
      ```

1. 두 `.ts` 프로그램(`GetDigest.ts` 및 `GetRevision.ts`)을 사용하여 다음 단계를 수행하세요.
   + `vehicle-registration` 원장에 새 다이제스트를 요청하세요.
   + `VehicleRegistration` 테이블에서 VIN `1N4AL11D75C109151`이 포함된 문서의 각 개정에 대한 증거를 요청합니다.
   + 반환된 다이제스트와 증거를 사용하여 다이제스트를 다시 계산하여 개정 내용을 검증합니다.

   `GetDigest.ts` 프로그램에는 다음 코드가 포함되어 있습니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QLDB } from "aws-sdk";
   import { GetDigestRequest, GetDigestResponse } from "aws-sdk/clients/qldb";
   
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { digestResponseToString } from "./qldb/Util";
   
   /**
    * Get the digest of a ledger's journal.
    * @param ledgerName Name of the ledger to operate on.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a GetDigestResponse.
    */
   export async function getDigestResult(ledgerName: string, qldbClient: QLDB): Promise<GetDigestResponse> {
       const request: GetDigestRequest = {
           Name: ledgerName
       };
       const result: GetDigestResponse = await qldbClient.getDigest(request).promise();
       return result;
   }
   
   /**
    * This is an example for retrieving the digest of a particular ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           log(`Retrieving the current digest for ledger: ${LEDGER_NAME}.`);
           const digest: GetDigestResponse = await getDigestResult(LEDGER_NAME, qldbClient);
           log(`Success. Ledger digest: \n${digestResponseToString(digest)}.`);
       } catch (e) {
           error(`Unable to get a ledger digest: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
`getDigest` 함수를 사용하여 원장에 있는 저널의 현재 *팁*을 포함하는 다이제스트를 요청합니다. 저널 팁은 QLDB가 요청을 수신한 시점을 기준으로 가장 최근에 커밋된 블록을 나타냅니다.

   `GetRevision.ts` 프로그램에는 다음 코드가 포함되어 있습니다.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs";
   import { QLDB } from "aws-sdk";
   import { Digest, GetDigestResponse, GetRevisionRequest, GetRevisionResponse, ValueHolder } from "aws-sdk/clients/qldb";
   import { dom, toBase64 } from "ion-js";
   
   import { getQldbDriver } from "./ConnectToLedger";
   import { getDigestResult } from './GetDigest';
   import { VEHICLE_REGISTRATION } from "./model/SampleData"
   import { blockAddressToValueHolder, getMetadataId } from './qldb/BlockAddress';
   import { LEDGER_NAME } from './qldb/Constants';
   import { error, log } from "./qldb/LogUtil";
   import { getBlobValue, valueHolderToString } from "./qldb/Util";
   import { flipRandomBit, verifyDocument } from "./qldb/Verifier";
   
   /**
    * Get the revision data object for a specified document ID and block address.
    * Also returns a proof of the specified revision for verification.
    * @param ledgerName Name of the ledger containing the document to query.
    * @param documentId Unique ID for the document to be verified, contained in the committed view of the document.
    * @param blockAddress The location of the block to request.
    * @param digestTipAddress The latest block location covered by the digest.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with a GetRevisionResponse.
    */
   async function getRevision(
       ledgerName: string,
       documentId: string,
       blockAddress: ValueHolder,
       digestTipAddress: ValueHolder,
       qldbClient: QLDB
   ): Promise<GetRevisionResponse> {
       const request: GetRevisionRequest = {
           Name: ledgerName,
           BlockAddress: blockAddress,
           DocumentId: documentId,
           DigestTipAddress: digestTipAddress
       };
       const result: GetRevisionResponse = await qldbClient.getRevision(request).promise();
       return result;
   }
   
   /**
    * Query the table metadata for a particular vehicle for verification.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param vin VIN to query the table metadata of a specific registration with.
    * @returns Promise which fulfills with a list of Ion values that contains the results of the query.
    */
   export async function lookupRegistrationForVin(txn: TransactionExecutor, vin: string): Promise<dom.Value[]> {
       log(`Querying the 'VehicleRegistration' table for VIN: ${vin}...`);
       let resultList: dom.Value[];
       const query: string = "SELECT blockAddress, metadata.id FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?";
   
       await txn.execute(query, vin).then(function(result) {
         resultList = result.getResultList();
       });
       return resultList;
   }
   
   /**
    * Verify each version of the registration for the given VIN.
    * @param txn The {@linkcode TransactionExecutor} for lambda execute.
    * @param ledgerName The ledger to get the digest from.
    * @param vin VIN to query the revision history of a specific registration with.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with void.
    * @throws Error: When verification fails.
    */
   export async function verifyRegistration(
       txn: TransactionExecutor, 
       ledgerName: string, 
       vin: string,
       qldbClient: QLDB
   ): Promise<void> {
       log(`Let's verify the registration with VIN = ${vin}, in ledger = ${ledgerName}.`);
       const digest: GetDigestResponse = await getDigestResult(ledgerName, qldbClient);
       const digestBytes: Digest = digest.Digest;
       const digestTipAddress: ValueHolder = digest.DigestTipAddress;
   
       log(
           `Got a ledger digest: digest tip address = \n${valueHolderToString(digestTipAddress)},
           digest = \n${toBase64(<Uint8Array> digestBytes)}.`
       );
       log(`Querying the registration with VIN = ${vin} to verify each version of the registration...`);
       const resultList: dom.Value[] = await lookupRegistrationForVin(txn, vin);
       log("Getting a proof for the document.");
   
       for (const result of resultList) {
           const blockAddress: ValueHolder =  blockAddressToValueHolder(result);
           const documentId: string = getMetadataId(result);
   
           const revisionResponse: GetRevisionResponse = await getRevision(
               ledgerName, 
               documentId, 
               blockAddress, 
               digestTipAddress,
               qldbClient
           );
   
           const revision: dom.Value = dom.load(revisionResponse.Revision.IonText);
           const documentHash: Uint8Array = getBlobValue(revision, "hash");
           const proof: ValueHolder = revisionResponse.Proof;
           log(`Got back a proof: ${valueHolderToString(proof)}.`);
   
           let verified: boolean = verifyDocument(documentHash, digestBytes, proof);
           if (!verified) {
              throw new Error("Document revision is not verified.");
           } else {
               log("Success! The document is verified.");
           }
           const alteredDocumentHash: Uint8Array = flipRandomBit(documentHash);
   
           log(
               `Flipping one bit in the document's hash and assert that the document is NOT verified.
               The altered document hash is: ${toBase64(alteredDocumentHash)}`
           );
           verified = verifyDocument(alteredDocumentHash, digestBytes, proof);
   
           if (verified) {
               throw new Error("Expected altered document hash to not be verified against digest.");
           } else {
               log("Success! As expected flipping a bit in the document hash causes verification to fail.");
           }
           log(`Finished verifying the registration with VIN = ${vin} in ledger = ${ledgerName}.`);
       }
   }
   
   /**
    * Verify the integrity of a document revision in a QLDB ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           const qldbDriver: QldbDriver = getQldbDriver();
   
           const registration = VEHICLE_REGISTRATION[0];
           const vin: string = registration.VIN;
   
           await qldbDriver.executeLambda(async (txn: TransactionExecutor) => {
               await verifyRegistration(txn, LEDGER_NAME, vin, qldbClient);
           });
       } catch (e) {
           error(`Unable to verify revision: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
`getRevision` 함수가 지정된 문서 개정에 대한 증거를 반환하면 이 프로그램은 클라이언트 측 API를 사용하여 해당 개정본을 확인합니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/GetRevision.js
   ```

`vehicle-registration` 원장을 더 이상 사용할 필요가 없는 경우 [8단계(선택 사항): 리소스 정리](getting-started.nodejs.step-8.md)로 진행하세요.

# 8단계(선택 사항): 리소스 정리
<a name="getting-started.nodejs.step-8"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

`vehicle-registration` 원장을 계속 사용할 수 있습니다. 그러나 더 이상 필요하지 않은 경우 삭제해야 합니다.

**원장을 삭제하려면**

1. 다음 프로그램(`DeleteLedger.ts`)을 사용하여 `vehicle-registration` 원장과 모든 내용을 삭제하세요.

   ```
   /*
    * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    * SPDX-License-Identifier: MIT-0
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy of this
    * software and associated documentation files (the "Software"), to deal in the Software
    * without restriction, including without limitation the rights to use, copy, modify,
    * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
    * permit persons to whom the Software is furnished to do so.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
    * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
    * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
   
   import { isResourceNotFoundException } from "amazon-qldb-driver-nodejs";
   import { AWSError, QLDB } from "aws-sdk";
   import { DeleteLedgerRequest, DescribeLedgerRequest } from "aws-sdk/clients/qldb";
   
   import { setDeletionProtection } from "./DeletionProtection";
   import { LEDGER_NAME } from "./qldb/Constants";
   import { error, log } from "./qldb/LogUtil";
   import { sleep } from "./qldb/Util";
   
   const LEDGER_DELETION_POLL_PERIOD_MS = 20000;
   
   /**
    * Send a request to QLDB to delete the specified ledger.
    * @param ledgerName Name of the ledger to be deleted.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with void.
    */
   export async function deleteLedger(ledgerName: string, qldbClient: QLDB): Promise<void> {
       log(`Attempting to delete the ledger with name: ${ledgerName}`);
       const request: DeleteLedgerRequest = {
           Name: ledgerName
       };
       await qldbClient.deleteLedger(request).promise();
       log("Success.");
   }
   
   /**
    * Wait for the ledger to be deleted.
    * @param ledgerName Name of the ledger to be deleted.
    * @param qldbClient The QLDB control plane client to use.
    * @returns Promise which fulfills with void.
    */
   export async function waitForDeleted(ledgerName: string, qldbClient: QLDB): Promise<void> {
       log("Waiting for the ledger to be deleted...");
       const request: DescribeLedgerRequest = {
           Name: ledgerName
       };
       let isDeleted: boolean = false;
       while (true) {
           await qldbClient.describeLedger(request).promise().catch((error: AWSError) => {
               if (isResourceNotFoundException(error)) {
                   isDeleted = true;
                   log("Success. Ledger is deleted.");
               }
           });
           if (isDeleted) {
               break;
           }
           log("The ledger is still being deleted. Please wait...");
           await sleep(LEDGER_DELETION_POLL_PERIOD_MS);
       }
   }
   
   /**
    * Delete a ledger.
    * @returns Promise which fulfills with void.
    */
   const main = async function(): Promise<void> {
       try {
           const qldbClient: QLDB = new QLDB();
           await setDeletionProtection(LEDGER_NAME, qldbClient, false);
           await deleteLedger(LEDGER_NAME, qldbClient);
           await waitForDeleted(LEDGER_NAME, qldbClient);
       } catch (e) {
           error(`Unable to delete the ledger: ${e}`);
       }
   }
   
   if (require.main === module) {
       main();
   }
   ```
**참고**  
원장에 대해 삭제 방지가 활성화된 경우 QLDB API를 사용하여 원장을 삭제하기 전에 먼저 이를 비활성화해야 합니다.

1. 트랜스파일된 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   node dist/DeleteLedger.js
   ```

# Amazon QLDB Python 자습서
<a name="getting-started.python.tutorial"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서 샘플 애플리케이션 구현에서는 Amazon QLDB 드라이버 AWS SDK for Python (Boto3) 를와 함께 사용하여 QLDB 원장을 생성하고 샘플 데이터로 채웁니다.

이 자습서로 학습하면서 *Python용AWS SDK (Boto3) API 참조*에서 [QLDB 하위 수준 클라이언트](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/qldb.html)를 참조하여 관리 API 작업을 수행할 수 있습니다. 트랜잭션 데이터 작업의 경우 [Python용 QLDB 드라이버 API 참조](https://amazon-qldb-driver-python.readthedocs.io/en/stable/)를 참조할 수 있습니다.

**참고**  
해당하는 경우 일부 자습서 단계에는 Python용 QLDB 드라이버의 지원되는 각 메이저 버전마다 서로 다른 명령 또는 코드 예제가 있습니다.

**Topics**
+ [

# Amazon QLDB Python 샘플 애플리케이션 설치
](sample-app.python.md)
+ [

# 1단계: 새 원장 생성
](getting-started.python.step-1.md)
+ [

# 2단계: 원장과의 연결을 테스트
](getting-started.python.step-2.md)
+ [

# 3단계: 테이블, 인덱스 및 샘플 데이터 생성
](getting-started.python.step-3.md)
+ [

# 4단계: 원장에서 테이블 쿼리
](getting-started.python.step-4.md)
+ [

# 5단계: 원장의 문서 수정
](getting-started.python.step-5.md)
+ [

# 6단계: 문서의 개정 기록 보기
](getting-started.python.step-6.md)
+ [

# 7단계: 원장에 있는 문서 검증
](getting-started.python.step-7.md)
+ [

# 8단계(선택 사항): 리소스 정리
](getting-started.python.step-8.md)

# Amazon QLDB Python 샘플 애플리케이션 설치
<a name="sample-app.python"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 섹션에서는 단계별 Python 자습서를 위해 제공된 Amazon QLDB 샘플 애플리케이션을 설치하고 실행하는 방법을 설명합니다. 이 샘플 애플리케이션의 사용 사례는 차량 등록에 대한 전체 기록 정보를 추적하는 자동차 부서(DMV) 데이터베이스입니다.

Python용 DMV 샘플 애플리케이션은 GitHub 리포지토리 [aws-samples/amazon-qldb-dmv-sample-python](https://github.com/aws-samples/amazon-qldb-dmv-sample-python) 내 오픈 소스입니다.

## 사전 조건
<a name="sample-app.python.prereqs"></a>

시작하기 전에 Python [사전 조건](getting-started.python.md#getting-started.python.prereqs)용 QLDB 드라이버를 완료했는지 확인합니다. 여기에는 Python 설치 및 다음 작업이 포함됩니다.

1. 가입합니다 AWS.

1. 적절한 QLDB 권한을 가진 사용자를 생성합니다.

1. 개발을 위한 프로그래밍 방식 액세스 권한을 부여합니다.

이 자습서의 모든 단계를 완료하려면 QLDB API를 통해 원장 리소스에 대한 전체 관리 액세스 권한이 필요합니다.

## 설치
<a name="sample-app.python.install"></a>

**샘플 애플리케이션을 설치하려면**

1. 다음 `pip` 명령을 입력하여 GitHub에서 샘플 애플리케이션을 복제하고 설치합니다.

------
#### [ 3.x ]

   ```
   pip install git+https://github.com/aws-samples/amazon-qldb-dmv-sample-python.git
   ```

------
#### [ 2.x ]

   ```
   pip install git+https://github.com/aws-samples/amazon-qldb-dmv-sample-python.git@v1.0.0
   ```

------

   샘플 애플리케이션은 이 자습서의 전체 소스 코드와 Python 드라이버 및 [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-python)을 포함한 해당 종속성을 패키징합니다.

1. 명령줄에서 코드를 실행하기 전에 현재 작업 디렉터리를 `pyqldbsamples` 패키지가 설치된 위치로 전환하세요. 다음 명령을 입력합니다.

   ```
   cd $(python -c "import pyqldbsamples; print(pyqldbsamples.__path__[0])")
   ```

1. [1단계: 새 원장 생성](getting-started.python.step-1.md)로 진행하여 자습서를 시작하고 원장을 생성하세요.

# 1단계: 새 원장 생성
<a name="getting-started.python.step-1"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 단계에서는 `vehicle-registration`라는 이름의 새 Amazon QLDB 원장을 생성합니다

**새 원장을 생성하려면**

1. 이 자습서의 다른 모든 프로그램에서 사용하는 상수 값이 들어 있는 다음 파일(`constants.py`)을 검토하세요.

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   
   
   class Constants:
       """
       Constant values used throughout this tutorial.
       """
       LEDGER_NAME = "vehicle-registration"
   
       VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration"
       VEHICLE_TABLE_NAME = "Vehicle"
       PERSON_TABLE_NAME = "Person"
       DRIVERS_LICENSE_TABLE_NAME = "DriversLicense"
   
       LICENSE_NUMBER_INDEX_NAME = "LicenseNumber"
       GOV_ID_INDEX_NAME = "GovId"
       VEHICLE_VIN_INDEX_NAME = "VIN"
       LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber"
       PERSON_ID_INDEX_NAME = "PersonId"
   
       JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export"
       USER_TABLES = "information_schema.user_tables"
       S3_BUCKET_ARN_TEMPLATE = "arn:aws:s3:::"
       LEDGER_NAME_WITH_TAGS = "tags"
   
       RETRY_LIMIT = 4
   ```

1. 다음 프로그램(`create_ledger.py`)을 사용하여 이름이 `vehicle-registration`로 지정된 원장을 생성합니다.

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   from time import sleep
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   LEDGER_CREATION_POLL_PERIOD_SEC = 10
   ACTIVE_STATE = "ACTIVE"
   
   
   def create_ledger(name):
       """
       Create a new ledger with the specified name.
   
       :type name: str
       :param name: Name for the ledger to be created.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info("Let's create the ledger named: {}...".format(name))
       result = qldb_client.create_ledger(Name=name, PermissionsMode='ALLOW_ALL')
       logger.info('Success. Ledger state: {}.'.format(result.get('State')))
       return result
   
   
   def wait_for_active(name):
       """
       Wait for the newly created ledger to become active.
   
       :type name: str
       :param name: The ledger to check on.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info('Waiting for ledger to become active...')
       while True:
           result = qldb_client.describe_ledger(Name=name)
           if result.get('State') == ACTIVE_STATE:
               logger.info('Success. Ledger is active and ready to use.')
               return result
           logger.info('The ledger is still creating. Please wait...')
           sleep(LEDGER_CREATION_POLL_PERIOD_SEC)
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Create a ledger and wait for it to be active.
       """
       try:
           create_ledger(ledger_name)
           wait_for_active(ledger_name)
       except Exception as e:
           logger.exception('Unable to create the ledger!')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```
**참고**  
`create_ledger` 호출 시 원장 이름과 권한 모드를 지정해야 합니다. 원장 데이터의 보안을 극대화하려면 `STANDARD` 권한 모드를 사용할 것을 권장합니다.
원장을 생성할 때 기본적으로 *삭제 방지*가 활성화됩니다. 이 기능은 사용자가 원장을 삭제하는 것을 방지하는 QLDB의 기능입니다. QLDB API 또는 AWS Command Line Interface ()를 사용하여 원장 생성 시 삭제 방지 기능을 비활성화할 수 있습니다AWS CLI.
선택적으로, 원장에 첨부할 태그를 지정할 수도 있습니다.

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python create_ledger.py
   ```

새 원장과의 연결을 확인하려면 [2단계: 원장과의 연결을 테스트](getting-started.python.step-2.md)로 이동하세요.

# 2단계: 원장과의 연결을 테스트
<a name="getting-started.python.step-2"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 단계에서는 트랜잭션 데이터 API 엔드포인트를 사용하여 Amazon QLDB의 `vehicle-registration` 원장에 연결할 수 있는지 확인합니다.

**원장과의 연결을 테스트하려면**

1. 다음 프로그램(`connect_to_ledger.py`)을 사용하여 `vehicle-registration` 원장에 대한 데이터 세션 연결을 생성합니다.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from botocore.exceptions import ClientError
   
   from pyqldb.driver.qldb_driver import QldbDriver
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_qldb_driver(ledger_name=Constants.LEDGER_NAME, region_name=None, endpoint_url=None, boto3_session=None):
       """
       Create a QLDB driver for executing transactions.
   
       :type ledger_name: str
       :param ledger_name: The QLDB ledger name.
   
       :type region_name: str
       :param region_name: See [1].
   
       :type endpoint_url: str
       :param endpoint_url: See [1].
   
       :type boto3_session: :py:class:`boto3.session.Session`
       :param boto3_session: The boto3 session to create the client with (see [1]).
   
       :rtype: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :return: A QLDB driver object.
   
       [1]: `Boto3 Session.client Reference <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`.
       """
       qldb_driver = QldbDriver(ledger_name=ledger_name, region_name=region_name, endpoint_url=endpoint_url,
                                boto3_session=boto3_session)
       return qldb_driver
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Connect to a given ledger using default settings.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               logger.info('Listing table names ')
               for table in driver.list_tables():
                   logger.info(table)
       except ClientError as ce:
           logger.exception('Unable to list tables.')
           raise ce
   
   
   if __name__ == '__main__':
       main()
   ```

**참고**  
원장에서 데이터 트랜잭션을 실행하려면 특정 원장에 연결할 QLDB 드라이버 객체를 생성해야 합니다. 이는 이전 단계에서 원장을 생성할 때 사용한 `qldb_client` 객체와는 다른 클라이언트 객체입니다. 이전 클라이언트는 [Amazon QLDB API 참조](api-reference.md)에 나열된 관리 API 작업을 실행하는 데만 사용됩니다.
이 드라이버 객체를 생성할 때 원장 이름을 지정해야 합니다.

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from botocore.exceptions import ClientError
   
   from pyqldb.driver.pooled_qldb_driver import PooledQldbDriver
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_qldb_driver(ledger_name=Constants.LEDGER_NAME, region_name=None, endpoint_url=None, boto3_session=None):
       """
       Create a QLDB driver for creating sessions.
   
       :type ledger_name: str
       :param ledger_name: The QLDB ledger name.
   
       :type region_name: str
       :param region_name: See [1].
   
       :type endpoint_url: str
       :param endpoint_url: See [1].
   
       :type boto3_session: :py:class:`boto3.session.Session`
       :param boto3_session: The boto3 session to create the client with (see [1]).
   
       :rtype: :py:class:`pyqldb.driver.pooled_qldb_driver.PooledQldbDriver`
       :return: A pooled QLDB driver object.
   
       [1]: `Boto3 Session.client Reference <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`.
       """
       qldb_driver = PooledQldbDriver(ledger_name=ledger_name, region_name=region_name, endpoint_url=endpoint_url,
                                      boto3_session=boto3_session)
       return qldb_driver
   
   
   def create_qldb_session():
       """
       Retrieve a QLDB session object.
   
       :rtype: :py:class:`pyqldb.session.pooled_qldb_session.PooledQldbSession`
       :return: A pooled QLDB session object.
       """
       qldb_session = pooled_qldb_driver.get_session()
       return qldb_session
   
   
   pooled_qldb_driver = create_qldb_driver()
   
   
   if __name__ == '__main__':
       """
       Connect to a session for a given ledger using default settings.
       """
       try:
           qldb_session = create_qldb_session()
           logger.info('Listing table names ')
           for table in qldb_session.list_tables():
               logger.info(table)
       except ClientError:
           logger.exception('Unable to create session.')
   ```

**참고**  
원장에서 데이터 트랜잭션을 실행하려면 특정 원장에 연결할 QLDB 드라이버 객체를 생성해야 합니다. 이는 이전 단계에서 원장을 생성할 때 사용한 `qldb_client` 객체와는 다른 클라이언트 객체입니다. 이전 클라이언트는 [Amazon QLDB API 참조](api-reference.md)에 나열된 관리 API 작업을 실행하는 데만 사용됩니다.
먼저 풀링된 QLDB 드라이버 객체를 생성합니다. 이 드라이버를 생성할 때 원장 이름을 지정해야 합니다.
그러면 이 풀링된 드라이버 객체에서 세션을 생성할 수 있습니다.

------

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python connect_to_ledger.py
   ```

`vehicle-registration` 원장에 테이블을 생성하려면 [3단계: 테이블, 인덱스 및 샘플 데이터 생성](getting-started.python.step-3.md)로 진행하세요.

# 3단계: 테이블, 인덱스 및 샘플 데이터 생성
<a name="getting-started.python.step-3"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 원장이 활성화되고 연결을 수락하면 차량, 소유자 및 등록 정보에 대한 데이터 테이블 생성을 시작할 수 있습니다. 테이블과 인덱스를 생성한 후 데이터를 로드할 수 있습니다.

이 단계에서는 `vehicle-registration` 원장에 4개의 테이블을 생성합니다.
+ `VehicleRegistration`
+ `Vehicle`
+ `Person`
+ `DriversLicense`

또한 다음과 같은 인덱스를 생성합니다.


****  

| 테이블 이름 | 필드 | 
| --- | --- | 
| VehicleRegistration | VIN | 
| VehicleRegistration | LicensePlateNumber | 
| Vehicle | VIN | 
| Person | GovId | 
| DriversLicense | LicenseNumber | 
| DriversLicense | PersonId | 

샘플 데이터를 삽입할 때는 먼저 `Person` 테이블에 문서를 삽입합니다. 그런 다음 각 `Person` 문서에서 시스템이 할당한 `id`를 사용하여 적절한 `VehicleRegistration` 및 `DriversLicense` 문서의 해당 필드를 채웁니다.

**작은 정보**  
가장 좋은 방법은 문서의 시스템 할당 `id`를 외래 키로 사용하는 것입니다. 고유 식별자(예: 차량의 VIN)로 사용되는 필드를 정의할 수 있지만 문서의 실제  고유 식별자는 `id`입니다. 이 필드는 문서의 메타데이터에 포함되며 *커밋된 뷰*(테이블의 시스텀 정의 뷰)에서 쿼리할 수 있습니다.  
QLDB 뷰에 대한 자세한 내용은 [핵심 개념](ledger-structure.md) 섹션을 참조하세요. 메타데이터에 대해 자세히 알아보려면 [문서 메타데이터 쿼리](working.metadata.md) 섹션을 참조하세요.

**테이블 및 인덱스를 생성하려면**

1. 다음 프로그램(`create_table.py`)을 사용하여 앞서 언급한 테이블을 생성합니다.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_table(driver, table_name):
       """
       Create a table with the specified name.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type table_name: str
       :param table_name: Name of the table to create.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating the '{}' table...".format(table_name))
       statement = 'CREATE TABLE {}'.format(table_name)
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement))
       logger.info('{} table created successfully.'.format(table_name))
       return len(list(cursor))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Create registrations, vehicles, owners, and licenses tables.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               create_table(driver, Constants.DRIVERS_LICENSE_TABLE_NAME)
               create_table(driver, Constants.PERSON_TABLE_NAME)
               create_table(driver, Constants.VEHICLE_TABLE_NAME)
               create_table(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME)
               logger.info('Tables created successfully.')
       except Exception as e:
           logger.exception('Errors creating tables.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_table(transaction_executor, table_name):
       """
       Create a table with the specified name using an Executor object.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: Name of the table to create.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating the '{}' table...".format(table_name))
       statement = 'CREATE TABLE {}'.format(table_name)
       cursor = transaction_executor.execute_statement(statement)
       logger.info('{} table created successfully.'.format(table_name))
       return len(list(cursor))
   
   
   if __name__ == '__main__':
       """
       Create registrations, vehicles, owners, and licenses tables in a single transaction.
       """
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda x: create_table(x, Constants.DRIVERS_LICENSE_TABLE_NAME) and
                                      create_table(x, Constants.PERSON_TABLE_NAME) and
                                      create_table(x, Constants.VEHICLE_TABLE_NAME) and
                                      create_table(x, Constants.VEHICLE_REGISTRATION_TABLE_NAME),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Tables created successfully.')
       except Exception:
           logger.exception('Errors creating tables.')
   ```

------
**참고**  
이 프로그램은 `execute_lambda` 함수의 사용 방법을 설명합니다. 이 예제에서는 단일 Lambda 표현식을 사용하여 여러 `CREATE TABLE` PartiQL 문을 실행합니다.  
이 실행 함수는 암시적으로 트랜잭션을 시작하고 Lambda에서 모든 문을 실행한 다음 트랜잭션을 자동 커밋합니다.

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python create_table.py
   ```

1. 앞에서 설명한 대로 다음 프로그램(`create_index.py`)을 사용하여 테이블에 인덱스를 생성합니다.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_index(driver, table_name, index_attribute):
       """
       Create an index for a particular table.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type table_name: str
       :param table_name: Name of the table to add indexes for.
   
       :type index_attribute: str
       :param index_attribute: Index to create on a single attribute.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating index on '{}'...".format(index_attribute))
       statement = 'CREATE INDEX on {} ({})'.format(table_name, index_attribute)
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement))
       return len(list(cursor))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Create indexes on tables in a particular ledger.
       """
       logger.info('Creating indexes on all tables...')
       try:
           with create_qldb_driver(ledger_name) as driver:
               create_index(driver, Constants.PERSON_TABLE_NAME, Constants.GOV_ID_INDEX_NAME)
               create_index(driver, Constants.VEHICLE_TABLE_NAME, Constants.VEHICLE_VIN_INDEX_NAME)
               create_index(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.LICENSE_PLATE_NUMBER_INDEX_NAME)
               create_index(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, Constants.VEHICLE_VIN_INDEX_NAME)
               create_index(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.PERSON_ID_INDEX_NAME)
               create_index(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, Constants.LICENSE_NUMBER_INDEX_NAME)
               logger.info('Indexes created successfully.')
       except Exception as e:
           logger.exception('Unable to create indexes.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def create_index(transaction_executor, table_name, index_attribute):
       """
       Create an index for a particular table.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: Name of the table to add indexes for.
   
       :type index_attribute: str
       :param index_attribute: Index to create on a single attribute.
   
       :rtype: int
       :return: The number of changes to the database.
       """
       logger.info("Creating index on '{}'...".format(index_attribute))
       statement = 'CREATE INDEX on {} ({})'.format(table_name, index_attribute)
       cursor = transaction_executor.execute_statement(statement)
       return len(list(cursor))
   
   
   if __name__ == '__main__':
       """
       Create indexes on tables in a particular ledger.
       """
       logger.info('Creating indexes on all tables in a single transaction...')
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda x: create_index(x, Constants.PERSON_TABLE_NAME,
                                                             Constants.GOV_ID_INDEX_NAME)
                                      and create_index(x, Constants.VEHICLE_TABLE_NAME,
                                                       Constants.VEHICLE_VIN_INDEX_NAME)
                                      and create_index(x, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                                                       Constants.LICENSE_PLATE_NUMBER_INDEX_NAME)
                                      and create_index(x, Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                                                       Constants.VEHICLE_VIN_INDEX_NAME)
                                      and create_index(x, Constants.DRIVERS_LICENSE_TABLE_NAME,
                                                       Constants.PERSON_ID_INDEX_NAME)
                                      and create_index(x, Constants.DRIVERS_LICENSE_TABLE_NAME,
                                                       Constants.LICENSE_NUMBER_INDEX_NAME),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Indexes created successfully.')
       except Exception:
           logger.exception('Unable to create indexes.')
   ```

------

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python create_index.py
   ```

**데이터를 테이블로 로드하려면**

1. `vehicle-registration` 테이블에 삽입한 샘플 데이터를 나타내는 다음 파일(`sample_data.py`)을 검토하세요. 또한 이 파일은 `amazon.ion` 패키지에서 가져와서 [Amazon Ion](ion.md) 데이터를 변환, 구문 분석 및 인쇄하는 도우미 함수를 제공합니다.

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   from datetime import datetime
   from decimal import Decimal
   from logging import basicConfig, getLogger, INFO
   
   from amazon.ion.simple_types import IonPyBool, IonPyBytes, IonPyDecimal, IonPyDict, IonPyFloat, IonPyInt, IonPyList, \
       IonPyNull, IonPySymbol, IonPyText, IonPyTimestamp
   from amazon.ion.simpleion import dumps, loads
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   IonValue = (IonPyBool, IonPyBytes, IonPyDecimal, IonPyDict, IonPyFloat, IonPyInt, IonPyList, IonPyNull, IonPySymbol,
               IonPyText, IonPyTimestamp)
   
   
   class SampleData:
       """
       Sample domain objects for use throughout this tutorial.
       """
       DRIVERS_LICENSE = [
           {
               'PersonId': '',
               'LicenseNumber': 'LEWISR261LL',
               'LicenseType': 'Learner',
               'ValidFromDate': datetime(2016, 12, 20),
               'ValidToDate': datetime(2020, 11, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': 'LOGANB486CG',
               'LicenseType': 'Probationary',
               'ValidFromDate': datetime(2016, 4, 6),
               'ValidToDate': datetime(2020, 11, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': '744 849 301',
               'LicenseType': 'Full',
               'ValidFromDate': datetime(2017, 12, 6),
               'ValidToDate': datetime(2022, 10, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': 'P626-168-229-765',
               'LicenseType': 'Learner',
               'ValidFromDate': datetime(2017, 8, 16),
               'ValidToDate': datetime(2021, 11, 15)
           },
           {
               'PersonId': '',
               'LicenseNumber': 'S152-780-97-415-0',
               'LicenseType': 'Probationary',
               'ValidFromDate': datetime(2015, 8, 15),
               'ValidToDate': datetime(2021, 8, 21)
           }
       ]
       PERSON = [
           {
               'FirstName': 'Raul',
               'LastName': 'Lewis',
               'Address': '1719 University Street, Seattle, WA, 98109',
               'DOB': datetime(1963, 8, 19),
               'GovId': 'LEWISR261LL',
               'GovIdType': 'Driver License'
           },
           {
               'FirstName': 'Brent',
               'LastName': 'Logan',
               'DOB': datetime(1967, 7, 3),
               'Address': '43 Stockert Hollow Road, Everett, WA, 98203',
               'GovId': 'LOGANB486CG',
               'GovIdType': 'Driver License'
           },
           {
               'FirstName': 'Alexis',
               'LastName': 'Pena',
               'DOB': datetime(1974, 2, 10),
               'Address': '4058 Melrose Street, Spokane Valley, WA, 99206',
               'GovId': '744 849 301',
               'GovIdType': 'SSN'
           },
           {
               'FirstName': 'Melvin',
               'LastName': 'Parker',
               'DOB': datetime(1976, 5, 22),
               'Address': '4362 Ryder Avenue, Seattle, WA, 98101',
               'GovId': 'P626-168-229-765',
               'GovIdType': 'Passport'
           },
           {
               'FirstName': 'Salvatore',
               'LastName': 'Spencer',
               'DOB': datetime(1997, 11, 15),
               'Address': '4450 Honeysuckle Lane, Seattle, WA, 98101',
               'GovId': 'S152-780-97-415-0',
               'GovIdType': 'Passport'
           }
       ]
       VEHICLE = [
           {
               'VIN': '1N4AL11D75C109151',
               'Type': 'Sedan',
               'Year': 2011,
               'Make': 'Audi',
               'Model': 'A5',
               'Color': 'Silver'
           },
           {
               'VIN': 'KM8SRDHF6EU074761',
               'Type': 'Sedan',
               'Year': 2015,
               'Make': 'Tesla',
               'Model': 'Model S',
               'Color': 'Blue'
           },
           {
               'VIN': '3HGGK5G53FM761765',
               'Type': 'Motorcycle',
               'Year': 2011,
               'Make': 'Ducati',
               'Model': 'Monster 1200',
               'Color': 'Yellow'
           },
           {
               'VIN': '1HVBBAANXWH544237',
               'Type': 'Semi',
               'Year': 2009,
               'Make': 'Ford',
               'Model': 'F 150',
               'Color': 'Black'
           },
           {
               'VIN': '1C4RJFAG0FC625797',
               'Type': 'Sedan',
               'Year': 2019,
               'Make': 'Mercedes',
               'Model': 'CLK 350',
               'Color': 'White'
           }
       ]
       VEHICLE_REGISTRATION = [
           {
               'VIN': '1N4AL11D75C109151',
               'LicensePlateNumber': 'LEWISR261LL',
               'State': 'WA',
               'City': 'Seattle',
               'ValidFromDate': datetime(2017, 8, 21),
               'ValidToDate': datetime(2020, 5, 11),
               'PendingPenaltyTicketAmount': Decimal('90.25'),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': 'KM8SRDHF6EU074761',
               'LicensePlateNumber': 'CA762X',
               'State': 'WA',
               'City': 'Kent',
               'PendingPenaltyTicketAmount': Decimal('130.75'),
               'ValidFromDate': datetime(2017, 9, 14),
               'ValidToDate': datetime(2020, 6, 25),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': '3HGGK5G53FM761765',
               'LicensePlateNumber': 'CD820Z',
               'State': 'WA',
               'City': 'Everett',
               'PendingPenaltyTicketAmount': Decimal('442.30'),
               'ValidFromDate': datetime(2011, 3, 17),
               'ValidToDate': datetime(2021, 3, 24),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': '1HVBBAANXWH544237',
               'LicensePlateNumber': 'LS477D',
               'State': 'WA',
               'City': 'Tacoma',
               'PendingPenaltyTicketAmount': Decimal('42.20'),
               'ValidFromDate': datetime(2011, 10, 26),
               'ValidToDate': datetime(2023, 9, 25),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           },
           {
               'VIN': '1C4RJFAG0FC625797',
               'LicensePlateNumber': 'TH393F',
               'State': 'WA',
               'City': 'Olympia',
               'PendingPenaltyTicketAmount': Decimal('30.45'),
               'ValidFromDate': datetime(2013, 9, 2),
               'ValidToDate': datetime(2024, 3, 19),
               'Owners': {
                   'PrimaryOwner': {'PersonId': ''},
                   'SecondaryOwners': []
               }
           }
       ]
   
   
   def convert_object_to_ion(py_object):
       """
       Convert a Python object into an Ion object.
   
       :type py_object: object
       :param py_object: The object to convert.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyValue`
       :return: The converted Ion object.
       """
       ion_object = loads(dumps(py_object))
       return ion_object
   
   
   def to_ion_struct(key, value):
       """
       Convert the given key and value into an Ion struct.
   
       :type key: str
       :param key: The key which serves as an unique identifier.
   
       :type value: str
       :param value: The value associated with a given key.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The Ion dictionary object.
       """
       ion_struct = dict()
       ion_struct[key] = value
       return loads(str(ion_struct))
   
   
   def get_document_ids(transaction_executor, table_name, field, value):
       """
       Gets the document IDs from the given table.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: The table name to query.
   
       :type field: str
       :param field: A field to query.
   
       :type value: str
       :param value: The key of the given field.
   
       :rtype: list
       :return: A list of document IDs.
       """
       query = "SELECT id FROM {} AS t BY id WHERE t.{} = ?".format(table_name, field)
       cursor = transaction_executor.execute_statement(query, convert_object_to_ion(value))
       return list(map(lambda table: table.get('id'), cursor))
   
   
   def get_document_ids_from_dml_results(result):
       """
       Return a list of modified document IDs as strings from DML results.
   
       :type result: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :param: result: The result set from DML operation.
   
       :rtype: list
       :return: List of document IDs.
       """
       ret_val = list(map(lambda x: x.get('documentId'), result))
       return ret_val
   
   
   def print_result(cursor):
       """
       Pretty print the result set. Returns the number of documents in the result set.
   
       :type cursor: :py:class:`pyqldb.cursor.stream_cursor.StreamCursor`/
                     :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :param cursor: An instance of the StreamCursor or BufferedCursor class.
   
       :rtype: int
       :return: Number of documents in the result set.
       """
       result_counter = 0
       for row in cursor:
           # Each row would be in Ion format.
           print_ion(row)
           result_counter += 1
       return result_counter
   
   
   def print_ion(ion_value):
       """
       Pretty print an Ion Value.
   
       :type ion_value: :py:class:`amazon.ion.simple_types.IonPySymbol`
       :param ion_value: Any Ion Value to be pretty printed.
       """
       logger.info(dumps(ion_value, binary=False, indent='  ', omit_version_marker=True))
   ```
**참고**  
이 `get_document_ids` 함수는 테이블에서 시스템 할당 문서 ID를 반환하는 쿼리를 실행합니다. 자세한 내용은 [BY 절을 사용하여 문서 ID 쿼리하기](working.metadata.by-clause.md) 섹션을 참조하세요.

1. 다음 프로그램(`insert_document.py`)을 사용하여 샘플 데이터를 테이블에 삽입합니다.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion, SampleData, get_document_ids_from_dml_results
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def update_person_id(document_ids):
       """
       Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
   
       :type document_ids: list
       :param document_ids: List of document IDs.
   
       :rtype: list
       :return: Lists of updated DriversLicense records and updated VehicleRegistration records.
       """
       new_drivers_licenses = SampleData.DRIVERS_LICENSE.copy()
       new_vehicle_registrations = SampleData.VEHICLE_REGISTRATION.copy()
       for i in range(len(SampleData.PERSON)):
           drivers_license = new_drivers_licenses[i]
           registration = new_vehicle_registrations[i]
           drivers_license.update({'PersonId': str(document_ids[i])})
           registration['Owners']['PrimaryOwner'].update({'PersonId': str(document_ids[i])})
       return new_drivers_licenses, new_vehicle_registrations
   
   
   def insert_documents(driver, table_name, documents):
       """
       Insert the given list of documents into a table in a single transaction.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type table_name: str
       :param table_name: Name of the table to insert documents into.
   
       :type documents: list
       :param documents: List of documents to insert.
   
       :rtype: list
       :return: List of documents IDs for the newly inserted documents.
       """
       logger.info('Inserting some documents in the {} table...'.format(table_name))
       statement = 'INSERT INTO {} ?'.format(table_name)
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement,
                                                                                  convert_object_to_ion(documents)))
       list_of_document_ids = get_document_ids_from_dml_results(cursor)
   
       return list_of_document_ids
   
   
   def update_and_insert_documents(driver):
       """
       Handle the insertion of documents and updating PersonIds.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
       """
       list_ids = insert_documents(driver, Constants.PERSON_TABLE_NAME, SampleData.PERSON)
   
       logger.info("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...")
       new_licenses, new_registrations = update_person_id(list_ids)
   
       insert_documents(driver, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLE)
       insert_documents(driver, Constants.VEHICLE_REGISTRATION_TABLE_NAME, new_registrations)
       insert_documents(driver, Constants.DRIVERS_LICENSE_TABLE_NAME, new_licenses)
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Insert documents into a table in a QLDB ledger.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               # An INSERT statement creates the initial revision of a document with a version number of zero.
               # QLDB also assigns a unique document identifier in GUID format as part of the metadata.
               update_and_insert_documents(driver)
               logger.info('Documents inserted successfully!')
       except Exception as e:
           logger.exception('Error inserting or updating documents.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion, SampleData, get_document_ids_from_dml_results
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def update_person_id(document_ids):
       """
       Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records.
   
       :type document_ids: list
       :param document_ids: List of document IDs.
   
       :rtype: list
       :return: Lists of updated DriversLicense records and updated VehicleRegistration records.
       """
       new_drivers_licenses = SampleData.DRIVERS_LICENSE.copy()
       new_vehicle_registrations = SampleData.VEHICLE_REGISTRATION.copy()
       for i in range(len(SampleData.PERSON)):
           drivers_license = new_drivers_licenses[i]
           registration = new_vehicle_registrations[i]
           drivers_license.update({'PersonId': str(document_ids[i])})
           registration['Owners']['PrimaryOwner'].update({'PersonId': str(document_ids[i])})
       return new_drivers_licenses, new_vehicle_registrations
   
   
   def insert_documents(transaction_executor, table_name, documents):
       """
       Insert the given list of documents into a table in a single transaction.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type table_name: str
       :param table_name: Name of the table to insert documents into.
   
       :type documents: list
       :param documents: List of documents to insert.
   
       :rtype: list
       :return: List of documents IDs for the newly inserted documents.
       """
       logger.info('Inserting some documents in the {} table...'.format(table_name))
       statement = 'INSERT INTO {} ?'.format(table_name)
       cursor = transaction_executor.execute_statement(statement, convert_object_to_ion(documents))
       list_of_document_ids = get_document_ids_from_dml_results(cursor)
   
       return list_of_document_ids
   
   
   def update_and_insert_documents(transaction_executor):
       """
       Handle the insertion of documents and updating PersonIds all in a single transaction.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       """
       list_ids = insert_documents(transaction_executor, Constants.PERSON_TABLE_NAME, SampleData.PERSON)
   
       logger.info("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'...")
       new_licenses, new_registrations = update_person_id(list_ids)
   
       insert_documents(transaction_executor, Constants.VEHICLE_TABLE_NAME, SampleData.VEHICLE)
       insert_documents(transaction_executor, Constants.VEHICLE_REGISTRATION_TABLE_NAME, new_registrations)
       insert_documents(transaction_executor, Constants.DRIVERS_LICENSE_TABLE_NAME, new_licenses)
   
   
   if __name__ == '__main__':
       """
       Insert documents into a table in a QLDB ledger.
       """
       try:
           with create_qldb_session() as session:
               # An INSERT statement creates the initial revision of a document with a version number of zero.
               # QLDB also assigns a unique document identifier in GUID format as part of the metadata.
               session.execute_lambda(lambda executor: update_and_insert_documents(executor),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Documents inserted successfully!')
       except Exception:
           logger.exception('Error inserting or updating documents.')
   ```

------
**참고**  
이 프로그램은 파라미터화된 값을 사용하여 `execute_statement` 함수를 호출하는 방법을 보여줍니다. 실행하려는 PartiQL 문 외에도 데이터 파라미터를 전달할 수 있습니다. 명령문 문자열에서 물음표(`?`)를 변수 자리 표시자로 사용하세요.
`INSERT` 문이 성공하면 삽입된 각 문서의 `id`이 반환됩니다.

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python insert_document.py
   ```

다음으로 `SELECT` 문을 사용하여 `vehicle-registration` 원장의 테이블에서 데이터를 읽을 수 있습니다. [4단계: 원장에서 테이블 쿼리](getting-started.python.step-4.md)로 이동합니다.

# 4단계: 원장에서 테이블 쿼리
<a name="getting-started.python.step-4"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB 원장에 테이블을 생성하고 데이터를 로드한 후 쿼리를 실행하여 방금 삽입한 차량 등록 데이터를 검토할 수 있습니다. QLDB는 [PartiQL](ql-reference.md)을 쿼리 언어로 사용하고 [Amazon Ion](ion.md)을 문서 지향 데이터 모델로 사용합니다.

PartiQL은 Ion과 함께 작동하도록 확장된 오픈 소스 SQL 호환 쿼리 언어입니다. PartiQL을 사용하면 익숙한 SQL 연산자를 사용하여 데이터를 삽입, 쿼리 및 관리할 수 있습니다. Amazon Ion은 JSON의 상위 집합입니다. Ion은 정형, 반정형 및 중첩 데이터를 저장하고 처리할 수 있는 유연성을 제공하는 오픈 소스 문서 기반 데이터 형식입니다.

이 단계에서는 `SELECT` 명령문을 사용하여 `vehicle-registration` 원장의 테이블에서 데이터를 읽습니다.

**주의**  
인덱싱된 조회 없이 QLDB에서 쿼리를 실행하면 전체 테이블 스캔이 호출됩니다. PartiQL은 SQL과 호환되므로 이러한 쿼리를 지원합니다. 하지만 QLDB의 프로덕션 사용 사례에 대해서는 테이블 스캔을 실행하지 *마세요.*. 테이블 스캔은 동시성 충돌 및 트랜잭션 시간 초과를 포함하여 대규모 테이블에서 성능 문제를 일으킬 수 있습니다.  
인덱싱된 필드 또는 문서 ID(예: `WHERE indexedField = 123` 또는 `WHERE indexedField IN (456, 789)`)에서 동등 *연산자*를 사용하여 `WHERE` 조건자 절이 포함된 문을 실행하는 것이 좋습니다. 자세한 내용은 [쿼리 성능 최적화](working.optimize.md)을 참조하세요.

**테이블을 쿼리하려면**

1. 다음 프로그램(`find_vehicles.py`)을 사용하여 원장에 있는 사람의 이름으로 등록된 모든 차량을 쿼리하세요.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_vehicles_for_owner(driver, gov_id):
       """
       Find vehicles registered under a driver using their government ID.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type gov_id: str
       :param gov_id: The owner's government ID.
       """
       document_ids = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
                                                                              'GovId', gov_id))
   
       query = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " \
               "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?"
   
       for ids in document_ids:
           cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, ids))
           logger.info('List of Vehicles for owner with GovId: {}...'.format(gov_id))
           print_result(cursor)
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Find all vehicles registered under a person.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               # Find all vehicles registered under a person.
               gov_id = SampleData.PERSON[0]['GovId']
               find_vehicles_for_owner(driver, gov_id)
       except Exception as e:
           logger.exception('Error getting vehicles for owner.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_vehicles_for_owner(transaction_executor, gov_id):
       """
       Find vehicles registered under a driver using their government ID.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type gov_id: str
       :param gov_id: The owner's government ID.
       """
       document_ids = get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', gov_id)
   
       query = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " \
               "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?"
   
       for ids in document_ids:
           cursor = transaction_executor.execute_statement(query, ids)
           logger.info('List of Vehicles for owner with GovId: {}...'.format(gov_id))
           print_result(cursor)
   
   
   if __name__ == '__main__':
       """
       Find all vehicles registered under a person.
       """
       try:
           with create_qldb_session() as session:
               # Find all vehicles registered under a person.
               gov_id = SampleData.PERSON[0]['GovId']
               session.execute_lambda(lambda executor: find_vehicles_for_owner(executor, gov_id),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
       except Exception:
           logger.exception('Error getting vehicles for owner.')
   ```

------
**참고**  
먼저 이 프로그램은 이 문서에 대한 `Person` 테이블을 `GovId LEWISR261LL`로 쿼리하여 `id` 메타데이터 필드를 가져옵니다.  
그런 다음 이 문서 `id`를 외래 키로 사용하여 `VehicleRegistration` 테이블을 `PrimaryOwner.PersonId`로 쿼리합니다. 또한 `VIN` 필드의 `Vehicle` 테이블과 `VehicleRegistration`를 조인합니다.

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python find_vehicles.py
   ```

`vehicle-registration` 원장의 테이블에 있는 문서를 수정하는 방법에 대한 자세한 내용은 [5단계: 원장의 문서 수정](getting-started.python.step-5.md)을 참조하세요.

# 5단계: 원장의 문서 수정
<a name="getting-started.python.step-5"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이제 작업할 데이터가 있으므로 Amazon QLDB에서 `vehicle-registration` 원장에 있는 문서를 변경할 수 있습니다. 이 단계에서 다음 코드 예제는 DML(데이터 조작 언어) 문을 실행하는 방법을 보여줍니다. 이러한 명령문은 한 차량의 주 소유자를 업데이트하고 다른 차량에 보조 소유자를 추가합니다.

**문서를 수정하려면**

1. 다음 프로그램(`transfer_vehicle_ownership.py`)을 사용하여 차량의 주 소유자를 원장에 VIN `1N4AL11D75C109151`으로 업데이트하세요.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.add_secondary_owner import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_person_from_document_id(transaction_executor, document_id):
       """
       Query a driver's information using the given ID.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: The document ID required to query for the person.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       query = 'SELECT p.* FROM Person AS p BY pid WHERE pid = ?'
       cursor = transaction_executor.execute_statement(query, document_id)
       return next(cursor)
   
   
   def find_primary_owner_for_vehicle(driver, vin):
       """
       Find the primary owner of a vehicle given its VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: The VIN to find primary owner for.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       logger.info('Finding primary owner for vehicle with VIN: {}.'.format(vin))
       query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?"
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, convert_object_to_ion(vin)))
       try:
           return driver.execute_lambda(lambda executor: find_person_from_document_id(executor,
                                                                                      next(cursor).get('PersonId')))
       except StopIteration:
           logger.error('No primary owner registered for this vehicle.')
           return None
   
   
   def update_vehicle_registration(driver, vin, document_id):
       """
       Update the primary owner for a vehicle using the given VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: The VIN for the vehicle to operate on.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: New PersonId for the primary owner.
   
       :raises RuntimeError: If no vehicle registration was found using the given document ID and VIN.
       """
       logger.info('Updating the primary owner for vehicle with Vin: {}...'.format(vin))
       statement = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?"
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, document_id,
                                                                                  convert_object_to_ion(vin)))
       try:
           print_result(cursor)
           logger.info('Successfully transferred vehicle with VIN: {} to new owner.'.format(vin))
       except StopIteration:
           raise RuntimeError('Unable to transfer vehicle, could not find registration.')
   
   
   def validate_and_update_registration(driver, vin, current_owner, new_owner):
       """
       Validate the current owner of the given vehicle and transfer its ownership to a new owner.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: The VIN of the vehicle to transfer ownership of.
   
       :type current_owner: str
       :param current_owner: The GovId of the current owner of the vehicle.
   
       :type new_owner: str
       :param new_owner: The GovId of the new owner of the vehicle.
   
       :raises RuntimeError: If unable to verify primary owner.
       """
       primary_owner = find_primary_owner_for_vehicle(driver, vin)
       if primary_owner is None or primary_owner['GovId'] != current_owner:
           raise RuntimeError('Incorrect primary owner identified for vehicle, unable to transfer.')
   
       document_ids = driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME,
                                                                              'GovId', new_owner))
       update_vehicle_registration(driver, vin, document_ids[0])
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Find primary owner for a particular vehicle's VIN.
       Transfer to another primary owner for a particular vehicle's VIN.
       """
       vehicle_vin = SampleData.VEHICLE[0]['VIN']
       previous_owner = SampleData.PERSON[0]['GovId']
       new_owner = SampleData.PERSON[1]['GovId']
   
       try:
           with create_qldb_driver(ledger_name) as driver:
               validate_and_update_registration(driver, vehicle_vin, previous_owner, new_owner)
       except Exception as e:
           logger.exception('Error updating VehicleRegistration.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.add_secondary_owner import get_document_ids, print_result, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.model.sample_data import convert_object_to_ion
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def find_person_from_document_id(transaction_executor, document_id):
       """
       Query a driver's information using the given ID.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: The document ID required to query for the person.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       query = 'SELECT p.* FROM Person AS p BY pid WHERE pid = ?'
       cursor = transaction_executor.execute_statement(query, document_id)
       return next(cursor)
   
   
   def find_primary_owner_for_vehicle(transaction_executor, vin):
       """
       Find the primary owner of a vehicle given its VIN.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: The VIN to find primary owner for.
   
       :rtype: :py:class:`amazon.ion.simple_types.IonPyDict`
       :return: The resulting document from the query.
       """
       logger.info('Finding primary owner for vehicle with VIN: {}.'.format(vin))
       query = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?"
       cursor = transaction_executor.execute_statement(query, convert_object_to_ion(vin))
       try:
           return find_person_from_document_id(transaction_executor, next(cursor).get('PersonId'))
       except StopIteration:
           logger.error('No primary owner registered for this vehicle.')
           return None
   
   
   def update_vehicle_registration(transaction_executor, vin, document_id):
       """
       Update the primary owner for a vehicle using the given VIN.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: The VIN for the vehicle to operate on.
   
       :type document_id: :py:class:`amazon.ion.simple_types.IonPyText`
       :param document_id: New PersonId for the primary owner.
   
       :raises RuntimeError: If no vehicle registration was found using the given document ID and VIN.
       """
       logger.info('Updating the primary owner for vehicle with Vin: {}...'.format(vin))
       statement = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?"
       cursor = transaction_executor.execute_statement(statement, document_id, convert_object_to_ion(vin))
       try:
           print_result(cursor)
           logger.info('Successfully transferred vehicle with VIN: {} to new owner.'.format(vin))
       except StopIteration:
           raise RuntimeError('Unable to transfer vehicle, could not find registration.')
   
   
   def validate_and_update_registration(transaction_executor, vin, current_owner, new_owner):
       """
       Validate the current owner of the given vehicle and transfer its ownership to a new owner in a single transaction.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: The VIN of the vehicle to transfer ownership of.
   
       :type current_owner: str
       :param current_owner: The GovId of the current owner of the vehicle.
   
       :type new_owner: str
       :param new_owner: The GovId of the new owner of the vehicle.
   
       :raises RuntimeError: If unable to verify primary owner.
       """
       primary_owner = find_primary_owner_for_vehicle(transaction_executor, vin)
       if primary_owner is None or primary_owner['GovId'] != current_owner:
           raise RuntimeError('Incorrect primary owner identified for vehicle, unable to transfer.')
   
       document_id = next(get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', new_owner))
   
       update_vehicle_registration(transaction_executor, vin, document_id)
   
   
   if __name__ == '__main__':
       """
       Find primary owner for a particular vehicle's VIN.
       Transfer to another primary owner for a particular vehicle's VIN.
       """
       vehicle_vin = SampleData.VEHICLE[0]['VIN']
       previous_owner = SampleData.PERSON[0]['GovId']
       new_owner = SampleData.PERSON[1]['GovId']
   
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda executor: validate_and_update_registration(executor, vehicle_vin,
                                                                                        previous_owner, new_owner),
                                      retry_indicator=lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
       except Exception:
           logger.exception('Error updating VehicleRegistration.')
   ```

------

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python transfer_vehicle_ownership.py
   ```

1. 다음 프로그램(`add_secondary_owner.py`)을 사용하여 차량의 보조 소유자를 원장에 VIN `KM8SRDHF6EU074761`으로 추가하세요.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import to_ion_struct, get_document_ids, print_result, SampleData, \
       convert_object_to_ion
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def get_document_id_by_gov_id(driver, government_id):
       """
       Find a driver's person ID using the given government ID.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type government_id: str
       :param government_id: A driver's government ID.
   
       :rtype: list
       :return: A list of document IDs.
       """
       logger.info("Finding secondary owner's person ID using given government ID: {}.".format(government_id))
       return driver.execute_lambda(lambda executor: get_document_ids(executor, Constants.PERSON_TABLE_NAME, 'GovId',
                                                                      government_id))
   
   
   def is_secondary_owner_for_vehicle(driver, vin, secondary_owner_id):
       """
       Check whether a secondary owner has already been registered for the given VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN of the vehicle to query.
   
       :type secondary_owner_id: str
       :param secondary_owner_id: The secondary owner's person ID.
   
       :rtype: bool
       :return: If the driver has already been registered.
       """
       logger.info('Finding secondary owners for vehicle with VIN: {}...'.format(vin))
       query = 'SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?'
       rows = driver.execute_lambda(lambda executor: executor.execute_statement(query, convert_object_to_ion(vin)))
   
       for row in rows:
           secondary_owners = row.get('SecondaryOwners')
           person_ids = map(lambda owner: owner.get('PersonId').text, secondary_owners)
           if secondary_owner_id in person_ids:
               return True
       return False
   
   
   def add_secondary_owner_for_vin(driver, vin, parameter):
       """
       Add a secondary owner into `VehicleRegistration` table for a particular VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN of the vehicle to add a secondary owner for.
   
       :type parameter: :py:class:`amazon.ion.simple_types.IonPyValue`
       :param parameter: The Ion value or Python native type that is convertible to Ion for filling in parameters of the
                         statement.
       """
       logger.info('Inserting secondary owner for vehicle with VIN: {}...'.format(vin))
       statement = "FROM VehicleRegistration AS v WHERE v.VIN = ? INSERT INTO v.Owners.SecondaryOwners VALUE ?"
   
       cursor = driver.execute_lambda(lambda executor: executor.execute_statement(statement, convert_object_to_ion(vin),
                                                                                  parameter))
       logger.info('VehicleRegistration Document IDs which had secondary owners added: ')
       print_result(cursor)
   
   
   def register_secondary_owner(driver, vin, gov_id):
       """
       Register a secondary owner for a vehicle if they are not already registered.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN of the vehicle to register a secondary owner for.
   
       :type gov_id: str
       :param gov_id: The government ID of the owner.
       """
       logger.info('Finding the secondary owners for vehicle with VIN: {}.'.format(vin))
   
       document_ids = get_document_id_by_gov_id(driver, gov_id)
   
       for document_id in document_ids:
           if is_secondary_owner_for_vehicle(driver, vin, document_id):
               logger.info('Person with ID {} has already been added as a secondary owner of this vehicle.'.format(gov_id))
           else:
               add_secondary_owner_for_vin(driver, vin, to_ion_struct('PersonId', document_id))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Finds and adds secondary owners for a vehicle.
       """
       vin = SampleData.VEHICLE[1]['VIN']
       gov_id = SampleData.PERSON[0]['GovId']
       try:
           with create_qldb_driver(ledger_name) as driver:
               register_secondary_owner(driver, vin, gov_id)
               logger.info('Secondary owners successfully updated.')
       except Exception as e:
           logger.exception('Error adding secondary owner.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import to_ion_struct, get_document_ids, print_result, SampleData, \
       convert_object_to_ion
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def get_document_id_by_gov_id(transaction_executor, government_id):
       """
       Find a driver's person ID using the given government ID.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type government_id: str
       :param government_id: A driver's government ID.
       :rtype: list
       :return: A list of document IDs.
       """
       logger.info("Finding secondary owner's person ID using given government ID: {}.".format(government_id))
       return get_document_ids(transaction_executor, Constants.PERSON_TABLE_NAME, 'GovId', government_id)
   
   
   def is_secondary_owner_for_vehicle(transaction_executor, vin, secondary_owner_id):
       """
       Check whether a secondary owner has already been registered for the given VIN.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type vin: str
       :param vin: VIN of the vehicle to query.
       :type secondary_owner_id: str
       :param secondary_owner_id: The secondary owner's person ID.
       :rtype: bool
       :return: If the driver has already been registered.
       """
       logger.info('Finding secondary owners for vehicle with VIN: {}...'.format(vin))
       query = 'SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?'
       rows = transaction_executor.execute_statement(query, convert_object_to_ion(vin))
   
       for row in rows:
           secondary_owners = row.get('SecondaryOwners')
           person_ids = map(lambda owner: owner.get('PersonId').text, secondary_owners)
           if secondary_owner_id in person_ids:
               return True
       return False
   
   
   def add_secondary_owner_for_vin(transaction_executor, vin, parameter):
       """
       Add a secondary owner into `VehicleRegistration` table for a particular VIN.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type vin: str
       :param vin: VIN of the vehicle to add a secondary owner for.
       :type parameter: :py:class:`amazon.ion.simple_types.IonPyValue`
       :param parameter: The Ion value or Python native type that is convertible to Ion for filling in parameters of the
                         statement.
       """
       logger.info('Inserting secondary owner for vehicle with VIN: {}...'.format(vin))
       statement = "FROM VehicleRegistration AS v WHERE v.VIN = '{}' INSERT INTO v.Owners.SecondaryOwners VALUE ?"\
           .format(vin)
   
       cursor = transaction_executor.execute_statement(statement, parameter)
       logger.info('VehicleRegistration Document IDs which had secondary owners added: ')
       print_result(cursor)
   
   
   def register_secondary_owner(transaction_executor, vin, gov_id):
       """
       Register a secondary owner for a vehicle if they are not already registered.
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
       :type vin: str
       :param vin: VIN of the vehicle to register a secondary owner for.
       :type gov_id: str
       :param gov_id: The government ID of the owner.
       """
       logger.info('Finding the secondary owners for vehicle with VIN: {}.'.format(vin))
       document_ids = get_document_id_by_gov_id(transaction_executor, gov_id)
   
       for document_id in document_ids:
           if is_secondary_owner_for_vehicle(transaction_executor, vin, document_id):
               logger.info('Person with ID {} has already been added as a secondary owner of this vehicle.'.format(gov_id))
           else:
               add_secondary_owner_for_vin(transaction_executor, vin, to_ion_struct('PersonId', document_id))
   
   
   if __name__ == '__main__':
       """
       Finds and adds secondary owners for a vehicle.
       """
       vin = SampleData.VEHICLE[1]['VIN']
       gov_id = SampleData.PERSON[0]['GovId']
       try:
           with create_qldb_session() as session:
               session.execute_lambda(lambda executor: register_secondary_owner(executor, vin, gov_id),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Secondary owners successfully updated.')
       except Exception:
           logger.exception('Error adding secondary owner.')
   ```

------

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python add_secondary_owner.py
   ```

`vehicle-registration` 원장의 이러한 변경 사항을 검토하려면 [6단계: 문서의 개정 기록 보기](getting-started.python.step-6.md)을 참조하세요.

# 6단계: 문서의 개정 기록 보기
<a name="getting-started.python.step-6"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이전 단계에서 차량의 등록 데이터를 수정한 후 등록된 모든 소유자의 기록과 기타 업데이트된 필드를 쿼리할 수 있습니다. 이 단계에서는 `vehicle-registration` 원장의 `VehicleRegistration` 테이블에 있는 문서의 개정 기록을 쿼리합니다.

**개정 기록을 보려면**

1. 다음 프로그램(`query_history.py`)을 사용하여 VIN `1N4AL11D75C109151`로 `VehicleRegistration` 문서의 개정 기록을 쿼리합니다.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from datetime import datetime, timedelta
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import print_result, get_document_ids, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def format_date_time(date_time):
       """
       Format the given date time to a string.
   
       :type date_time: :py:class:`datetime.datetime`
       :param date_time: The date time to format.
   
       :rtype: str
       :return: The formatted date time.
       """
       return date_time.strftime('`%Y-%m-%dT%H:%M:%S.%fZ`')
   
   
   def previous_primary_owners(driver, vin):
       """
       Find previous primary owners for the given VIN in a single transaction.
       In this example, query the `VehicleRegistration` history table to find all previous primary owners for a VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN to find previous primary owners for.
       """
       person_ids = driver.execute_lambda(lambda executor: get_document_ids(executor,
                                                                            Constants.VEHICLE_REGISTRATION_TABLE_NAME,
                                                                            'VIN', vin))
   
       todays_date = datetime.utcnow() - timedelta(seconds=1)
       three_months_ago = todays_date - timedelta(days=90)
       query = 'SELECT data.Owners.PrimaryOwner, metadata.version FROM history({}, {}, {}) AS h WHERE h.metadata.id = ?'.\
           format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, format_date_time(three_months_ago),
                  format_date_time(todays_date))
   
       for ids in person_ids:
           logger.info("Querying the 'VehicleRegistration' table's history using VIN: {}.".format(vin))
           cursor = driver.execute_lambda(lambda executor: executor.execute_statement(query, ids))
           if not (print_result(cursor)) > 0:
               logger.info('No modification history found within the given time frame for document ID: {}'.format(ids))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Query a table's history for a particular set of documents.
       """
       try:
           with create_qldb_driver(ledger_name) as driver:
               vin = SampleData.VEHICLE_REGISTRATION[0]['VIN']
               previous_primary_owners(driver, vin)
               logger.info('Successfully queried history.')
       except Exception as e:
           logger.exception('Unable to query history to find previous owners.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from datetime import datetime, timedelta
   from logging import basicConfig, getLogger, INFO
   
   from pyqldbsamples.model.sample_data import print_result, get_document_ids, SampleData
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   
   
   def format_date_time(date_time):
       """
       Format the given date time to a string.
   
       :type date_time: :py:class:`datetime.datetime`
       :param date_time: The date time to format.
   
       :rtype: str
       :return: The formatted date time.
       """
       return date_time.strftime('`%Y-%m-%dT%H:%M:%S.%fZ`')
   
   
   def previous_primary_owners(transaction_executor, vin):
       """
       Find previous primary owners for the given VIN in a single transaction.
       In this example, query the `VehicleRegistration` history table to find all previous primary owners for a VIN.
   
       :type transaction_executor: :py:class:`pyqldb.execution.executor.Executor`
       :param transaction_executor: An Executor object allowing for execution of statements within a transaction.
   
       :type vin: str
       :param vin: VIN to find previous primary owners for.
       """
       person_ids = get_document_ids(transaction_executor, Constants.VEHICLE_REGISTRATION_TABLE_NAME, 'VIN', vin)
   
       todays_date = datetime.utcnow() - timedelta(seconds=1)
       three_months_ago = todays_date - timedelta(days=90)
       query = 'SELECT data.Owners.PrimaryOwner, metadata.version FROM history({}, {}, {}) AS h WHERE h.metadata.id = ?'.\
           format(Constants.VEHICLE_REGISTRATION_TABLE_NAME, format_date_time(three_months_ago),
                  format_date_time(todays_date))
   
       for ids in person_ids:
           logger.info("Querying the 'VehicleRegistration' table's history using VIN: {}.".format(vin))
           cursor = transaction_executor.execute_statement(query, ids)
           if not (print_result(cursor)) > 0:
               logger.info('No modification history found within the given time frame for document ID: {}'.format(ids))
   
   
   if __name__ == '__main__':
       """
       Query a table's history for a particular set of documents.
       """
       try:
           with create_qldb_session() as session:
               vin = SampleData.VEHICLE_REGISTRATION[0]['VIN']
               session.execute_lambda(lambda lambda_executor: previous_primary_owners(lambda_executor, vin),
                                      lambda retry_attempt: logger.info('Retrying due to OCC conflict...'))
               logger.info('Successfully queried history.')
       except Exception:
           logger.exception('Unable to query history to find previous owners.')
   ```

------
**참고**  
다음 구문에 내장된 [기록 함수](working.history.md#working.history.function)를 쿼리하여 문서의 개정 기록을 볼 수 있습니다  

     ```
     SELECT * FROM history( table_name [, `start-time` [, `end-time` ] ] ) AS h
     [ WHERE h.metadata.id = 'id' ]
     ```
*시작 시간*과 *종료 시간*은 모두 선택 사항입니다. 이 값은 백틱(``...``)으로 표시할 수 있는 Amazon Ion 리터럴 값입니다. 자세한 내용은 [Amazon QLDB에서 PartiQL을 사용한 Ion 쿼리](ql-reference.query.md) 섹션을 참조하세요.
가장 좋은 방법은 날짜 범위(*시작 시간* 및 *종료 시간*)와 문서 ID(`metadata.id`)를 모두 사용하여 기록 쿼리를 한정하는 것입니다. QLDB는 트랜잭션에서 `SELECT` 쿼리를 처리하며, 이 쿼리에는 [트랜잭션 시간 초과 제한](limits.md#limits.fixed)이 적용됩니다.  
QLDB 기록은 문서 ID로 인덱싱되며, 이번에는 추가 기록 인덱스를 만들 수 없습니다. 시작 시간과 종료 시간을 포함하는 기록 쿼리는 날짜 범위 한정이라는 이점을 갖습니다.

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python query_history.py
   ```

`vehicle-registration` 원장의 문서 개정을 암호화 방식으로 검증하려면 [7단계: 원장에 있는 문서 검증](getting-started.python.step-7.md)으로 진행하세요.

# 7단계: 원장에 있는 문서 검증
<a name="getting-started.python.step-7"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB를 사용하면 SHA-256 암호화 해싱을 사용하여 원장 저널에 있는 문서의 무결성을 효율적으로 검증할 수 있습니다. QLDB에서 검증 및 암호화 해싱이 작동하는 방식에 대한 자세한 내용은 [Amazon QLDB에서의 데이터 확인](verification.md)을 참조하세요.

이 단계에서는 `vehicle-registration` 원장의 `VehicleRegistration` 테이블에 있는 문서 개정을 검증합니다. 먼저 다이제스트를 요청합니다. 다이제스트는 출력 파일로 반환되며 원장의 전체 변경 내역에 대한 서명 역할을 합니다. 그런 다음 해당 다이제스트와 관련된 개정 증거를 요청합니다. 이 증거를 사용하면 모든 유효성 검사를 통과한 경우 개정 내용의 무결성을 확인할 수 있습니다.

**문서 개정을 검증하려면**

1. 검증에 필요한 QLDB 객체와 QLDB 응답 유형을 문자열로 변환하는 도우미 함수가 있는 유틸리티 모듈을 나타내는 다음 `.py` 파일을 검토하세요.

   1. `block_address.py`

      ```
      # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
      # SPDX-License-Identifier: MIT-0
      #
      # Permission is hereby granted, free of charge, to any person obtaining a copy of this
      # software and associated documentation files (the "Software"), to deal in the Software
      # without restriction, including without limitation the rights to use, copy, modify,
      # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
      # permit persons to whom the Software is furnished to do so.
      #
      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
      # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
      # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      
      
      def block_address_to_dictionary(ion_dict):
          """
          Convert a block address from IonPyDict into a dictionary.
          Shape of the dictionary must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"}
      
          :type ion_dict: :py:class:`amazon.ion.simple_types.IonPyDict`/str
          :param ion_dict: The block address value to convert.
      
          :rtype: dict
          :return: The converted dict.
          """
          block_address = {'IonText': {}}
          if not isinstance(ion_dict, str):
              py_dict = '{{strandId: "{}", sequenceNo:{}}}'.format(ion_dict['strandId'], ion_dict['sequenceNo'])
              ion_dict = py_dict
          block_address['IonText'] = ion_dict
          return block_address
      ```

   1. `verifier.py`

      ```
      # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
      # SPDX-License-Identifier: MIT-0
      #
      # Permission is hereby granted, free of charge, to any person obtaining a copy of this
      # software and associated documentation files (the "Software"), to deal in the Software
      # without restriction, including without limitation the rights to use, copy, modify,
      # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
      # permit persons to whom the Software is furnished to do so.
      #
      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
      # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
      # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      #
      # This code expects that you have AWS credentials setup per:
      # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
      from array import array
      from base64 import b64encode
      from functools import reduce
      from hashlib import sha256
      from random import randrange
      
      from amazon.ion.simpleion import loads
      
      HASH_LENGTH = 32
      UPPER_BOUND = 8
      
      
      def parse_proof(value_holder):
          """
          Parse the Proof object returned by QLDB into an iterator.
      
          The Proof object returned by QLDB is a dictionary like the following:
          {'IonText': '[{{<hash>}},{{<hash>}}]'}
      
          :type value_holder: dict
          :param value_holder: A structure containing an Ion string value.
      
          :rtype: :py:class:`amazon.ion.simple_types.IonPyList`
          :return: A list of hash values.
          """
          value_holder = value_holder.get('IonText')
          proof_list = loads(value_holder)
          return proof_list
      
      
      def parse_block(value_holder):
          """
          Parse the Block object returned by QLDB and retrieve block hash.
      
          :type value_holder: dict
          :param value_holder: A structure containing an Ion string value.
      
          :rtype: :py:class:`amazon.ion.simple_types.IonPyBytes`
          :return: The block hash.
          """
          value_holder = value_holder.get('IonText')
          block = loads(value_holder)
          block_hash = block.get('blockHash')
          return block_hash
      
      
      def flip_random_bit(original):
          """
          Flip a single random bit in the given hash value.
          This method is used to demonstrate QLDB's verification features.
      
          :type original: bytes
          :param original: The hash value to alter.
      
          :rtype: bytes
          :return: The altered hash with a single random bit changed.
          """
          assert len(original) != 0, 'Invalid bytes.'
      
          altered_position = randrange(len(original))
          bit_shift = randrange(UPPER_BOUND)
          altered_hash = bytearray(original).copy()
      
          altered_hash[altered_position] = altered_hash[altered_position] ^ (1 << bit_shift)
          return bytes(altered_hash)
      
      
      def compare_hash_values(hash1, hash2):
          """
          Compare two hash values by converting them into byte arrays, assuming they are little endian.
      
          :type hash1: bytes
          :param hash1: The hash value to compare.
      
          :type hash2: bytes
          :param hash2: The hash value to compare.
      
          :rtype: int
          :return: Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes.
          """
          assert len(hash1) == HASH_LENGTH
          assert len(hash2) == HASH_LENGTH
      
          hash_array1 = array('b', hash1)
          hash_array2 = array('b', hash2)
      
          for i in range(len(hash_array1) - 1, -1, -1):
              difference = hash_array1[i] - hash_array2[i]
              if difference != 0:
                  return difference
          return 0
      
      
      def join_hash_pairwise(hash1, hash2):
          """
          Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values.
      
          :type hash1: bytes
          :param hash1: Hash value to concatenate.
      
          :type hash2: bytes
          :param hash2: Hash value to concatenate.
      
          :rtype: bytes
          :return: The new hash value generated from concatenated hash values.
          """
          if len(hash1) == 0:
              return hash2
          if len(hash2) == 0:
              return hash1
      
          concatenated = hash1 + hash2 if compare_hash_values(hash1, hash2) < 0 else hash2 + hash1
          new_hash_lib = sha256()
          new_hash_lib.update(concatenated)
          new_digest = new_hash_lib.digest()
          return new_digest
      
      
      def calculate_root_hash_from_internal_hashes(internal_hashes, leaf_hash):
          """
          Combine the internal hashes and the leaf hash until only one root hash remains.
      
          :type internal_hashes: map
          :param internal_hashes: An iterable over a list of hash values.
      
          :type leaf_hash: bytes
          :param leaf_hash: The revision hash to pair with the first hash in the Proof hashes list.
      
          :rtype: bytes
          :return: The root hash constructed by combining internal hashes.
          """
          root_hash = reduce(join_hash_pairwise, internal_hashes, leaf_hash)
          return root_hash
      
      
      def build_candidate_digest(proof, leaf_hash):
          """
          Build the candidate digest representing the entire ledger from the Proof hashes.
      
          :type proof: dict
          :param proof: The Proof object.
      
          :type leaf_hash: bytes
          :param leaf_hash: The revision hash to pair with the first hash in the Proof hashes list.
      
          :rtype: bytes
          :return: The calculated root hash.
          """
          parsed_proof = parse_proof(proof)
          root_hash = calculate_root_hash_from_internal_hashes(parsed_proof, leaf_hash)
          return root_hash
      
      
      def verify_document(document_hash, digest, proof):
          """
          Verify document revision against the provided digest.
      
          :type document_hash: bytes
          :param document_hash: The SHA-256 value representing the document revision to be verified.
      
          :type digest: bytes
          :param digest: The SHA-256 hash value representing the ledger digest.
      
          :type proof: dict
          :param proof: The Proof object retrieved from :func:`pyqldbsamples.get_revision.get_revision`.
      
          :rtype: bool
          :return: If the document revision verify against the ledger digest.
          """
          candidate_digest = build_candidate_digest(proof, document_hash)
          return digest == candidate_digest
      
      
      def to_base_64(input):
          """
          Encode input in base64.
      
          :type input: bytes
          :param input: Input to be encoded.
      
          :rtype: string
          :return: Return input that has been encoded in base64.
          """
          encoded_value = b64encode(input)
          return str(encoded_value, 'UTF-8')
      ```

   1. `qldb_string_utils.py`

      ```
      # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
      # SPDX-License-Identifier: MIT-0
      #
      # Permission is hereby granted, free of charge, to any person obtaining a copy of this
      # software and associated documentation files (the "Software"), to deal in the Software
      # without restriction, including without limitation the rights to use, copy, modify,
      # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
      # permit persons to whom the Software is furnished to do so.
      #
      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
      # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
      # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      from amazon.ion.simpleion import dumps, loads
      
      
      def value_holder_to_string(value_holder):
          """
          Returns the string representation of a given `value_holder`.
      
          :type value_holder: dict
          :param value_holder: The `value_holder` to convert to string.
      
          :rtype: str
          :return: The string representation of the supplied `value_holder`.
          """
          ret_val = dumps(loads(value_holder), binary=False, indent='  ', omit_version_marker=True)
          val = '{{ IonText: {}}}'.format(ret_val)
          return val
      
      
      def block_response_to_string(block_response):
          """
          Returns the string representation of a given `block_response`.
      
          :type block_response: dict
          :param block_response: The `block_response` to convert to string.
      
          :rtype: str
          :return: The string representation of the supplied `block_response`.
          """
          string = ''
          if block_response.get('Block', {}).get('IonText') is not None:
              string += 'Block: ' + value_holder_to_string(block_response['Block']['IonText']) + ', '
      
          if block_response.get('Proof', {}).get('IonText') is not None:
              string += 'Proof: ' + value_holder_to_string(block_response['Proof']['IonText'])
      
          return '{' + string + '}'
      
      
      def digest_response_to_string(digest_response):
          """
          Returns the string representation of a given `digest_response`.
      
          :type digest_response: dict
          :param digest_response: The `digest_response` to convert to string.
      
          :rtype: str
          :return: The string representation of the supplied `digest_response`.
          """
          string = ''
          if digest_response.get('Digest') is not None:
              string += 'Digest: ' + str(digest_response['Digest']) + ', '
      
          if digest_response.get('DigestTipAddress', {}).get('IonText') is not None:
              string += 'DigestTipAddress: ' + value_holder_to_string(digest_response['DigestTipAddress']['IonText'])
      
          return '{' + string + '}'
      ```

1. 두 `.py` 프로그램(`get_digest.py` 및 `get_revision.py`)을 사용하여 다음 단계를 수행하세요.
   + `vehicle-registration` 원장에 새 다이제스트를 요청하세요.
   + `VehicleRegistration` 테이블에서 VIN `1N4AL11D75C109151`이 포함된 문서의 각 개정에 대한 증거를 요청합니다.
   + 반환된 다이제스트와 증거를 사용하여 다이제스트를 다시 계산하여 개정 내용을 검증합니다.

   `get_digest.py` 프로그램에는 다음 코드가 포함되어 있습니다.

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.qldb.qldb_string_utils import digest_response_to_string
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def get_digest_result(name):
       """
       Get the digest of a ledger's journal.
   
       :type name: str
       :param name: Name of the ledger to operate on.
   
       :rtype: dict
       :return: The digest in a 256-bit hash value and a block address.
       """
       logger.info("Let's get the current digest of the ledger named {}".format(name))
       result = qldb_client.get_digest(Name=name)
       logger.info('Success. LedgerDigest: {}.'.format(digest_response_to_string(result)))
       return result
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       This is an example for retrieving the digest of a particular ledger.
       """
       try:
           get_digest_result(ledger_name)
       except Exception as e:
           logger.exception('Unable to get a ledger digest!')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```
**참고**  
`get_digest_result` 함수를 사용하여 원장에 있는 저널의 현재 *팁*을 포함하는 다이제스트를 요청합니다. 저널 팁은 QLDB가 요청을 수신한 시점을 기준으로 가장 최근에 커밋된 블록을 나타냅니다.

   `get_revision.py` 프로그램에는 다음 코드가 포함되어 있습니다.

------
#### [ 3.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from amazon.ion.simpleion import loads
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.get_digest import get_digest_result
   from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion
   from pyqldbsamples.qldb.block_address import block_address_to_dictionary
   from pyqldbsamples.verifier import verify_document, flip_random_bit, to_base_64
   from pyqldbsamples.connect_to_ledger import create_qldb_driver
   from pyqldbsamples.qldb.qldb_string_utils import value_holder_to_string
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def get_revision(ledger_name, document_id, block_address, digest_tip_address):
       """
       Get the revision data object for a specified document ID and block address.
       Also returns a proof of the specified revision for verification.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger containing the document to query.
   
       :type document_id: str
       :param document_id: Unique ID for the document to be verified, contained in the committed view of the document.
   
       :type block_address: dict
       :param block_address: The location of the block to request.
   
       :type digest_tip_address: dict
       :param digest_tip_address: The latest block location covered by the digest.
   
       :rtype: dict
       :return: The response of the request.
       """
       result = qldb_client.get_revision(Name=ledger_name, BlockAddress=block_address, DocumentId=document_id,
                                         DigestTipAddress=digest_tip_address)
       return result
   
   
   def lookup_registration_for_vin(driver, vin):
       """
       Query revision history for a particular vehicle for verification.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :rtype: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :return: Cursor on the result set of the statement query.
       """
       logger.info("Querying the 'VehicleRegistration' table for VIN: {}...".format(vin))
       query = 'SELECT * FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?'
       return driver.execute_lambda(lambda txn: txn.execute_statement(query, convert_object_to_ion(vin)))
   
   
   def verify_registration(driver, ledger_name, vin):
       """
       Verify each version of the registration for the given VIN.
   
       :type driver: :py:class:`pyqldb.driver.qldb_driver.QldbDriver`
       :param driver: An instance of the QldbDriver class.
   
       :type ledger_name: str
       :param ledger_name: The ledger to get digest from.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :raises AssertionError: When verification failed.
       """
       logger.info("Let's verify the registration with VIN = {}, in ledger = {}.".format(vin, ledger_name))
       digest = get_digest_result(ledger_name)
       digest_bytes = digest.get('Digest')
       digest_tip_address = digest.get('DigestTipAddress')
   
       logger.info('Got a ledger digest: digest tip address = {}, digest = {}.'.format(
           value_holder_to_string(digest_tip_address.get('IonText')), to_base_64(digest_bytes)))
   
       logger.info('Querying the registration with VIN = {} to verify each version of the registration...'.format(vin))
       cursor = lookup_registration_for_vin(driver, vin)
       logger.info('Getting a proof for the document.')
   
       for row in cursor:
           block_address = row.get('blockAddress')
           document_id = row.get('metadata').get('id')
   
           result = get_revision(ledger_name, document_id, block_address_to_dictionary(block_address), digest_tip_address)
           revision = result.get('Revision').get('IonText')
           document_hash = loads(revision).get('hash')
   
           proof = result.get('Proof')
           logger.info('Got back a proof: {}.'.format(proof))
   
           verified = verify_document(document_hash, digest_bytes, proof)
           if not verified:
               raise AssertionError('Document revision is not verified.')
           else:
               logger.info('Success! The document is verified.')
   
           altered_document_hash = flip_random_bit(document_hash)
           logger.info("Flipping one bit in the document's hash and assert that the document is NOT verified. "
                       "The altered document hash is: {}.".format(to_base_64(altered_document_hash)))
           verified = verify_document(altered_document_hash, digest_bytes, proof)
           if verified:
               raise AssertionError('Expected altered document hash to not be verified against digest.')
           else:
               logger.info('Success! As expected flipping a bit in the document hash causes verification to fail.')
   
           logger.info('Finished verifying the registration with VIN = {} in ledger = {}.'.format(vin, ledger_name))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Verify the integrity of a document revision in a QLDB ledger.
       """
       registration = SampleData.VEHICLE_REGISTRATION[0]
       vin = registration['VIN']
       try:
           with create_qldb_driver(ledger_name) as driver:
               verify_registration(driver, ledger_name, vin)
       except Exception as e:
           logger.exception('Unable to verify revision.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

------
#### [ 2.x ]

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from amazon.ion.simpleion import loads
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.get_digest import get_digest_result
   from pyqldbsamples.model.sample_data import SampleData, convert_object_to_ion
   from pyqldbsamples.qldb.block_address import block_address_to_dictionary
   from pyqldbsamples.verifier import verify_document, flip_random_bit, to_base_64
   from pyqldbsamples.connect_to_ledger import create_qldb_session
   from pyqldbsamples.qldb.qldb_string_utils import value_holder_to_string
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def get_revision(ledger_name, document_id, block_address, digest_tip_address):
       """
       Get the revision data object for a specified document ID and block address.
       Also returns a proof of the specified revision for verification.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger containing the document to query.
   
       :type document_id: str
       :param document_id: Unique ID for the document to be verified, contained in the committed view of the document.
   
       :type block_address: dict
       :param block_address: The location of the block to request.
   
       :type digest_tip_address: dict
       :param digest_tip_address: The latest block location covered by the digest.
   
       :rtype: dict
       :return: The response of the request.
       """
       result = qldb_client.get_revision(Name=ledger_name, BlockAddress=block_address, DocumentId=document_id,
                                         DigestTipAddress=digest_tip_address)
       return result
   
   
   def lookup_registration_for_vin(qldb_session, vin):
       """
       Query revision history for a particular vehicle for verification.
   
       :type qldb_session: :py:class:`pyqldb.session.qldb_session.QldbSession`
       :param qldb_session: An instance of the QldbSession class.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :rtype: :py:class:`pyqldb.cursor.buffered_cursor.BufferedCursor`
       :return: Cursor on the result set of the statement query.
       """
       logger.info("Querying the 'VehicleRegistration' table for VIN: {}...".format(vin))
       query = 'SELECT * FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?'
       parameters = [convert_object_to_ion(vin)]
       cursor = qldb_session.execute_statement(query, parameters)
       return cursor
   
   
   def verify_registration(qldb_session, ledger_name, vin):
       """
       Verify each version of the registration for the given VIN.
   
       :type qldb_session: :py:class:`pyqldb.session.qldb_session.QldbSession`
       :param qldb_session: An instance of the QldbSession class.
   
       :type ledger_name: str
       :param ledger_name: The ledger to get digest from.
   
       :type vin: str
       :param vin: VIN to query the revision history of a specific registration with.
   
       :raises AssertionError: When verification failed.
       """
       logger.info("Let's verify the registration with VIN = {}, in ledger = {}.".format(vin, ledger_name))
       digest = get_digest_result(ledger_name)
       digest_bytes = digest.get('Digest')
       digest_tip_address = digest.get('DigestTipAddress')
   
       logger.info('Got a ledger digest: digest tip address = {}, digest = {}.'.format(
           value_holder_to_string(digest_tip_address.get('IonText')), to_base_64(digest_bytes)))
   
       logger.info('Querying the registration with VIN = {} to verify each version of the registration...'.format(vin))
       cursor = lookup_registration_for_vin(qldb_session, vin)
       logger.info('Getting a proof for the document.')
   
       for row in cursor:
           block_address = row.get('blockAddress')
           document_id = row.get('metadata').get('id')
   
           result = get_revision(ledger_name, document_id, block_address_to_dictionary(block_address), digest_tip_address)
           revision = result.get('Revision').get('IonText')
           document_hash = loads(revision).get('hash')
   
           proof = result.get('Proof')
           logger.info('Got back a proof: {}.'.format(proof))
   
           verified = verify_document(document_hash, digest_bytes, proof)
           if not verified:
               raise AssertionError('Document revision is not verified.')
           else:
               logger.info('Success! The document is verified.')
   
           altered_document_hash = flip_random_bit(document_hash)
           logger.info("Flipping one bit in the document's hash and assert that the document is NOT verified. "
                       "The altered document hash is: {}.".format(to_base_64(altered_document_hash)))
           verified = verify_document(altered_document_hash, digest_bytes, proof)
           if verified:
               raise AssertionError('Expected altered document hash to not be verified against digest.')
           else:
               logger.info('Success! As expected flipping a bit in the document hash causes verification to fail.')
   
           logger.info('Finished verifying the registration with VIN = {} in ledger = {}.'.format(vin, ledger_name))
   
   
   if __name__ == '__main__':
       """
       Verify the integrity of a document revision in a QLDB ledger.
       """
       registration = SampleData.VEHICLE_REGISTRATION[0]
       vin = registration['VIN']
       try:
           with create_qldb_session() as session:
               verify_registration(session, Constants.LEDGER_NAME, vin)
       except Exception:
           logger.exception('Unable to verify revision.')
   ```

------
**참고**  
`get_revision` 함수가 지정된 문서 개정에 대한 증거를 반환하면 이 프로그램은 클라이언트 측 API를 사용하여 해당 개정본을 확인합니다.

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python get_revision.py
   ```

`vehicle-registration` 원장을 더 이상 사용할 필요가 없는 경우 [8단계(선택 사항): 리소스 정리](getting-started.python.step-8.md)로 진행하세요.

# 8단계(선택 사항): 리소스 정리
<a name="getting-started.python.step-8"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

`vehicle-registration` 원장을 계속 사용할 수 있습니다. 그러나 더 이상 필요하지 않은 경우 삭제해야 합니다.

**원장을 삭제하려면**

1. 다음 프로그램(`delete_ledger.py`)을 사용하여 `vehicle-registration` 원장과 모든 내용을 삭제하세요.

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   from time import sleep
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   from pyqldbsamples.describe_ledger import describe_ledger
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   LEDGER_DELETION_POLL_PERIOD_SEC = 20
   
   
   def delete_ledger(ledger_name):
       """
       Send a request to QLDB to delete the specified ledger.
   
       :type ledger_name: str
       :param ledger_name: Name for the ledger to be deleted.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info('Attempting to delete the ledger with name: {}...'.format(ledger_name))
       result = qldb_client.delete_ledger(Name=ledger_name)
       logger.info('Success.')
       return result
   
   
   def wait_for_deleted(ledger_name):
       """
       Wait for the ledger to be deleted.
   
       :type ledger_name: str
       :param ledger_name: The ledger to check on.
       """
       logger.info('Waiting for the ledger to be deleted...')
       while True:
           try:
               describe_ledger(ledger_name)
               logger.info('The ledger is still being deleted. Please wait...')
               sleep(LEDGER_DELETION_POLL_PERIOD_SEC)
           except qldb_client.exceptions.ResourceNotFoundException:
               logger.info('Success. The ledger is deleted.')
               break
   
   
   def set_deletion_protection(ledger_name, deletion_protection):
       """
       Update an existing ledger's deletion protection.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger to update.
   
       :type deletion_protection: bool
       :param deletion_protection: Enable or disable the deletion protection.
   
       :rtype: dict
       :return: Result from the request.
       """
       logger.info("Let's set deletion protection to {} for the ledger with name {}.".format(deletion_protection,
                                                                                             ledger_name))
       result = qldb_client.update_ledger(Name=ledger_name, DeletionProtection=deletion_protection)
       logger.info('Success. Ledger updated: {}'.format(result))
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Delete a ledger.
       """
       try:
           set_deletion_protection(ledger_name, False)
           delete_ledger(ledger_name)
           wait_for_deleted(ledger_name)
       except Exception as e:
           logger.exception('Unable to delete the ledger.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```
**참고**  
원장에 대해 삭제 방지가 활성화된 경우 QLDB API를 사용하여 원장을 삭제하기 전에 먼저 이를 비활성화해야 합니다.

   또한 `delete_ledger.py` 파일은 다음 프로그램(`describe_ledger.py`)에 대한 종속성을 갖습니다.

   ```
   # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   # SPDX-License-Identifier: MIT-0
   #
   # Permission is hereby granted, free of charge, to any person obtaining a copy of this
   # software and associated documentation files (the "Software"), to deal in the Software
   # without restriction, including without limitation the rights to use, copy, modify,
   # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
   # permit persons to whom the Software is furnished to do so.
   #
   # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
   # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   #
   # This code expects that you have AWS credentials setup per:
   # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html
   from logging import basicConfig, getLogger, INFO
   
   from boto3 import client
   
   from pyqldbsamples.constants import Constants
   
   logger = getLogger(__name__)
   basicConfig(level=INFO)
   qldb_client = client('qldb')
   
   
   def describe_ledger(ledger_name):
       """
       Describe a ledger.
   
       :type ledger_name: str
       :param ledger_name: Name of the ledger to describe.
       """
       logger.info('describe ledger with name: {}.'.format(ledger_name))
       result = qldb_client.describe_ledger(Name=ledger_name)
       result.pop('ResponseMetadata')
       logger.info('Success. Ledger description: {}.'.format(result))
       return result
   
   
   def main(ledger_name=Constants.LEDGER_NAME):
       """
       Describe a QLDB ledger.
       """
       try:
           describe_ledger(ledger_name)
       except Exception as e:
           logger.exception('Unable to describe a ledger.')
           raise e
   
   
   if __name__ == '__main__':
       main()
   ```

1. 프로그램을 실행하려면 다음 명령을 입력합니다.

   ```
   python delete_ledger.py
   ```

# Amazon QLDB에서 Amazon Ion 데이터 타입을 사용한 작업
<a name="driver-working-with-ion"></a>

**중요**  
지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB는 데이터를 Amazon Ion 형식으로 저장합니다. QLDB에서 데이터를 사용하려면 지원되는 프로그래밍 언어의 종속 항목으로 [Ion 라이브러리](http://amzn.github.io/ion-docs/libs.html)를 사용해야 합니다.

이 섹션에서는 데이터를 네이티브 타입에서 해당 타입의 Ion 형식으로 변환하는 방법과 그 반대의 방법을 알아봅니다. 이 참조 가이드는 QLDB 드라이버를 사용하여 QLDB 원장의 Ion 데이터를 처리하는 코드 예를 보여줍니다. 여기에는 Java, .NET(C\$1)Go, Node.js(TypeScript) 및 Python의 코드 예가 포함됩니다.

**Topics**
+ [

## 사전 조건
](#driver-ion-prereqs)
+ [

## 부울
](#driver-ion-bool)
+ [

## 정수
](#driver-ion-int)
+ [

## Float
](#driver-ion-float)
+ [

## 10진수
](#driver-ion-decimal)
+ [

## Timestamp
](#driver-ion-timestamp)
+ [

## String
](#driver-ion-string)
+ [

## Blob
](#driver-ion-blob)
+ [

## 나열
](#driver-ion-list)
+ [

## Struct
](#driver-ion-struct)
+ [

## Null 값 및 동적 타입
](#driver-ion-null)
+ [

## JSON으로 하향 변환
](#driver-ion-json)

## 사전 조건
<a name="driver-ion-prereqs"></a>

다음 코드 예는 `ExampleTable`라는 명칭의 활성 원장에 연결된 QLDB 드라이버 인스턴스가 있다고 가정합니다. 이 표에는 다음과 같은 8개 필드가 있는 기존 문서 하나가 포함되어 있습니다:
+ ExampleBool
+ ExampleInt
+ ExampleFloat
+ ExampleDecimal
+ ExampleTimestamp
+ ExampleString
+ ExampleBlob
+ ExampleList

**참고**  
이 참조에서는 각 필드에 저장된 형식이 해당 명칭과 일치한다고 가정합니다. 실제로 QLDB는 문서 필드에 스키마 또는 데이터 타입 정의를 적용하지 않습니다.

## 부울
<a name="driver-ion-bool"></a>

다음 코드 예에서는 Ion 부울 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

boolean exampleBoolean = driver.execute((txn) -> {
    // Transforming a Java boolean to Ion
    boolean aBoolean = true;
    IonValue ionBool = ionSystem.newBool(aBoolean);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleBool = ?", ionBool);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleBool from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java boolean
        // Cast IonValue to IonBool first
        aBoolean = ((IonBool)ionValue).booleanValue();
    }

    // exampleBoolean is now the value fetched from QLDB
    return aBoolean;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# bool to Ion.
bool nativeBool  = true;
IIonValue ionBool = valueFactory.NewBool(nativeBool);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleBool = ?", ionBool);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleBool from ExampleTable");
});

bool? retrievedBool = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# bool.
    retrievedBool = ionValue.BoolValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleBool, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aBool := true

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleBool = ?", aBool)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleBool FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult bool
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleBool is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonBoolean(driver: QldbDriver): Promise<boolean> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleBool = ?", true);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleBool FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Boolean
            const boolValue: boolean = ionValue.booleanValue();
            return boolValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_bool(txn):
    # QLDB can take in a Python bool
    a_bool = True

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleBool = ?", a_bool)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleBool FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python bool is a child class of Python bool
        a_bool = ion_value

    # example_bool is now the value fetched from QLDB
    return a_bool


example_bool = driver.execute_lambda(lambda txn: update_and_query_ion_bool(txn))
```

------

## 정수
<a name="driver-ion-int"></a>

다음 코드 예에서는 Ion 정수형을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

int exampleInt = driver.execute((txn) -> {
    // Transforming a Java int to Ion
    int aInt = 256;
    IonValue ionInt = ionSystem.newInt(aInt);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleInt = ?", ionInt);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleInt from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java int
        // Cast IonValue to IonInt first
        aInt = ((IonInt)ionValue).intValue();
    }

    // exampleInt is now the value fetched from QLDB
    return aInt;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# int to Ion.
int nativeInt  = 256;
IIonValue ionInt = valueFactory.NewInt(nativeInt);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleInt = ?", ionInt);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleInt from ExampleTable");
});

int? retrievedInt = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# int.
    retrievedInt = ionValue.IntValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleInt, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aInt := 256

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleInt = ?", aInt)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleInt FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult int
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleInt is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonInt(driver: QldbDriver): Promise<number> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleInt = ?", 256);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleInt FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Number
            const intValue: number = ionValue.numberValue();
            return intValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_int(txn):
    # QLDB can take in a Python int
    a_int = 256

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleInt = ?", a_int)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleInt FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python int is a child class of Python int
        a_int = ion_value

    # example_int is now the value fetched from QLDB
    return a_int


example_int = driver.execute_lambda(lambda txn: update_and_query_ion_int(txn))
```

------

## Float
<a name="driver-ion-float"></a>

다음 코드 예에서는 Ion 플로트 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

float exampleFloat = driver.execute((txn) -> {
    // Transforming a Java float to Ion
    float aFloat = 256;
    IonValue ionFloat = ionSystem.newFloat(aFloat);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleFloat = ?", ionFloat);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleFloat from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java float
        // Cast IonValue to IonFloat first
        aFloat = ((IonFloat)ionValue).floatValue();
    }

    // exampleFloat is now the value fetched from QLDB
    return aFloat;
});
```

------
#### [ .NET ]

**C\$1 플로트 사용**

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# float to Ion.
float nativeFloat = 256;
IIonValue ionFloat = valueFactory.NewFloat(nativeFloat);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleFloat = ?", ionFloat);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleFloat from ExampleTable");
});

float? retrievedFloat = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# float. We cast ionValue.DoubleValue to a float
    // but be cautious, this is a down-cast and can lose precision.
    retrievedFloat = (float)ionValue.DoubleValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

**C\$1 더블 사용**

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# double to Ion.
double nativeDouble = 256;
IIonValue ionFloat = valueFactory.NewFloat(nativeDouble);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleFloat = ?", ionFloat);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleFloat from ExampleTable");
});

double? retrievedDouble = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# double.
    retrievedDouble = ionValue.DoubleValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleFloat, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aFloat := float32(256)

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleFloat = ?", aFloat)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleFloat FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		// float64 would work as well
		var decodedResult float32
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleFloat is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonFloat(driver: QldbDriver): Promise<number> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleFloat = ?", 25.6);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleFloat FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Number
            const floatValue: number = ionValue.numberValue();
            return floatValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_float(txn):
    # QLDB can take in a Python float
    a_float = float(256)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleFloat = ?", a_float)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleFloat FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python float is a child class of Python float
        a_float = ion_value

    # example_float is now the value fetched from QLDB
    return a_float


example_float = driver.execute_lambda(lambda txn: update_and_query_ion_float(txn))
```

------

## 10진수
<a name="driver-ion-decimal"></a>

다음 코드 예에서는 Ion 10진수 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

double exampleDouble = driver.execute((txn) -> {
    // Transforming a Java double to Ion
    double aDouble = 256;
    IonValue ionDecimal = ionSystem.newDecimal(aDouble);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleDecimal = ?", ionDecimal);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleDecimal from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java double
        // Cast IonValue to IonDecimal first
        aDouble = ((IonDecimal)ionValue).doubleValue();
    }

    // exampleDouble is now the value fetched from QLDB
    return aDouble;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# decimal to Ion.
decimal nativeDecimal = 256.8723m;
IIonValue ionDecimal = valueFactory.NewDecimal(nativeDecimal);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleDecimal = ?", ionDecimal);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleDecimal from ExampleTable");
});

decimal? retrievedDecimal = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# decimal.
    retrievedDecimal = ionValue.DecimalValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleDecimal, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aDecimal, err := ion.ParseDecimal("256")
	if err != nil {
		return nil, err
	}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleDecimal = ?", aDecimal)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleDecimal FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult ion.Decimal
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleDecimal is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonDecimal(driver: QldbDriver): Promise<Decimal> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Creating a Decimal value. Decimal is an Ion Type with high precision
            let ionDecimal: Decimal = dom.load("2.5d-6").decimalValue();
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleDecimal = ?", ionDecimal);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleDecimal FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Get the Ion Decimal
            ionDecimal = ionValue.decimalValue();
            return ionDecimal;
        })
    );
}
```

**참고**  
`ionValue.numberValue()`를 사용하여 Ion 10진수를 JavaScript 숫자로 변환할 수도 있습니다. `ionValue.numberValue()`를 사용하면 성능이 더 좋지만 정확도는 떨어집니다.

------
#### [ Python ]

```
def update_and_query_ion_decimal(txn):
    # QLDB can take in a Python decimal
    a_decimal = Decimal(256)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleDecimal = ?", a_decimal)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleDecimal FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python decimal is a child class of Python decimal
        a_decimal = ion_value

    # example_decimal is now the value fetched from QLDB
    return a_decimal


example_decimal = driver.execute_lambda(lambda txn: update_and_query_ion_decimal(txn))
```

------

## Timestamp
<a name="driver-ion-timestamp"></a>

다음 코드 예에서는 Ion 타임스탬프 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

Date exampleDate = driver.execute((txn) -> {
    // Transforming a Java Date to Ion
    Date aDate = new Date();
    IonValue ionTimestamp = ionSystem.newUtcTimestamp(aDate);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleTimestamp = ?", ionTimestamp);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleTimestamp from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java Date
        // Cast IonValue to IonTimestamp first
        aDate = ((IonTimestamp)ionValue).dateValue();
    }

    // exampleDate is now the value fetched from QLDB
    return aDate;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
using Amazon.IonDotnet;
...

IValueFactory valueFactory = new ValueFactory();

// Convert C# native DateTime to Ion.
DateTime nativeDateTime = DateTime.Now;

// First convert it to a timestamp object from Ion.
Timestamp timestamp = new Timestamp(nativeDateTime);
// Then convert to Ion timestamp.
IIonValue ionTimestamp = valueFactory.NewTimestamp(timestamp);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleTimestamp = ?", ionTimestamp);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleTimestamp from ExampleTable");
});

DateTime? retrievedDateTime = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# DateTime.
    retrievedDateTime = ionValue.TimestampValue.DateTimeValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleTimestamp, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aTimestamp := time.Date(2006, time.May, 20, 12, 30, 0, 0, time.UTC)
	if err != nil {
		return nil, err
	}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleTimestamp = ?", aTimestamp)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleTimestamp FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult ion.Timestamp
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleTimestamp is now the value fetched from QLDB
		return decodedResult.GetDateTime(), nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonTimestamp(driver: QldbDriver): Promise<Date> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            let exampleDateTime: Date = new Date(Date.now());
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleTimestamp = ?", exampleDateTime);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleTimestamp FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript Date
            exampleDateTime = ionValue.timestampValue().getDate();
            return exampleDateTime;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_timestamp(txn):
    # QLDB can take in a Python timestamp
    a_datetime = datetime.now()

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleTimestamp = ?", a_datetime)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleTimestamp FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python timestamp is a child class of Python datetime
        a_timestamp = ion_value

    # example_timestamp is now the value fetched from QLDB
    return a_timestamp


example_timestamp = driver.execute_lambda(lambda txn: update_and_query_ion_timestamp(txn))
```

------

## String
<a name="driver-ion-string"></a>

다음 코드 예에서는 Ion 문자열 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

String exampleString = driver.execute((txn) -> {
    // Transforming a Java String to Ion
    String aString = "Hello world!";
    IonValue ionString = ionSystem.newString(aString);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleString = ?", ionString);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleString from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java String
        // Cast IonValue to IonString first
        aString = ((IonString)ionValue).stringValue();
    }

    // exampleString is now the value fetched from QLDB
    return aString;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Convert C# string to Ion.
String nativeString = "Hello world!";
IIonValue ionString = valueFactory.NewString(nativeString);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleString = ?", ionString);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleString from ExampleTable");
});

String retrievedString = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# string.
    retrievedString = ionValue.StringValue;
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleString, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aString := "Hello World!"

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleString = ?", aString)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleString FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult string
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleString is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonString(driver: QldbDriver): Promise<string> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleString = ?", "Hello World!");

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleString FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to a TypeScript String
            const stringValue: string = ionValue.stringValue();
            return stringValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_string(txn):
    # QLDB can take in a Python string
    a_string = "Hello world!"

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleString = ?", a_string)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleString FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python string is a child class of Python string
        a_string = ion_value

    # example_string is now the value fetched from QLDB
    return a_string


example_string = driver.execute_lambda(lambda txn: update_and_query_ion_string(txn))
```

------

## Blob
<a name="driver-ion-blob"></a>

다음 코드 예에서는 Ion blob 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

byte[] exampleBytes = driver.execute((txn) -> {
    // Transforming a Java byte array to Ion
    // Transform any arbitrary data to a byte array to store in QLDB
    String aString = "Hello world!";
    byte[] aByteArray = aString.getBytes();
    IonValue ionBlob = ionSystem.newBlob(aByteArray);

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleBlob = ?", ionBlob);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleBlob from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Transforming Ion to a Java byte array
        // Cast IonValue to IonBlob first
        aByteArray = ((IonBlob)ionValue).getBytes();
    }

    // exampleBytes is now the value fetched from QLDB
    return aByteArray;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
using System.Text;
...

IValueFactory valueFactory = new ValueFactory();

// Transform any arbitrary data to a byte array to store in QLDB.
string nativeString = "Hello world!";
byte[] nativeByteArray = Encoding.UTF8.GetBytes(nativeString);
// Transforming a C# byte array to Ion.
IIonValue ionBlob = valueFactory.NewBlob(nativeByteArray);

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleBlob = ?", ionBlob);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleBlob from ExampleTable");
});

byte[] retrievedByteArray = null;

// Assume there is only one document in ExampleTable.
await foreach (IIonValue ionValue in selectResult)
{
    // Transforming Ion to a C# byte array.
    retrievedByteArray = ionValue.Bytes().ToArray();
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleBlob, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aBlob := []byte("Hello World!")

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleBlob = ?", aBlob)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleBlob FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult []byte
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleBlob is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonBlob(driver: QldbDriver): Promise<Uint8Array> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            const enc = new TextEncoder();
            let blobValue: Uint8Array = enc.encode("Hello World!");
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleBlob = ?", blobValue);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleBlob FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Transforming Ion to TypeScript Uint8Array
            blobValue = ionValue.uInt8ArrayValue();
            return blobValue;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_blob(txn):
    # QLDB can take in a Python byte array
    a_string = "Hello world!"
    a_byte_array = str.encode(a_string)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleBlob = ?", a_byte_array)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleBlob FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python blob is a child class of Python byte array
        a_blob = ion_value

    # example_blob is now the value fetched from QLDB
    return a_blob


example_blob = driver.execute_lambda(lambda txn: update_and_query_ion_blob(txn))
```

------

## 나열
<a name="driver-ion-list"></a>

다음 코드 예는 Ion 리스트 타입을 처리하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

List<Integer> exampleList = driver.execute((txn) -> {
    // Transforming a Java List to Ion
    List<Integer> aList = new ArrayList<>();
    // Add 5 Integers to the List for the sake of example
    for (int i = 0; i < 5; i++) {
        aList.add(i);
    }
    // Create an empty Ion List
    IonList ionList = ionSystem.newEmptyList();
    // Add the 5 Integers to the Ion List
    for (Integer i : aList) {
        // Convert each Integer to Ion ints first to add it to the Ion List
        ionList.add(ionSystem.newInt(i));
    }

    // Insertion into QLDB
    txn.execute("UPDATE ExampleTable SET ExampleList = ?", (IonValue) ionList);

    // Fetching from QLDB
    Result result = txn.execute("SELECT VALUE ExampleList from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Iterate through the Ion List to map it to a Java List
        List<Integer> intList = new ArrayList<>();
        for (IonValue ionInt : (IonList)ionValue) {
            // Convert the 5 Ion ints to Java Integers
            intList.add(((IonInt)ionInt).intValue());
        }
        aList = intList;
    }

    // exampleList is now the value fetched from QLDB
    return aList;
});
```

------
#### [ .NET ]

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
using System.Collections.Generic;
...

IValueFactory valueFactory = new ValueFactory();

// Transforming a C# list to Ion.
IIonValue ionList = valueFactory.NewEmptyList();
foreach (int i in new List<int> {0, 1, 2, 3, 4})
{
    // Convert to Ion int and add to Ion list.
    ionList.Add(valueFactory.NewInt(i));
}

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleList = ?", ionList);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleList from ExampleTable");
});


List<int> retrievedList = new List<int>();
await foreach (IIonValue ionValue in selectResult)
{
    // Iterate through the Ion List to map it to a C# list.
    foreach (IIonValue ionInt in ionValue)
    {
        retrievedList.Add(ionInt.IntValue);
    }
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

------
#### [ Go ]

```
exampleList, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aList := []int{1, 2, 3, 4, 5}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleList = ?", aList)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleList FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult []int
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleList is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonList(driver: QldbDriver): Promise<number[]> {
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            let listOfNumbers: number[] = [1, 2, 3, 4, 5];
            // Updating QLDB
            await txn.execute("UPDATE ExampleTable SET ExampleList = ?", listOfNumbers);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT VALUE ExampleList FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // Get Ion List
            const ionList: dom.Value[] = ionValue.elements();
            // Iterate through the Ion List to map it to a JavaScript Array
            let intList: number[] = [];
            ionList.forEach(item => {
                // Transforming Ion to a TypeScript Number
                const intValue: number = item.numberValue();
                intList.push(intValue);
            });
            listOfNumbers = intList;
            return listOfNumbers;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_list(txn):
    # QLDB can take in a Python list
    a_list = list()
    for i in range(0, 5):
        a_list.append(i)

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleList = ?", a_list)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleList FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python blob is a child class of Python list
        a_list = ion_value

    # example_list is now the value fetched from QLDB
    return a_list


example_list = driver.execute_lambda(lambda txn: update_and_query_ion_list(txn))
```

------

## Struct
<a name="driver-ion-struct"></a>

QLDB에서 `struct` 데이터 타입은 다른 Ion 타입과 특히 다릅니다. 표에 삽입하는 최상위 문서는 `struct` 타입이어야 합니다. 문서 필드는 중첩된 `struct`를 저장할 수도 있습니다.

단순화를 위해 다음 예에서는 `ExampleString` 및 `ExampleInt` 필드만 있는 문서를 정의합니다.

------
#### [ Java ]

```
class ExampleStruct {
    public String exampleString;
    public int exampleInt;

    public ExampleStruct(String exampleString, int exampleInt) {
        this.exampleString = exampleString;
        this.exampleInt = exampleInt;
    }
}

// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();

ExampleStruct examplePojo = driver.execute((txn) -> {
    // Transforming a POJO to Ion
    ExampleStruct aPojo = new ExampleStruct("Hello world!", 256);
    // Create an empty Ion struct
    IonStruct ionStruct = ionSystem.newEmptyStruct();
    // Map the fields of the POJO to Ion values and put them in the Ion struct
    ionStruct.add("ExampleString", ionSystem.newString(aPojo.exampleString));
    ionStruct.add("ExampleInt", ionSystem.newInt(aPojo.exampleInt));

    // Insertion into QLDB
    txn.execute("INSERT INTO ExampleTable ?", ionStruct);

    // Fetching from QLDB
    Result result = txn.execute("SELECT * from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Map the fields of the Ion struct to Java values and construct a new POJO
        ionStruct = (IonStruct)ionValue;
        IonString exampleString = (IonString)ionStruct.get("ExampleString");
        IonInt exampleInt = (IonInt)ionStruct.get("ExampleInt");
        aPojo = new ExampleStruct(exampleString.stringValue(), exampleInt.intValue());
    }

    // examplePojo is now the document fetched from QLDB
    return aPojo;
});
```

또는 [Jackson 라이브러리](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/ion)를 사용하여 Ion에 데이터 타입을 매핑하거나 Ion에서 데이터 타입을 매핑할 수 있습니다. 라이브러리는 다른 Ion 데이터 타입을 지원하지만 이 예에서는 `struct` 타입에 초점을 맞춥니다.

```
class ExampleStruct {
    public String exampleString;
    public int exampleInt;

    @JsonCreator
    public ExampleStruct(@JsonProperty("ExampleString") String exampleString,
                         @JsonProperty("ExampleInt") int exampleInt) {
        this.exampleString = exampleString;
        this.exampleInt = exampleInt;
    }

    @JsonProperty("ExampleString")
    public String getExampleString() {
        return this.exampleString;
    }

    @JsonProperty("ExampleInt")
    public int getExampleInt() {
        return this.exampleInt;
    }
}

// Instantiate an IonSystem from the Ion library
IonSystem ionSystem = IonSystemBuilder.standard().build();
// Instantiate an IonObjectMapper from the Jackson library
IonObjectMapper ionMapper = new IonValueMapper(ionSystem);

ExampleStruct examplePojo = driver.execute((txn) -> {
    // Transforming a POJO to Ion
    ExampleStruct aPojo = new ExampleStruct("Hello world!", 256);
    IonValue ionStruct;
    try  {
        // Use the mapper to convert Java objects into Ion
        ionStruct = ionMapper.writeValueAsIonValue(aPojo);
    } catch (IOException e) {
        // Wrap the exception and throw it for the sake of simplicity in this example
        throw new RuntimeException(e);
    }

    // Insertion into QLDB
    txn.execute("INSERT INTO ExampleTable ?", ionStruct);

    // Fetching from QLDB
    Result result = txn.execute("SELECT * from ExampleTable");
    // Assume there is only one document in ExampleTable
    for (IonValue ionValue : result)
    {
        // Use the mapper to convert Ion to Java objects
        try {
            aPojo = ionMapper.readValue(ionValue, ExampleStruct.class);
        } catch (IOException e) {
            // Wrap the exception and throw it for the sake of simplicity in this example
            throw new RuntimeException(e);
        }
    }

    // examplePojo is now the document fetched from QLDB
    return aPojo;
});
```

------
#### [ .NET ]

[Amazon.QLDB.Driver.Serialization](https://www.nuget.org/packages/Amazon.QLDB.Driver.Serialization/) 라이브러리를 사용하여 네이티브 C\$1 데이터 타입을 Ion과 매핑하거나 Ion에서 네이티브 C\$1 데이터 타입을 매핑할 수 있습니다. 라이브러리는 다른 Ion 데이터 타입을 지원하지만 이 예에서는 `struct` 타입에 초점을 맞춥니다.

```
using Amazon.QLDB.Driver.Generic;
using Amazon.QLDB.Driver.Serialization;
...

IAsyncQldbDriver driver = AsyncQldbDriver.Builder()
    .WithLedger("vehicle-registration")
    // Add Serialization library
    .WithSerializer(new ObjectSerializer())
    .Build();

// Creating a C# POCO.
ExampleStruct exampleStruct = new ExampleStruct
{
    ExampleString = "Hello world!",
    ExampleInt = 256
};

IAsyncResult<ExampleStruct> selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute(txn.Query<Document>("UPDATE ExampleTable SET ExampleStruct = ?", exampleStruct));

    // Fetching from QLDB.
    return await txn.Execute(txn.Query<ExampleStruct>("SELECT VALUE ExampleStruct from ExampleTable"));
});

await foreach (ExampleStruct row in selectResult)
{
    Console.WriteLine(row.ExampleString);
    Console.WriteLine(row.ExampleInt);
}
```

**참고**  
동기 코드로 변환하려면 `await` 및 `async` 키워드를 제거하고 `IAsyncResult` 유형을 `IResult`로 변경하세요.

또는 [Amazon.IonDotnet.Builders](https://www.nuget.org/packages/Amazon.IonDotnet/) 라이브러리를 사용하여 Ion 데이터 타입을 처리할 수도 있습니다.

```
using IAsyncResult = Amazon.QLDB.Driver.IAsyncResult;
...

IValueFactory valueFactory = new ValueFactory();

// Creating Ion struct.
IIonValue ionStruct = valueFactory.NewEmptyStruct();
ionStruct.SetField("ExampleString", valueFactory.NewString("Hello world!"));
ionStruct.SetField("ExampleInt", valueFactory.NewInt(256));

IAsyncResult selectResult = await driver.Execute(async txn =>
{
    // Insertion into QLDB.
    await txn.Execute("UPDATE ExampleTable SET ExampleStruct = ?", ionStruct);

    // Fetching from QLDB.
    return await txn.Execute("SELECT VALUE ExampleStruct from ExampleTable");
});

string retrievedString = null;
int? retrievedInt = null;

await foreach (IIonValue ionValue in selectResult)
{
    retrievedString = ionValue.GetField("ExampleString").StringValue;
    retrievedInt = ionValue.GetField("ExampleInt").IntValue;
}
```

------
#### [ Go ]

```
exampleStruct, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
	aStruct := map[string]interface{} {
		"ExampleString": "Hello World!",
		"ExampleInt": 256,
	}

	// Insertion into QLDB
	_, err = txn.Execute("UPDATE ExampleTable SET ExampleStruct = ?", aStruct)
	if err != nil {
		return nil, err
	}

	// Fetching from QLDB
	result, err := txn.Execute("SELECT VALUE ExampleStruct FROM ExampleTable")
	if err != nil {
		return nil, err
	}

	// Assume there is only one document in ExampleTable
	if result.Next(txn) {
		var decodedResult map[string]interface{}
		err := ion.Unmarshal(result.GetCurrentData(), &decodedResult)
		if err != nil {
			return nil, err
		}

		// exampleStruct is now the value fetched from QLDB
		return decodedResult, nil
	}
	return nil, result.Err()
})
```

------
#### [ Node.js ]

```
async function queryIonStruct(driver: QldbDriver): Promise<any> {
    let exampleStruct: any = {stringValue: "Hello World!", intValue: 256};
    return (driver.executeLambda(async (txn: TransactionExecutor) => {
            // Inserting into QLDB
            await txn.execute("INSERT INTO ExampleTable ?", exampleStruct);

            // Fetching from QLDB
            const resultList: dom.Value[] = (await txn.execute("SELECT * FROM ExampleTable")).getResultList();

            // Assume there is only one document in ExampleTable
            const ionValue: dom.Value = resultList[0];
            // We can get all the keys of Ion struct and their associated values
            const ionFieldNames: string[] = ionValue.fieldNames();

            // Getting key and value of Ion struct to TypeScript String and Number
            const nativeStringVal: string = ionValue.get(ionFieldNames[0]).stringValue();
            const nativeIntVal: number = ionValue.get(ionFieldNames[1]).numberValue();
            // Alternatively, we can access to Ion struct fields, using their literal field names:
            //   const nativeStringVal = ionValue.get("stringValue").stringValue();
            //   const nativeIntVal = ionValue.get("intValue").numberValue();

            exampleStruct = {[ionFieldNames[0]]: nativeStringVal, [ionFieldNames[1]]: nativeIntVal};
            return exampleStruct;
        })
    );
}
```

------
#### [ Python ]

```
def update_and_query_ion_struct(txn):
    # QLDB can take in a Python struct
    a_struct = {"ExampleString": "Hello world!", "ExampleInt": 256}

    # Insertion into QLDB
    txn.execute_statement("UPDATE ExampleTable SET ExampleStruct = ?", a_struct)

    # Fetching from QLDB
    cursor = txn.execute_statement("SELECT VALUE ExampleStruct FROM ExampleTable")

    # Assume there is only one document in ExampleTable
    for ion_value in cursor:
        # Ion Python struct is a child class of Python struct
        a_struct = ion_value

    # example_struct is now the value fetched from QLDB
    return a_struct


example_struct = driver.execute_lambda(lambda txn: update_and_query_ion_struct(txn))
```

------

## Null 값 및 동적 타입
<a name="driver-ion-null"></a>

QLDB는 개방형 콘텐츠를 지원하며 문서 필드에 대한 스키마 또는 데이터 타입 정의를 적용하지 않습니다. QLDB 문서에 Ion null 값을 저장할 수도 있습니다. 위의 모든 예에서는 반환된 각 데이터 타입이 알려져 있고 null이 아니라고 가정합니다. 다음 예에서는 데이터 타입을 알 수 없거나 null일 가능성이 있는 경우 Ion을 사용하는 방법을 보여줍니다.

------
#### [ Java ]

```
// Empty variables
String exampleString = null;
Integer exampleInt = null;

// Assume ionValue is some queried data from QLDB
IonValue ionValue = null;

// Check the value type and assign it to the variable if it is not null
if (ionValue.getType() == IonType.STRING) {
    if (ionValue.isNullValue()) {
        exampleString = null;
    } else {
        exampleString = ((IonString)ionValue).stringValue();
    }
} else if (ionValue.getType() == IonType.INT) {
    if (ionValue.isNullValue()) {
        exampleInt = null;
    } else {
        exampleInt = ((IonInt)ionValue).intValue();
    }
};

// Creating null values
IonSystem ionSystem = IonSystemBuilder.standard().build();

// A null value still has an Ion type
IonString ionString;
if (exampleString == null) {
    // Specifically a null string
    ionString = ionSystem.newNullString();
} else {
    ionString = ionSystem.newString(exampleString);
}

IonInt ionInt;
if (exampleInt == null) {
    // Specifically a null int
    ionInt = ionSystem.newNullInt();
} else {
    ionInt = ionSystem.newInt(exampleInt);
}

// Special case regarding null!
// There is a generic null type which has Ion type 'Null'.
// The above null values still have the types 'String' and 'Int'
IonValue specialNull = ionSystem.newNull();
if (specialNull.getType() == IonType.NULL) {
    // This is true!
}
if (specialNull.isNullValue()) {
    // This is also true!
}
if (specialNull.getType() == IonType.STRING || specialNull.getType() == IonType.INT) {
    // This is false!
}
```

------
#### [ .NET ]

```
// Empty variables.
string exampleString = null;
int? exampleInt = null;

// Assume ionValue is some queried data from QLDB.
IIonValue ionValue;

if (ionValue.Type() == IonType.String)
{
    exampleString = ionValue.StringValue;
}
else if (ionValue.Type() == IonType.Int)
{
    if (ionValue.IsNull)
    {
        exampleInt = null;
    }
    else
    {
        exampleInt = ionValue.IntValue;
    }
};

// Creating null values.
IValueFactory valueFactory = new ValueFactory();

// A null value still has an Ion type.
IIonValue ionString = valueFactory.NewString(exampleString);

IIonValue ionInt;
if (exampleInt == null)
{
    // Specifically a null int.
    ionInt = valueFactory.NewNullInt();
}
else
{
    ionInt = valueFactory.NewInt(exampleInt.Value);
}

// Special case regarding null!
// There is a generic null type which has Ion type 'Null'.
IIonValue specialNull = valueFactory.NewNull();
if (specialNull.Type() == IonType.Null) {
    // This is true!
}
if (specialNull.IsNull) {
    // This is also true!
}
```

------
#### [ Go ]

**수렴 제한**

Go에서 `nil` 값을 수렴한 다음 수렴 취소하면 해당 값의 타입이 유지되지 않습니다. `nil` 값은 Ion null로 수렴되지만, Ion null은 `nil`이 아닌 값 0으로 수렴 취소됩니다.

```
ionNull, err := ion.MarshalText(nil) // ionNull is set to ion null
if err != nil {
    return
}

var result int
err = ion.Unmarshal(ionNull, &result) // result unmarshals to 0
if err != nil {
    return
}
```

------
#### [ Node.js ]

```
// Empty variables
let exampleString: string;
let exampleInt: number;

// Assume ionValue is some queried data from QLDB
// Check the value type and assign it to the variable if it is not null
if (ionValue.getType() === IonTypes.STRING) {
    if (ionValue.isNull()) {
        exampleString = null;
    } else {
        exampleString = ionValue.stringValue();
    }
} else if (ionValue.getType() === IonTypes.INT) {
    if (ionValue.isNull()) {
        exampleInt = null;
    } else {
        exampleInt = ionValue.numberValue();
    }
}

// Creating null values
if (exampleString === null) {
    ionString = dom.load('null.string');
} else {
    ionString = dom.load.of(exampleString);
}

if (exampleInt === null) {
    ionInt = dom.load('null.int');
} else {
    ionInt = dom.load.of(exampleInt);
}

// Special case regarding null!
// There is a generic null type which has Ion type 'Null'.
// The above null values still have the types 'String' and 'Int'
specialNull: dom.Value = dom.load("null.null");
if (specialNull.getType() === IonType.NULL) {
    // This is true!
}
if (specialNull.getType() === IonType.STRING || specialNull.getType() === IonType.INT) {
    // This is false!
}
```

------
#### [ Python ]

```
# Empty variables
example_string = None
example_int = None

# Assume ion_value is some queried data from QLDB
# Check the value type and assign it to the variable if it is not null
if ion_value.ion_type == IonType.STRING:
    if isinstance(ion_value, IonPyNull):
        example_string = None
    else:
        example_string = ion_value
elif ion_value.ion_type == IonType.INT:
    if isinstance(ion_value, IonPyNull):
        example_int = None
    else:
        example_int = ion_value

# Creating Ion null values
if example_string is None:
    # Specifically a null string
    ion_string = loads("null.string")
else:
    # QLDB can take in Python string
    ion_string = example_string
if example_int is None:
    # Specifically a null int
    ion_int = loads("null.int")
else:
    # QLDB can take in Python int
    ion_int = example_int


# Special case regarding null!
# There is a generic null type which has Ion type 'Null'.
# The above null values still have the types 'String' and 'Int'
special_null = loads("null.null")
if special_null.ion_type == IonType.NULL:
    # This is true!
if special_null.ion_type == IonType.STRING or special_null.ion_type == IonType.INT:
    # This is false!
```

------

## JSON으로 하향 변환
<a name="driver-ion-json"></a>

애플리케이션에 JSON 병용성이 필요한 경우, Amazon Ion 데이터를 JSON으로 하향 변환할 수 있습니다. 하지만 JSON에 없는 풍부한 Ion 유형을 데이터에 사용하는 경우 Ion을 JSON으로 변환해도 손실이 발생합니다.

Ion에서 JSON으로의 변환 규칙에 대한 자세한 내요은 *Amazon Ion Cookbook*의 [JSON으로 하향 변환](https://amzn.github.io/ion-docs/guides/cookbook.html#down-converting-to-json)을 참조하세요.