はじめに
お久しぶりです。k.s.ロジャースの西谷です。
最近はKotlinを使ったREST APIサーバのボイラープレートを設計及び開発を行いました。
リリースが無事完了し、弊社の開発に導入されています。
今回はサーバサイドKotlinを使ってみて思ったことや困ったことを共有できたらと思います。
間違い等があればコメントにて教えて頂けたらと思います。
環境など
今回は以下の構成で実装しました。
- Kotlin 1.3.61
- SprintBoot 2.2.4.RELEASE
- Exposed 0.21.1
- Mysql 8.0.19
Webフレームワークは選択肢としてSpringBoot
とKtor
がありましたが、SpringBoot
の実績や安定性を考慮してSpringBoot
を使いました。
作ったもの
DDDベースの設計を行いました。
具体的なディレクトリ構成は次の通りです。
.
├── kotlin
│ └── com
│ └── ksrogers
│ ├── application
│ │ ├── common
│ │ │ ├── annotation
│ │ │ └── error
│ │ │ ├── exception
│ │ │ └── handler
│ │ ├── controller
│ │ ├── middleware
│ │ │ ├── advice
│ │ │ ├── filter
│ │ │ └── interceptor
│ │ ├── request
│ │ └── response
│ ├── config
│ ├── domain
│ │ ├── model
│ │ ├── repository
│ │ └── service
│ ├── infrastructure
│ │ ├── table
│ │ ├── mysql
│ │ └── repository
│ └── util
└── resources
├── db
│ └── migration
└── i18n
├── common
└── errors
中心となる層の実装内容は以下の通りで、詳細なDDDアーキテクチャの解説は他記事にお任せしたいと思います。
- application
- controller: APIのエンドポイントと処理
- middleware: controller前の共通処理
- request: リクエスト
- response: レスポンス
- infrastructure
- table: DBのテーブル構造
- repository: DBのレコード操作
- domain
- model: ドメインモデル
- repository: repositoryのインターフェイス定義
- service: ドメインサービス
サーバサイドKotlinの良かったところ
コードが直感的
Kotlin独特の書き方は少ない印象でした。
過度な省略記法や暗黙的な呼び出しもなく、必要に応じてコードを追いかけることが容易でした。
このことから複数の言語を触ったことのある方でしたら気軽にKotlinに移ることができると思います。
言語機能が豊富
Collection, Generics, Extensionなどの機能が揃っており、実装したい機能に対して「あの言語のこれがあれば」みたいなことは無かった印象です。
また、ライブラリ関連もJavaのものが使えるため困ることがありませんでした。
解説記事が多い
Kotlinの解説記事は少ないですが、Javaに関する解説記事は大量にあります。
特にSpringBootを使う場合は、困ったときにJavaの記事から調査ができるため手詰まりになることは少ないです。
InteliJを使っている場合はJavaのコードをKotlinに自動変換する機能もあり、そのまま動いたりすることもあります。
サーバサイドKotlinで困ったところ
Kotlinは凄くいい言語と感じましたが、SpringBootの方は落とし穴が結構ありました。
他言語から来た場合は学習コストが高いかもしれません。
RequestのValidationは全てnullableで行う必要がある
SpringBootはRequestの定義とValidationを同時に行います。
具体的には以下のようなクラスを定義します。
data class SampleRequest(
@field:NotNull
val title: String?,
val other: String?
上記のサンプルの場合、title
はNon-NullにもかかわらずNullableで定義する必要があります。
(Nullableで設定しない場合はNullが飛んできたときに例外が発生します)
Non-Nullにも関わらず毎回nullチェックが必要になり非常に煩雑です。
これを回避するために以下のように実装しました。
data class SampleRequest(
@JsonProperty("title")
@field:NotNull
private val _title: String?,
val other: String?
) {
val title: String
get() = _title ?: ""
}
リクエストの前処理が大掛かりになる
通常のWebフレームワークであれば、ControllerのBaseクラスにbeforeExecuteXXX
を書いて終わりと思っていました。
しかし、SpringBootは複雑で幾つもの割り込みのタイミングがあります。
こちらの記事に詳細が纏められており、参考にさせて頂きました。
RequestBodyをコントローラに渡す前にメタデータを付与したい
リクエストの生データ自体はrequest.getInputStream()
で取得することができます。
しかし、これを使ってしまうとメソッド名の通りstreamとして取得してしまうため、Controllerにデータが流れなくなります。
こちらに関しては以下のようにoverrideすることで解決できました。
class RequestControllerAdvice : RequestBodyAdvice {
override fun beforeBodyRead(
inputMessage: HttpInputMessage,
parameter: MethodParameter,
targetType: Type,
converterType: Class<out HttpMessageConverter<*>>
): HttpInputMessage {
val body = IOUtils.toString(inputMessage.body)
val builder = StringBuilder()
builder.append(body)
val parser = Parser.default()
val json = parser.parse(builder) as JsonObject
// do something
json["_meta"] = "hogehoge"
val returnInputStream: InputStream = json.toJsonString().byteInputStream()
val returnHeaders: HttpHeaders = inputMessage.headers
return object : HttpInputMessage {
override fun getBody(): InputStream = returnInputStream
override fun getHeaders(): HttpHeaders = returnHeaders
}
}
}
マイグレーションツールがない
JavaのマイグレーションはFlyway
が主流です。
しかし、これはバージョンに沿ってSQLを実行するだけで機能として不十分な印象があります。
モデルベースの操作やロールバックなども必要だと思いました。
今後のExposedに期待したいです。
おわりに
今回はサーバサイドKotlinを触ってみた感想について共有させて頂きました。
今後はKtorの方も触ってみようと思います。
間違い等あればご指摘頂けたらと思います。
Wantedlyでもブログ投稿してます
Techブログに加えて会社ブログなどもやっているので、気になった方はぜひ覗いてみてください。
https://www.wantedly.com/companies/ks-rogers