サーバサイド Kotlin から AWS にアクセスしたいときにはどのSDK を使えばいいのか？ S3編

## 背景
先日の Re:Invent 2021で AWS SDK for Kotlin の デベロッパープレビューが公開されました。
  https://aws.amazon.com/jp/about-aws/whats-new/2021/12/aws-sdk-kotlin-developer-preview/
 これまで Kotlin から AWS SDK を使う際には AWS SDK for Java が使われてきたと思います。新たな選択肢が増えたので、どちらのSDKを使うのが良いか比較していきたいと思います。

:::note warn
※比較時点（2021/12/14）ではAWS SDK for Kotlinはv0.9.4-beta バージョンです。後述する機能不足は1.0版では改善されている可能性がある点にご注意ください。
:::

## 比較観点の前提
* 比較対象
  * AWS SDK for Kotlin 0.94-beta
  * AWS SDK for Java 2.17.000
* サーバサイドKotlinで使う。AndroidやKotlin MPPで iOSアプリを開発する場合はまた違った観点になると思います。

## セットアップ
gradle でセットアップ。認証情報の設定などより詳しい設定は公式ドキュメントを参照ください
https://docs.aws.amazon.com/ja_jp/sdk-for-java/latest/developer-guide/setup-project-gradle.html

以下コマンドで gradle project を作成し、build.gradleの依存にAWS SDKを追加する。
```
gradle init --type kotlin-application
```

```groovy:build.gradle
plugins {
    kotlin("jvm") version "1.5.30"
    application
}

// ...
dependencies {
    // for AWS SDK Kotlin
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0"
    implementation "aws.sdk.kotlin:s3:0.9.4-beta"
    implementation "aws.sdk.kotlin:dynamodb:0.9.4-beta"

    // for AWS SDK Java
    implementation platform('software.amazon.awssdk:bom:2.15.0')
    implementation 'software.amazon.awssdk:s3'
    implementation 'software.amazon.awssdk:dynamodb'
    implementation 'software.amazon.awssdk:dynamodb-enhanced'
}

```

## S3操作の比較

### お題
以下の interface を Kotlin SDK, Java SDK それぞれで実装してみます。

```kotlin：S3ClientSample.kt
import java.nio.file.Files
import java.nio.file.Path

interface S3ClientSample {
    /** S3からファイルを dest にダウンロード */
    fun download(bucketName: String, s3Key: String, dest: Path)

    /** source のファイルをS3へアップロード */
    fun upload(bucketName: String, s3Key: String, source: Path)

    /** s3KeyPrefix 配下のファイル名(key名)一覧を取得 */
    fun list(bucketName: String, s3KeyPrefix: String): List<String>
}
```

### AWS SDK for Java の実装
まず、SDK for Java を使った実装です。特徴的なのはS3Clientや各APIのリクエストの作成がBuilderパターンで提供されていることです。これはAWS SDK全体で共通する設計でパタメータが一つのリクエストでもBuilderで公開されています。Xxxx.builder().....build()という記載がちょっとくどいですね。


```kotlin：S3ClientSampleJavaSdk.kt
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.nio.file.Files
import java.nio.file.Path

class S3ClientSampleJavaSdk: S3ClientSample {
    private val s3Client = S3Client.builder()
        .region(Region.AP_NORTHEAST_1)
        .build()

    override fun download(bucketName: String, s3Key: String, dest: Path) {
        s3Client.getObject(
            GetObjectRequest.builder()
                .bucket(bucketName)
                .key(s3Key).build()
        ).reader().use { reader ->
            Files.write(dest, reader.readLines())
        }
    }

    override fun upload(bucketName: String, s3Key: String, source: Path) {
        s3Client.putObject(
            PutObjectRequest.builder()
                .bucket(bucketName)
                .key(s3Key).build(),
            RequestBody.fromFile(source)
        )
    }

    override fun list(bucketName: String, s3KeyPrefix: String): List<String> {
        return s3Client.listObjectsV2Paginator(
            ListObjectsV2Request.builder()
                .bucket(bucketName)
                .prefix(s3KeyPrefix).build()
        ).flatMap { listObjectV2Response ->
            listObjectV2Response.contents().map { s3Object ->
                s3Object.key()
            }
        }
    }
}
```

### AWS SDK for Kotlinの実装
次にSDK for Kotlinを使った場合の実装です。JavaではBuilderパターンで提供されていた部分がKotlin のDSLで提供されていて少しスッキリしました。一方、Java SDKで提供されているlistObjectsV2Paginatorのような生APIをラップした便利関数が提供されていないため、list()の実装のように自分で複雑な処理を記載する必要がある部分もあります。（[ロードマップ](https://github.com/awslabs/smithy-kotlin/issues/124)によるとPaginatorは絶賛開発中のようです）

```kotlin:S3ClientSampleKotlinSdk.kt
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
import aws.sdk.kotlin.services.s3.model.ListObjectsV2Request
import aws.sdk.kotlin.services.s3.model.ListObjectsV2Response
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
import aws.smithy.kotlin.runtime.content.asByteStream
import aws.smithy.kotlin.runtime.content.writeToFile

class S3ClientSampleKotlinSdk: S3ClientSample {

    private val s3Client = aws.sdk.kotlin.services.s3.S3Client {
        region = "ap-northeast-1"
    }

    override fun download(bucketName: String, s3Key: String, dest: Path) {
        // AWS SDK for Kotlinでは各APIがCoroutine（suspend関数）として提供されている
        // ここでは1APIを同期的に呼びたいので runBlocking で囲んで実行する
        runBlocking {
            s3Client.getObject(GetObjectRequest {
                bucket = bucketName
                key = s3Key
            }) { response ->
                response.body?.writeToFile(dest)
            }
        }
    }

    override fun upload(bucketName: String, s3Key: String, source: Path) {
        runBlocking {
            s3Client.putObject(PutObjectRequest {
                bucket = bucketName
                key = s3Key
                body = source.asByteStream()
            })
        }
    }

    override fun list(bucketName: String, s3KeyPrefix: String): List<String> {
        val keys = mutableListOf<String>()
        var nextToken: String? = null
        var response: ListObjectsV2Response? = null
        runBlocking {
            // listObjects は1リクエストで1000件までしか取得できないため、
            // 全要素取得する場合はresponseが途中で切れているか（isTruncated）を見て、再リクエストする必要がある
            do {
                response = s3Client.listObjectsV2(ListObjectsV2Request {
                    bucket = bucketName
                    prefix = s3KeyPrefix
                    continuationToken = nextToken
                })
                response?.contents?.map { s3Object -> s3Object.key!! }?.let { list ->
                    keys += list
                }
                nextToken = response?.nextContinuationToken
            } while(response?.isTruncated ?: false)
        }
        return keys
    }
}
```

## どちらを使うべき？
あくまで2021/12/14現在の比較ですが、実プロダクトに採用するならAWS SDK for Javaの方を使うのが無難だと思います。AWS SDK for Kotlinの方はまだベータ版ということもあり機能不足感が否めません。この記事では扱い切れませんでしたが、DynamoDBクライアントも結構機能差がある印象でした。
AWS SDK for Kotlinのほうもコードが簡潔になるKotlin DSLやCoroutineサポートなど優れた点が多いので今後の進化に期待です。