エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

アンケート作成システムのサーバサイドをKotlin + Quarkus + Doma, CQRSで構築している話

エムスリー エンジニアの岩本です。

現在私の所属しているチームでは、以前作成したアンケートを作るためのシステムのサーバサイドリプレースを検討しています。 ちなみにクライアントサイドは下記のものです。

qiita.com

採用した技術

以前のサーバサイドはJavaで作られており、再利用可能な既存の資産もあるため、JVM系の言語を検討しています。 他のシステムではKotlin + SpringBootを使っているため同様の構成も考えられるのですが、 SpringBootが開発しているうちに重くなり、起動に数十秒かかるようになり開発スピードが遅くなってしまったことや 新しいものに挑戦したいという技術者の考えを重要視しているため、Kotin + Qurkus + Domaを採用することにしました。

なぜQurkus?

最近はSpringBoot以外のFWも選択肢があります。Kotlinを使いたいということを考えると、下記の選択肢があがりました。

  • Ktor
  • Micronaut
  • Helidon
  • Quarkus

そこで、Star数の伸びを見てみると下記のようになっています。 f:id:cpw:20200302110737p:plain Star history いずれのFWも伸びていますが、Quarkusの伸びが頭1つ抜けています。

実際にQuarkusを軽く使ってみたところ軽量で、使い勝手も悪くありませんでした。

  • 軽量
  • OpenAPIが簡単に使える
  • SpringBootとほぼ同じ書き方も可能なため、メンバーの学習コストも高くない
  • 技術的に新しいことにチャレンジしたい

新しいFWなのでリスクは有りますが、現状のチーム内はマイクロサービスアーキテクチャを採用しているため、 1つ1つのシステムの粒度は小さく抑えられています。この設計方針によってリスクは抑えられためQuarkusを選択しました。

  • アンケートの編集と回答画面が今回のシステムの担当範囲
  • その他のエムスリーに特化した管理画面やチャネル部分に関しては別システムで構築済み

SQLアクセスはDoma

次の選択はSQLで使うライブラリです。要件としては以下の2つです。

  • 複雑なSQLをかけること
  • SQLを簡単にコードとマッピングできること

これらの2way SQLを実現しようとすると新し目の良いライブラリは見つからず、 すでに採用実績があるDomaを選択することになりました。

ただし、Quarkusはリフレクションを使っているとnativeモードで動かすことができないという問題もありますが、 nativeモードで動かす予定はないので今回は気にしないことにします。 検証していませんが、Domaはリフレクションを使っているので、nativeモードでうごかない可能性があります。

Domaの記述方法

KotlinをDomaで記述する場合には下記のようにコードを書きます。 AnnotationTarget.CLASSApplicationScoped::classを指定していますが、バージョン2.27.0から使えるように修正してもらいました

@Dao
@AnnotateWith(annotations =[
    Annotation(target = AnnotationTarget.CLASS, type = ApplicationScoped::class),
    Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject::class)
])
interface SurveyDao {
    @Select
    fun selectById(id: String): SurveyDomaEntity?

    @Select
    fun selectByPublicId(publicId: String): SurveyDomaEntity?

    @Insert
    fun insert(entity: SurveyDomaEntity): Result<SurveyDomaEntity>

    @Update
    fun update(entity: SurveyDomaEntity): Result<SurveyDomaEntity>

    @Delete
    fun delete(entity: SurveyDomaEntity): Result<SurveyDomaEntity>
}

エンティティは下記のようにdata classで定義します。

@Entity(immutable = true)
@Table(name = "survey")
data class SurveyDomaEntity(
        /** ID  */
        @Id
        @Column(name = "id")
        val id: String,
        ...
)

Quarkus + Domaで見つかった問題

また、annotation processingで生成されるDomaのコードがJavaであるため、コンパイルされたクラスはbuild/classes/javaに出力されます。 しかし、kotlinはbuild/classes/kotlinに出力されますが、Quarkus v1.1.1ではQuarkusが片方のクラスしかみてくれないため Domaのクラスが見つからないという不具合がありました。

当時すでにIssueとして上がっていたので、 そのうち対応されるはずということで、下記の対応をbuild.gradleに適用して不具合を回避しています。

compileJava {
    doLast {
        // Quarkusが/build/classes/kotlin/mainしかみないため、annotation processingしたクラスをコピーする
        copy {
            from "$projectDir/build/classes/java/main"
            into "$projectDir/build/classes/kotlin/main"
        }
        File f = file("$projectDir/build/classes/java/main")
        f.deleteDir()
    }
}

設計方針はCQRS

チーム内で作成している他のシステムでは、クリーンアーキテクチャの考え方を採用しています。 更新系に関してはすごく納得しているのですが、参照系の実装が不満でした。 参照系では更新系とは必要となるデータが異なり、複雑なデータの結合が必要となります。 しかし、現状のアーキテクチャでは更新系で使っているエンティティに一度マッピングし 参照系に必要な形へ整形して出力しており、必要以上に複雑だなと感じていました。 そこで、見つけたのが下記の記事です。

docs.microsoft.com

まさに私が感じていた不満点を解決してくれるものでした。記事自体はMicrosoftのものなので、 C#で具体例が書かれていますが、考え方は言語に寄らないので、この設計方針を参考にシステムを構築しています。 なおパッケージを分けることで設計方針を明確にしています。

パッケージ 設計方針
command クリーンアーキテクチャの考え方
query Domaを使って出力専用のモデルを構築して返す

今、絶賛開発中ですがいまのところ開発しやすい状態をキープできています。

まとめ

  • マイクロサービスアーキテクチャを採用していることで、攻めたアーキテクチャのKotlin + Quarkus + Domaを採用するができた。
  • CQRSで設計したら参照系を心地よく開発できている。

We're hiring!

エムスリーではKotlinでサーバサイド開発をしたいエンジニアを募集しています。 社員とカジュアルにお話することもできますので、興味を持たれた方は下記よりお問い合わせください。

open.talentio.com

jobs.m3.com