NewsPicks におけるサーバーサイド Kotlin 活用事例

monzou
monzou
Nov 15, 2017 · 30 min read

はじめに

この記事では NewsPicks での Kotlin 活用事例 — — ただしサーバーサイドでの活用事例 — — について紹介します。今秋 9 月から 10 月にかけて、NewsPicks で新しいスマホアプリを開発する機会があり、その際のサーバーサイド開発に Kotlin を利用しました。開発チームの発足が 9 月、仕様設計から開発・リリースまで約 2 ヶ月と比較的短い時間軸での開発だったのでそれほど規模が大きいものではありませんが、サーバーサイドに関してはほぼ僕が一人で開発したこともあり、知見の共有も兼ねて筆をとった次第です。

サーバーサイド Kotlin は少しずつ流行りつつあると思うのですが、まだまだ世間には事例が少なく、採用を躊躇している方も多いのではないかと思います。長文にはなりますが、この記事が何らか皆様の参考になれば幸いです。

対象読者

この記事では主に「サーバーサイドで Kotlin を利用しようと検討しているけど、実際のところどうなの?」と考えている開発者の方を対象に、Kotlin の良いところ/悪いところを、実際に開発したアプリを例に取りながら説明します。Kotlin は JVM で動作する言語であるという特性上、ある程度 Java に習熟している開発者が選択する言語だと思うので、サーバーサイド Java に関する前提知識はある程度あるものと想定します。また、基本的な Kotlin の言語機能に関する説明などは割愛します。

対象システム

対象となるシステムは、NewsPicks のプレミアム会員向けのコミュニティアプリです。NewsPicks では今春から月額 5,000 円(!)のプレミアムプラン(アカデミアプランと呼んでいます)を始めているのですが、こちらの会員を対象としてイベント申込やコミュニケーションを行うためのスマホアプリを新しく開発しました。

なお、この記事で取り扱う範囲は、サーバーサイド実装のみとなります(Android アプリも 100 % Kotlin で書いていますが本記事では対象外とします)。リリース初期なので最低限の機能しか実装しておらず、比較的小規模なアプリになります。Kotlin コードにしておよそ 22,00 行・開発した API の数はおよそ 50 強でした。ユーザー認証には JWT を利用し、ユーザー情報の管理や決済管理などはバックエンドサービスに委託する構成になっています。システム全体はシンプルなレイヤードアーキテクチャを採用しており、DDD で開発していますが CQRS のような大仰なアーキテクチャは採用していません。DDD で言うところの「境界付けられたコンテキスト」はおよそ 6 つでした。

Kotlin 採用の動機

今回新しくアプリを開発するにあたって Kotlin を採用した理由は主に以下の 2 つです。

・Java からの移行先言語として Kotlin を検討したかった
・社内の技術的モチベーションを底上げしたかった

NewsPicks は約 4 年前から稼働しているサービスです。サーバーサイドは概ね Java 8 + Lombok で開発しており、NewsPicks 本体に関して言えば、ほぼモノリシックなシステム構成になっています。お陰様でローンチ以来順調にユーザー基盤も成長してきており、(随分前の数字なのですが)公表数値でユーザー数が 2 百万人強・月 1,500 円の有料会員数が 5 万人弱となっています。また、ライブ動画配信や今回アプリを作ったアカデミアプランなど、新規プロジェクトも次々と立ち上がってきており、直近ではエンジニアの数も 20 名弱まで増加しています。このような事業成長を鑑みると、そろそろ巨大なモノリスを少しずつマイクロサービスに分割していく必要性があるのではないかと考えていました。

