

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

# 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 콘솔을 엽니다.