一方でこのようなマイクロサービスの実装言語に関しては決め手に欠ける印象を持っていました。Java は安定しておりエコシステムも十分に発達していますが、如何せん冗長であることは否定できない短所です。この点に関しては Java 8 と Lombok の採用により多少改善しており、特別大きな不満があるわけではないものの、果たしてこのまま Java を使い続けるのかと問われると、ややモヤっとするというのが正直な気持ちでした。これまで Java でサービスを運用してきている組織/メンバーのナレッジは活かしつつ、もう少し書きやすくモダンな言語機能を持つプログラミング言語 、「Better Java」となる言語は無いものか — —そう思っていたところに登場したのが Kotlin でした。

Kotlin は(少なくとも僕にとっては)「ちょうど良い言語」です。静的型付けですが書きやすく、モダンな言語の良いところを取り入れていますが過度に行き過ぎていません(勿論不満な点はあります)。現状では Kotlin のエコシステムは Java のそれに立脚しているので、Java を書ける開発者にとっては学習コストも極僅かです。つまり文法の学習コストも限りなく低く、エコシステムに関してはこれまでに学んだものを使い回すことが出来ます。極端に CPU ヘビーな処理や省メモリで運用したいようなアプリケーションを書く場合には別でしょうが、大抵の Web アプリケーションでは Kotlin を使うことによるオーバーヘッドは問題にならないでしょう。コンパイル速度は Java 本体よりは多少遅いですが、少なくとも僕が触っている限りでは問題を感じるほど遅いわけではありません。数十万行・数百万行のコード規模になった時のコンパイル速度に関しては検証していませんが、昨今のマイクロサービスのような文脈で言えば、ひとつひとつのアプリケーションの規模は少しずつ小さくなっています。こうした観点から言えば、Kotlin は導入しやすく適度なメリットを享受できる「ちょうど良い」言語です。また弊社に関して言えば、職能ではなく事業ミッション毎にエンジニアをアサインしていることが多いので、Android エンジニアがサーバーサイドの開発に入りやすいなどのメリットもあり、採用を決めました。


Kotlin の良いところ

では具体的に Kotlin を使って便利だった点を紹介しながら、今回開発したアプリケーションで具体的にどのようにその機能を利用したのかを紹介していきたいと思います。

Java より「気軽に」書ける

Java と比較して圧倒的に気楽に「他のモダンな言語を使っているような感覚で」書けます。正直に言えば、Kotlin を採用して開発を始めた頃は「これなら Java 8 + Lombok と殆ど変わらないな 😩」と感じていたのですが、約 2 ヶ月使ってみると、この少しの差が大きな違いになっていることに気付きました。

凄く些細なところで言えばセミコロンレスであるところや data class でバリューオブジェクトが宣言できるところ。或いは if や try が式になっているところ。もう少し大きなところで言えば、コレクション操作が(JavaScript や Ruby を使っている時のように)直感的に書けること。何しろ Java ではコレクションを連結して文字列化するにしても collection.collect(Collectors.joining("\n")) のように書かなければなりません。filterNotNull() すら無く、fold しようと思ったら驚くほど複雑な定義の reduce を使うことになります。Kotlin を使うと、こういったちょっとした煩わしさから解放されます 😃

Java との相互運用性の高さ

Scala と比べると Kotlin は「Better Java」としての側面が強くなっています。

例えばコレクションライブラリに関して言えば、独自実装している Scala と比較して、Kotlin は java.util.* を流用しています(その上で拡張関数として幾つかの機能を追加しています)。ListMutableList が分かれているように見えますが、実際にはそれぞれに独自のコレクション実装があるわけではなく、リードオンリーな型を作って見た目上イミュータブルにしているだけに過ぎません。 しかしその結果、Kotlin から Java の API を呼び出す際はコレクションを変換する必要などはありません。これはつまり、ランタイムにおいて余計なコストを支払う必要がないということを意味します。

ビルドシステムに関しても標準で Gradle を採用しており、Java プロジェクトと同じように build.gradle を記述し、Kotlin のランタイムをインポートすれば問題なく動きます。新しいビルドシステムを覚える必要はありません。Java のライブラリを利用したければ、単に build.gradle に依存モジュールを追加することで、Java から使うのと何ら変わりなく API を呼び出すことが出来ます。

このように Kotlin は既存の Java のエコシステムと協調して動くようになっており、Java との相互運用性は驚くほど高いです。例えば(如何にもハマりそうな)リフレクションの利用に関しても、問題になることは基本的にありませんでした。

安定した開発環境

IntelliJ IDEA を開発している JetBrains 社が開発しているだけあり、IntelliJ IDEA での開発は非常に安定しています。動作速度なども問題ありませんし、当然ながら Kotlin の言語機能のアップデートと同時に IDE 自体がアップデートされるので、最新の言語機能をすぐに利用することが出来ます。

現代においてはプログラミング言語それだけを取り出して比較するのは片手落ちの議論です。充実した開発環境が保証されているということは Kotlin の大きなアドバンテージだと言えると思います。

拡張メソッド

濫用厳禁ではありますが、拡張メソッドを用いて読みやすいコードを書けます。実際には実装クラス内で private な拡張メソッドを定義することが多かったです。例えば以下の asDiscussionUser はユーザーをドメインモデルのロールオブジェクトに変換している箇所ですが、このような箇所でヘルパー的に private な拡張メソッドを使うことは多かったです(Scala だと implicit conversion を用いてもっと自然に書けるかと思います)。

もっとパブリックに拡張メソッドを使うケースでは、既存の Java ライブラリに対してヘルパー関数を生やすという使い方をする場面がありました。例えば以下は Retrofit で同期呼び出しをするためのヘルパー関数です。

委譲プロパティ

Kotlin には委譲クラスと委譲プロパティという言語機構がありますが、委譲プロパティは意外と使い勝手が良く便利に利用できました。例えば今回開発したサーバーアプリは、設定値をシステムプロパティで管理していますが、このプロパティを読み出すために委譲プロパティを実装して利用しています。

この SystemProperty は、実際にはシステムプロパティから読み出されます(例えば SystemEnv#stageappName.env.stage から読み出され、値が見当たらなかった場合は Stage.DEV という Enum になります)。これは以下のような移譲プロパティを実装することで実現できます(この例では基礎的な型のみに対応していますが、実アプリでは任意の型を解決出来るようにしています)。

こういった簡単なヘルパーを作るときに便利な機能だと思います。

レシーバ付きラムダ

Kotlin には型安全ビルダーという機能があり、Groovy のように簡単に DSL を定義することが出来ます。これは中置呼び出しや拡張関数などに加えてレシーバ付きラムダを用いて実現します。今回は RDB の ORM に JPA(Hibernate)を採用しているのですが、型安全ビルダーを用いて簡単な Criteriaのビルダーを定義すると便利でした。例えば以下は実コードですが、Criteria を利用して次のように検索することが出来ます。

結構なコード量になるのでポイントとなる部分を一部だけ抜粋しますが、これは以下のように定義しています。RootExpressionExpressionCriteriaBuilder.(path: Root<R>, context: CriteriaCompileContext) -> Predicate のようにレシーバ付ラムダとして定義されているのがポイントになります。

これに似た機能を提供しているライブラリに Spring Data JPA Specification DSL for Kotlin があります。これはもともとスマートニュースさんに遊びに行ったときに教えてもらったものなのですが、このライブラリ自体は Spring Data に特化していたため、もう少し汎用的な JPA のヘルパーライブラリとして自分にとって必要な機能を追加しながら実装しました。

レシーバ付きラムダはもっと単純なところにも使えます。例えば今回のアプリでは分散ロック、簡単な Pub/Sub、および一部データのキャッシュ/永続化先として Redis を利用していますが、このクライアントライブラリとして Jedis を採用しています。Jedis は単純なコマンドしか提供していないのでトランザクションやバッチ処理などは簡単なヘルパー関数を用意することが多いと思いますが、こういう場面でもレシーバ付きラムダは有効です。例えば以下のような簡単なクライアントを用意すれば、処理を記述するブロックは Pipeline をレシーバとして記述することが出来るためスッキリ書くことが出来ます。

sealed class

sealed class を用いてある程度網羅的に分岐をチェックすることが出来ます。今回ドメイン層での業務バリデーション結果については Either に統一しており、アプリケーション層でアプリケーション例外に翻訳しているのですが、ここで定義しているアプリケーション例外については全て sealed class として定義しています。sealed class を利用することで、Web インターフェース層で例外をエラーレスポンスに変換するコードを上手く扱うことが出来ました。今回 WAF として JAX-RS を利用しているのですが、ExceptionMapper を以下のように記述することが出来ます。これによりアプリケーション例外が増えたときに適切な HTTP レスポンスへの変換を強制できます。

他にもドメインイベントを基点に通知やビューモデルの生成などの処理を行っているため、こういった場面に sealed class を活用しています。

コルーチン

並列に I/O を処理するために Kotlin 1.1 で導入されたコルーチンを活用しました。「コルーチンとは何か」という人は、「任意の箇所で中断できる計算」であると考えると良いと思います。コルーチンには様々な使い道がありますが、典型的なユースケースは以下になるでしょう。

・非同期計算
・ジェネレータ

特に非同期計算においては、Future あるいは Promise スタイルのプログラミングよりも直感的なプログラミングが可能です。例えばコールバックスタイル・或いは近年良く見かける以下のような Promiseスタイルのプログラミングモデルと比較し、

コルーチンは比較的自然な文で非同期な処理を記述できます

ここでは try-catch により自然なエラー処理も可能です(少し細かく書けば上記のコード例は同期的に処理を行うので、非同期並列に処理を行いたい場合は fetch(foo)fetch(bar) を別のコルーチンで起動する必要があります 。

Kotlin におけるコルーチンとは何か

Kotlin のコルーチンはその殆どが言語機能ではなくライブラリとして実装されています。例えば Node や C# で馴染み深い async / await もライブラリ関数です。詳細については Qiita で公式ドキュメントを翻訳して下さっている方 がいらっしゃるので、こちらを読むのが良いでしょう。

コルーチンの実装状況

Kotlin のコルーチンは 1.1 の時点ではまだ experimental な機能であるため、フラグをつけることで有効化する必要があります。ただしコルーチンの実装自体が安定していないというわけではなく、現在はコミュニティによるフィードバックを受け付けている状態です。フィードバックを受けて多少の API の修正は想定されていますが、大きく API を変えることは予定されていません。また、現在コルーチンは kotlinx.coroutines.experimental パッケージで提供されていますが、仮に experimental でなくなった場合にも、当面 kotlinx.coroutines.experimental パッケージは残されるので、ある日突然使えなくなるということもありません。ライブラリ自体の品質も、最初に幾つかプログラムを書いて試した範囲では十分に実用に耐えうるクオリティだったため、問題ないと判断して導入しました。

アプリでの使いどころ

今回開発したアプリではバックエンドサービスとの通信が大量に発生します。特にユーザー基盤をバックエンドサーバーに任せているため、表示用のユーザー情報を大量に取得するようなケースでは、大量のユーザー情報取得リクエストを発行します。一般にバックエンドサービスとの通信が大量に発生するフロントエンドサービスにとって、バックエンドサービスの呼び出しに伴う I/O 待ちは好ましいものではありません。同期的に I/O するとたちどころにスレッドを浪費しスループットが下がることになります。必然的に非同期に I/O をする必要性が生じるのですが、そこにコルーチンを使えないかと考えました。

このような場合の対処方法は幾つかあり、NewsPicks でもこれまで様々な方法でこの問題に対応してきました。例えばある API では Node の JVM 版とも言える Vert.x を採用しています。ここでは非同期プログラミングの複雑さは RxJava で改善を試みました。また、NewsPicks 本体では Servlet 3.0 の非同期処理を使っています。Servlet の上で非同期処理をするために、非同期処理専用のワーカースレッドプールを作り、非同期処理は Future スタイルで実装しています。

これらはどちらも大きな問題を起こしてはいませんが、前者については Vert.x 自体の問題(JDBC 接続まで含めて全て非同期に処理する必要があり ORM など一般的な JVM エコシステムが利用できない) + Reactive プログラミングの複雑性 という 2 つの問題がありました。後者については Future スタイルでのプログラミングによる見通しの悪さ が課題になりました。

このような課題に対して、Kotlin ではコルーチンを使うことで自然な文で非同期処理を記述できるのではないかと考えました。

結論から言えば、コルーチンを使えばこういった問題を幾分か楽に実装することが出来ます。例えば以下は複数のユーザーをバックエンドから並列に取得するコードです。suspend 関数となっている以外は極めて自然な文で記述できていることが分かります(実際にはワンライナーで書いていますが可読性のために改行しています)。

ここで async を呼び出しているのを見れば分かる通り、Kotlin のコルーチンの並行性は常に明示的に指定します(context については後述します)。

なお、コルーチンの起動ポイントはコントローラになります。今回は JAX-RS を利用しているため、AsyncResponse を返す際にコルーチンを起動しています。以下はコルーチンを利用した API エンドポイントの実装例です。

コルーチンにおける共有ステートの受け渡し

一般的に Java アプリケーションでは、セッション情報・トランザクション情報・AOP のステートなどアプリケーションの共有ステートはスレッドにバインドされることが多いです。このようなスレッドにバインドされたステートをコルーチンに受け渡すために、以下のような専用の非同期タスク実行用コンポーネントを用意しています(以下は例になります)。

共有ステートをコルーチンに受け渡すためには CoroutineContext を利用します。これは AbstractCoroutineContextElement を実装することで簡単に実装できます。

このように実装した CoroutineSessionContext をコルーチンの起動時に指定することで、コルーチン間でステートを受け渡すことが出来ます。またコルーチンコンテキストは合成することが出来るので、例えば少し上に出てきた例のように CoroutineName などのコンテキストを合成して起動することが出来ます。コルーチンにバインドされたステートを取得するためには以下のようにします。

なお今回は各スレッドにバインドされたステートをコルーチンにも受け渡しつつ、スレッドにバインドされたステートも書き換えたいというケースがあったので、ContinuationInterceptor を利用してコルーチンの起動時に処理をインターセプトしています。例えばセッションユーザーを受け渡す場合は以下のようにしています。

これまで見てきたように、コルーチンは十分にプロダクションで利用することが出来ます。また、従来の FuturePromise スタイルの非同期プログラミングと比較して十分に分かりやすく記述できます。ステートの受け渡しも簡単で、これまでの Java の資産を活かしつつ簡単に利用できることが分かると思います。

分解宣言

Kotlin では data class など componentN 関数を定義しているオブジェクトを分解することが出来ますが、上述のコルーチンと組み合わせる場合にこれが便利になります。例えば以下のように 4 つの非同期処理を待ち合わせる場合にまとめて変数に代入することが出来ます。


Kotlin の微妙なところ

さてここまで Kotlin の便利な面を強調してきましたが、勿論良いところばかりではありません。以下では Kotlin を使ってイマイチだと感じた点を書こうと思います。

ラムダ式

まず筆頭はラムダ式の奇妙な文法です。最近良くあるラムダ式の文法は(アローかファットアローかのような些細な違いはあれ)arg: String -> { doSomething(arg) } のような定義だと思うのですが、Kotlin では引数が内側に入ってきます。つまり { arg: String -> doSomething(arg) } なのです。

ここは出来ればもう少し一般的な文法にして、Java に合わせて欲しかったなぁと思います(開発初期は良く書き間違えました・・・)。更に言えばラムダ式は return を省略できるのに、関数では省略できないなども少し違和感を感じるところです。

ちなみにこんな文法なので、 val f = {} と定義すると、f() -> Unit 型として解決されます 😵

when 式

when 式も便利なようで微妙に使いづらいです。所謂パターンマッチがないので自分で引数を分解する必要があります。どういうことかというと、つまり一度ローカル変数を置く必要があります(少し例が悪くてスミマセン)。

このように when 式では、一度 when 式より手前で変数を置いてから値を取り出して処理を継続する必要があります。この辺りは Scala を使っているような方は凄くストレスを感じるところだと思います。

廃れる可能性

良いところの裏返しになりますが、Kotlin は現時点ではあくまで「Better Java」の趣が強い言語です。これは学習コストが低いという良い面がある一方、Java 本体が Kotlin のように進化していくと Kotlin 自体が優位性を失うというリスクを抱えているという捉え方も出来ると思います。例えばかつての JavaScript にとっての CoffeeScript がそうであったように、Kotlin も本体の勢いに呑まれてしまう可能性は否定できません。Android と異なりサーバーサイド Java は常に最新のランタイムを使うことが出来るため、乗り換えは比較的容易です。折しもつい最近、今後の Java のリリースサイクルは半年毎になる という記事が世間を騒がせましたが、仮に今後 Java が JavaScript のようにドラスティックに変化した場合、独自の生態系を持たない Kotlin が廃れる可能性は無くはないと思います。

ツールチェイン

ほぼ Java のエコシステムを使えると言っても、当然ですが Kotlin では使えないようなツールも存在します。その筆頭が Findbugs や PMD のような静的解析ツールだと思います。例えばチームで開発しておりスキルレベルにバラツキがあるような場合、これらの静的解析ツールを使いたいというケースは出てくるかと思いますが、現状 Kotlin にはこのような静的解析ツールはありません。一応今回のプロジェクトでは Detekt を使いましたが、お世辞にもそれほど役に立っているとは言い難い状況です。また、コーディング規約なども、Checkstyle のように事細かに設定できるツールはありません(もちろん ktlint などの Lint ツールはあります)。IntelliJ IDEA でも Java ほど細かくフォーマットルールを規定出来ないのは少し残念なところです。例えば Java では以下のような画面でクラスやメソッドの宣言順を細かく定義できるのに対し、Kotlin には同等の機能はありません。


Kotlin の採用に向いている組織/向いていない組織

さてここまで Kotlin の良い面/悪い面について書いてきましたが、約 2 ヶ月 Kotlin で開発してみて、Kotlin の採用に向いている組織/向いていない組織が自分の中でハッキリしてきたので簡単に纏めておきます。あくまで僕の主観ですので、参考程度に読んで頂ければと思います。

向いている組織

既に Java を採用しており、メンバーの生産性・技術的モチベーションを高くしていきたいと考えている組織。かつ開発メンバー数が数人 ~ 数十人程度の比較的小規模の組織。このような組織にとっては Kotlin はこれまでのナレッジを活かしつつ生産性を高めることが出来る良い選択肢であると言えます。

また、既に Ruby など動的型付けのオブジェクト指向言語を使っているような組織が静的型付け言語への移行を検討したとき、Kotlin は悪くない選択肢だと個人的には思います(最近だと Retty さんが PHP からの移行先として Kotlin を選択されていますね)。「Java は冗長に思えて嫌だなぁ」と感じている人達でも、Kotlin はそれほど抵抗感なく受け入れられるのではないでしょうか。IDE の品質の高さも導入を推進しやすいポイントです。

向いていない組織

まず組織が極めて小規模であったり、短期間での作り捨てが目的なのであれば、静的型付け言語よりも動的型付け言語の方が、スピードを出すという意味では向いていると思います(実際に自分もそのような場合は Node や Ruby を使います)。

また、これまで Java を使った経験の少ない組織がいきなり Kotlin を使おうとした場合、Scala を使う方が良いケースはあると思います。これは Scala の方が Java のエコシステムとの結合度がやや低く(ライブラリなどは Scala 独自の生態系で完結している場合が多い印象)、言語としての機能が豊富であるためです。組織内で既に Scala を十分なレベルで使いこなせるエンジニアが多く存在するのであれば、敢えて Kotlin を利用する必要はないでしょう。逆に Scala と比べた Kotlin の利点は、言語機能の貧弱さ故に Java や一般的なオブジェクト指向言語から大きなジャンプが起きづらいところです。学習コストが低く緩やかに導入できる点が Kotlin を採用する主たる理由になり得ます。

他に向いていない組織としては、大規模でスキルレベルがバラバラな組織です。上述した通り Java と比べるとツールチェインが整っているわけではないので、スキルレベルにバラツキがある組織でやや統制を取りづらい面は否めないと思います。


余談:今回利用したライブラリなど

今回利用した主要なライブラリは以下の通りです。

Grizzly 2(HTTP サーバー)
Jersey 2(WAF)
Kodein(DI コンテナ)
Hibernate 5(ORM)
Jedis(Redis クライアント)
Retrofit 2(REST クライアント)
Jackson(JSON パーサ)

Kotlin のサーバーサイドと言えば Spring、というイメージが強いかと思いますが、僕自身は Spring の(Java EE のような)重厚感が好きになれず、自分で好きなものを組み合わせて使う主義なので使っていません。ご覧の通り殆どが Java のライブラリで、唯一 Kotlin らしい点は DI コンテナとして採用している Kodein ぐらいでしょうか。これは Kotlin の言語機能を活かした DI コンテナです。その他のライブラリの Kotlin での使いこなし術などについては機会があればまた別で紹介出来ればと思いますが、Hibernate(JPA)に関しては少々ハマりどころや妥協点などもあるので、簡単にご紹介しておきます。

Kotlin での JPA の利用

まず Kotlin で JPA を利用するには Kotlin JPA Plugin を利用する必要があります。これは JPA のエンティティに対して引数なしコンストラクタを自動的に生成してくれるプラグインです(但しリフレクション経由で生成するため実コードからは呼び出せません)。これを用いることで、次のように Kotlin でも自然に JPA のエンティティを定義できるようになります。

このセットアップさえ済ませれば、あとは Java とそれほど変わることなく自然に JPA を利用できます。ただし、もしエンティティを不変にしたい、data class を使いたいというような場合は注意が必要です。Kotlin の data class は継承できないので、JPA の subclass などを利用できなくなってしまいます。JPA の機能を十分に利用したい場合、エンティティは通常の class として定義する方が良いでしょう。

余談にはなりますが、今回は実装コストを勘案して JPA のエンティティを直接ドメインモデルとして利用しています。最初はエンティティを不変クラスとして定義して実装していたのですが、不変なドメインモデルと JPA エンティティの変換処理を実装するのが過度な設計による開発コストだと感じられたこと、JPA とイミュータブルなモデルの相性が悪いこと、実態として集約ルートは大抵の場合 JPA のエンティティと一致することが多いなどの理由から、このような実装になりました。結果、エンティティ自体はミュータブルな作りになっています(VO は data class を用いているのでイミュータブルです)。ミュータブルなモデルは出来るだけ避けたいのですが、今回はコスパ見合いで妥協したところです。Java の ORM については余りしっくり来ているものが無いので、余裕があれば Kotlin で自作したいところです(経験上 ORM を作るとついつい凝ってしまうのですが……)。

エンティティに関しては以下のような基底クラスを定義し、ドメインイベントのパブリッシャをフレームワークレイヤでインジェクトしています。

イベントパブリッシャは Hibernate でエンティティをロードしたり更新するタイミングでインターセプタを引っ掛けて、フレームワークレイヤでインジェクトしています。実際に配信されたドメインイベントは JTA トランザクションの synchronization に引っ掛けてトランザクショナルにコミットされ、各ドメインイベントのサブスクライバで処理されます。

この「フレームワークレイヤでインジェクトする」というやり方は他にも採用しており、例えば集約が巨大なエンティティのコレクションを持つ場合、プロキシオブジェクトをインジェクトするのにも使用しています。DDD で巨大なエンティティのコレクションを扱う場合、まず大前提として「本当にそのコレクションは集約ルートと同じライフサイクルで扱う必要があるか」を考えることになると思いますが、プロキシオブジェクト使うという手法も有効だと考えています。一例を挙げれば先の Comment#reactions です。コメントには大量のリアクションが高頻度で付くことが想定されるため、プロキシオブジェクトをインジェクトしています。これはコメントのリポジトリがエンティティのロードイベントに対するリスナーを実装しており、内部的に Redis を永続化ストアとして利用するようなコレクションのプロキシオブジェクトを生成してインジェクトするようになっています。


まとめ

ここ 2 ヶ月ほど Kotlin に触れてみた感想と、NewsPicks での活用事例について簡単にご紹介しました。

僕自身はここ 1–2 年ほど本格的な開発からは離れがちで、かつ今回のプロジェクトも他の案件と並行しての開発だったので特に初月はなかなか開発時間が取れず、最初はスピードが出なくて苦しかったのですが、それでも言語機能の理解や技術検証・アーキテクチャ設計などを含めて 2 ヶ月でアプリをリリースまで持っていくことが出来ました。内訳としては Kotlin 自体の調査とライブラリ選定に 2 日程度、開発環境のセットアップに半日、アーキテクチャ設計と基礎コンポーネント/フレームワーク層の実装・およびユースケース処理の雛形作成に 3 日というところで、初期開発のセットアップに関してはおおよそ 1 週間程度で終わりました。そこから暫く別件のプロジェクトが忙しくなったこともあり開発が停滞したのですが、10 月は順調に開発を進めることができ、本格的な開発を始めてからは特に詰まる所もなく進められたと思います。今回時間がかかったのは殆どがアーキテクチャやドメインの設計で、アプリケーションの実装に詰まる所は特にありませんでした。1 つだけ引っかかったのはコルーチンの使い方で、最初のうちコルーチンの使い方を間違えてデッドロックを起こしてしまいました 😢

とはいえそれ以外には特に問題なく開発を進められたという点・言語理解と初期開発のセットアップがすぐに完了したという点は、やはり Java エンジニアにとっての Kotlin の学習コストの低さを証明している事実ではないかと思います。

Kotlin の良いところ/悪いところ、向き不向きまで含めて自分の経験から纏めたものですので、読者によって異論反論などはあると思いますが、この記事が何らか皆さんの参考になれば幸いです。

最後に

NewsPicks では腕に覚えのあるサーバーサイドエンジニアを募集しています。冒頭にも書いた通り NewsPicks では次々と新規事業が立ち上がってきており、ユーザー数も着実に伸びています(US でのチャレンジも始まっています!

来年も引き続き新たなチャレンジは行いつつ、NewsPicks という巨大なモノリスを少しずつ分割し、事業成長に見合う基盤へと整えていく必要があります。フロントに立って事業開発にコミットするという方も、成長中サービスのバックエンドを整えるチャレンジをしたいという方も歓迎しておりますので、ぜひお気軽にご連絡下さい 😀

Twitter でも Facebook でも Wantedly でも大丈夫です!

monzou

Written by

monzou

NewsPicks をつくっています。最近はプロダクト/チームマネジメント寄りの仕事が多め。前職ではデリバティブのトレーディングシステムなど金融フロントシステムのアーキテクトをしていました。

monzou

Written by

monzou

NewsPicks をつくっています。最近はプロダクト/チームマネジメント寄りの仕事が多め。前職ではデリバティブのトレーディングシステムなど金融フロントシステムのアーキテクトをしていました。

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store