title | emoji | type | topics | published | publication_name | |||||
---|---|---|---|---|---|---|---|---|---|---|
ログラスのバックエンド技術スタック2023 |
📑 |
tech |
|
true |
loglass |
:::message この記事はログラスアドベントカレンダーの 20 日目の記事です! :::
こんにちは、ログラスの小林(@mako-makok)です!
昨日は@asa_kossyさんの「ログラスのプロダクトマネージャーチームが今年取り組んだこと、いま苦労していること 2023」でした。
ログラス社はありがたいことに、 CTO 協会主催の「開発者体験が良い」イメージのある企業で 25 位にランクインさせていただいております。 この結果は非常に光栄だと思っており、自分が想像する要因としては DDD、スクラム、技術的投資の 3 点だと思っています。
https://zenn.dev/yuitosato/articles/9db2a0fe90313e https://levtech.jp/media/article/interview/detail_304/ https://speakerdeck.com/urmot/loglass-technical-investment https://note.com/go_nambu/n/n9b15c88dcf10
これらは継続的に活動しています。 ライブラリバージョンアップは欠かさずやりますし、DDD に関しては社内で DDD という言葉はもうほぼ使われていないレベルで浸透しており、機能追加の際はドメインエキスパートと会話つつ、モデルと実装を行き来して開発しております。
この認知は非常に嬉しい結果ではありますが、実際は開発者体験に課題を感じる部分はまだまだあります。 もちろん都度改善を回してはいるものの、開発者体験は青天井なので常に高みを目指したくなってしまいます。
今回は、現状のログラスの技術スタックをご紹介し、
- ログラスってまだまだ改善の余地がいっぱいあるな、いっちょ俺が直したる
- ライブラリの選定に迷っているけど参考になった
と感じていただけるといいな、と思っております。
「Loglass 経営管理」をはじめとした Loglass シリーズ(以下 Loglass)は、経営管理領域の中でも、特に予算・実績の管理と予実対比に強みがあるアプリケーションです。 多くの場合、各企業で Excel で管理されている予算データと会計システム等に存在している実績データを取り込み、それぞれ同じ単位で集計し対比することで経営分析を行えます。
このことから、アプリケーションとしては以下のような特性があります。
- データ量は BtoB のアプリケーションにしては多め
- つまりデータ取り込みは大量データを扱う
- 特に実績は会計システムから出力されるレコードを保持する必要がある
- 大量のデータをバックエンドで集計してクライアント側で大量データを表示する
- バックエンド・フロントエンドともにパフォーマンスがボトルネックになることが多い
- 逆にトラフィックが急に増えてスパイクするようなことは現状ない
- 予算は Excel を利用していることが多く、xlsx ファイルをアップロードする機会が多い
- CSV アップロードはほぼ無い
- 正規化されていないデータをアップロードするケースもある
- フレームワーク: SpringBoot
- 言語: Kotlin
- O/R マッパ: jooq, Exposed
- データベース: PostgreSQL
- マイグレーション: Flyway
- テスト: jUnit5, mockk
- lint・formatter: ktlint
- API ドキュメント: springdoc-openapi
- クライアントコード生成: openapi-generator
https://spring.io/projects/spring-boot
SpringBoot は、Spring Framework という Web 開発のためのモジュールを、一定ひとまとめにしたフレームワークです。 枯れたフレームワークのため、機能開発という面に関しては基本的に何をするにも困りません。
特徴的なのはそのモジュールの多さで、トランザクションや認証はもちろんのこと、モジュラモノリスまでサポートされています。 開発に必要なものはほぼ Spring Framework のモジュールとして用意されていますし、ドキュメントもとても充実しています。
ドキュメントが充実している・アプリケーション開発に必要なモジュールのラインナップが充実している他に、 Aspect Oriented Programming(以下 AOP)という概念があり、それを利用することでフレームワークの利用者はアプリケーションのロジックを書くことに専念できます。
https://docs.spring.io/spring-framework/reference/core/aop.html
例えば、SpringBoot でトランザクションのハンドリングは以下のようなコードで行うことができます。
@Transactional
をつけることにより、自動的にメソッドの開始時にトランザクションが開始さる- 自動的にメソッドの終了後にトランザクションが終了される
- 例外が発生するとロールバックが行われる
@Transactional
public class FindUserUsecase {
public User findUser(String id) {
// ...
}
}
AOP は自前で定義が可能です。
例えば、下記コードでは com.xyz.dao
パッケージ以下に定義されているメソッドが正常に終了(例外が発生していなければ) doAccessCheck
が実行されます。
@Aspect
public class AfterReturningExample {
@AfterReturning("execution(* com.xyz.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
他にも
- あるアノテーションがついていたら
- 特定の例外が throw されたら
と、かなり細かくハンドリングできるのでロギングなど共通処理を抜き出して隠蔽することが可能になっています。 SpringBoot にはここでは紹介しきれないくらいたくさんの便利な機能があるので、気になる方はぜひドキュメントや実際に触ってみてください。
とはいえ SpringBoot も万能ではなく、他のフレームワークや言語と比べて気になる部分も存在します。
- SpringBoot の起動時は、DI コンテナに全コンポーネントを登録する処理が走るため、アプリケーションが大きくなると相対的に起動時間が長くなる
- 数秒の差だが、開発の中ではもちろん何回も起動するので、地味にストレスになる
- チューニング手法はいくつかあるが、手をつけられていない
- JVM 自体の起動が遅い/JVM はリソース消費が激しいというのが主な理由
- もちろんチューニングすれば一定解決はする
https://speakerdeck.com/hhiroshell/jvm-on-kubernetes
- 最近は GraalVM など、ハイパフォーマンスなランタイムやネイティブイメージへのビルドも可能になっているため、クラウドネイティブ化への投資は進んでいる
- とはいえ Loglass は BtoB の SaaS で、アプリケーションのトラフィックが急に増え、オートスケールが走ることはほぼないので問題にはなっていない
AOP は便利ですが、共通処理が増えてくると共通処理同士がかち合って意図せぬ挙動になる/そもそも AOP のコード自体が追いづらいという問題があります。
例えば Loglass ではテナント間でデータが漏洩しないように、AOP で Row Level Security 関連の事前設定を行っています。
https://www.slideshare.net/koichiromatsuoka/postgresql-springaop
実装者が意識せずとも、セキュリティが担保される素晴らしい仕組みですが、今まで JVM に触れてこなかったメンバーに説明する際のコストはどうしてもトレードオフになってしまいます。
Kotlin はとてもバランスの取れた言語で、言語仕様もシンプルかつ専用の API も非常に便利で、書き心地がとても良いです。
またアップデートの周期が非常に早く、次バージョンではコンパイラの刷新によってパフォーマンス含め大幅に改善されるとのことなので、今後の進化も楽しみです。 https://kotlinlang.org/docs/roadmap.html
※ Java との比較が中心です
- 拡張関数
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to the list
this[index1] = this[index2]
this[index2] = tmp
}
fun main() {
// あたかも特定のデータの振る舞いとするかのように記述可能
mutableListOf(1, 2, 3).swap(0, 2) // => [3, 2, 1]
}
- Java に比べて記述量が少ない
// Streamへの変換/終端操作/itでのelementへのアクセスが可能
listOf(1, 2, 3).map { it * it }
- 非同期処理(coroutine)
- 所謂軽量スレッドで、I/O 待ちや、複数スレッドでの並列処理が可能になります
https://kotlinlang.org/docs/coroutines-basics.html#scope-builder-and-concurrency
Kotlin 製のモックライブラリですが、非常に便利です。 テストの頁で詳しく記載します。
- GitHub で kotlin で Issue を検索するといくつかある
https://github.com/spring-projects/spring-boot
- 非同期処理や AOP でバグは何度か実際に踏みました
- これは Kotlin 特有の箇所が追加でかかってくることが原因か
- 具体的にベンチマークを取ったわけでは無いので杞憂の可能性
- 上述した通り、K2 コンパイラという新コンパイラに刷新される予定のため、バージョンアップにより速度向上は期待できる
- Kotlin のオブジェクトは immutable が基本になっているため、カジュアルに変数へ展開していると意図せずメモリを消費することがある
- こちらに関連して、GC のタイミングが一部 Java と違うので、大量データを捌くときなどは注意
- こちらの記事がとてもよくまとまっていたので、参考にさせていただきました
https://retheviper.github.io/posts/kotlin-hidden-cost-1/
Flyway を利用しています。
Loglass の DB スキーマは SQL をバージョニングしています。
実行は Gradle の公式のプラグイン付属のスクリプトを利用しています。( flywayMigrate
)
以前 Flyway の記事を書かせていたので、こちらもよろしければ御覧ください。
https://zenn.dev/mako_makok/articles/use-flyway-migration
Flyway の気になる部分は特別ないのですが、複数人で開発しているとマイグレーションスクリプトのバージョン番号がコンフリクトして修正の手間が発生します。 この部分に関しては GitHub のマージキューやプロテクト、CI など仕組みで解決していきたいと思っています。
jooq
を使っています。
https://www.jooq.org/
jooq は専用の API を扱う必要があるものの、SQL ライクに記述でき、単純なクエリなどはほぼ copilot が書いてくれるためとても体験が良いです。
DSLContext create = DSL.using(connection, dialect);
create.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count())
.from(AUTHOR)
.join(BOOK).on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
.where(BOOK.LANGUAGE.eq("DE"))
.and(BOOK.PUBLISHED_IN.gt(2008))
.groupBy(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.having(count().gt(5))
.orderBy(AUTHOR.LAST_NAME.asc().nullsFirst())
.limit(2)
.offset(1)
.forUpdate()
.fetch();
- サンプルの
AUTHOR
やBOOK
などが自動生成されるイメージ - コードの自動生成することによって、タイプセーフに SQL を書くことができる
- あるカラムを drop したときコード生成の結果削除されるので、そのカラムの利用箇所はコンパイルエラーになる
実運用としては
- 前述の Flyway の
flywayMigrate
- jooq の自動コード生成
- テストデータの流し込み
をひとまとめにしたスクリプトを用意することで、DB からコード自動生成まで一気通貫で行うようにしています。
概ね体験は良いのですが、課題点としては自動生成コードを Git に commit しているので、自動生成漏れが発生して、merge 後に気づく問題があります。
Flyway 同様、GitHub のマージキューやプロテクト、CI など仕組みで解決していきたいと思っています。
Exposed は Kotlin で書かれた JetBrains 製の O/R マッパ です。そのため Kotlin と非常に相性は良いのですが、
- 公式を含めてもドキュメントがかなり少ない
- Exposed の SpringBoot プラグインが Exposed のメンテナによって管理されているため、SpringBoot の追従に遅れてしまう
- プラグインで SpringBoot のバージョンがロックされてしまう
などの問題があります。
やはり flyway でバージョニングされたスキーマ + jooq の自動コード生成がとても便利なため、現在は jooq への置き換えを行っており、Exposed で書かれたコード割合は減少しています。
テストランナーは jUnit5
、モックは mockk
を利用しています。
https://junit.org/junit5/ https://mockk.io/
jUnit5 はパラメータ化テストが若干書きづらいという課題があり、それを解決するため一時期 kotest
という、Kotlin 製のテストライブラリを検討した時期があったのですが、
- 乗り換えコストが大きい
- 得られるメリットはパラメータ化テストが一定書きやすくなる
- IntelliJ に専用のプラグインを入れる必要がある
といった理由で見送りました。 とはいえ圧倒的に書き味は良くなるので、初期選定時などでは候補として有力です。 https://kotest.io/
mockk は非常に強力で、少ない記述量でモックを書くことができます。 static な関数のモックや柔軟な値キャプチャもでき、非常に高機能です。
- 枯れており、ドキュメントが豊富かつ様々な記事もある
- エクステンションが豊富で、少し込み入ったことをするときに既にライブラリがある
https://zenn.dev/loglass/articles/595a91af94ff27
- 少ない記述量
- static な関数のモック、キャプチャなど基本的になんでもモックと検証ができる
パラメータ化テストが非常に高機能ですが、他のライブラリと比べると若干書きづらい部分があります。 https://qiita.com/oohira/items/5030182af29a30166868
ちなみに kotest だとかなり簡潔に記述できます。 https://kotest.io/docs/proptest/property-test-functions.html
- なんでもできる反面、簡単に実装の詳細をテストできてしまう
- そもそもモック自体使い所はかなり絞ったほうが良い
- テストしづらければ interface を変えることを検討した方が良い
- テストし辛いコードが出現したときに mockk で解決してしまいがち
- 直近のアップデートでパフォーマンス劣化? が起きていそう(CI でメモリが溢れてたまに落ちてしまう)
ktlint
を利用しています。
https://pinterest.github.io/ktlint/1.0.1/
Gradle プラグインを入れており、 ktlintFormat
を実行しフォーマットをかけ、CI でチェックを行っています。
特殊な設定もしておらず、基本的に生で使っています。
detekt
に移行するかはかなり迷っています。
https://detekt.dev/
detekt だとフォーマッティング以外にもカスタムチェックや IntelliJ でのチェックなどがあり、便利なのですが Kotlin のバージョンがロックされやすいのがトレードオフになります。
もちろん ktlint にも通ずることではあります。 しかし detekt は ktlint と比べて機能が豊富(detekt formatter 自体 ktlint のラッパー)な分、体感 Kotlin のバージョンアップ時に detekt が壊れてしまうことが多い気がしています。 特に Kotlin はバージョンアップ頻度が高いので、非常に迷うところです。 折衷案として、現在はランタイムを分けて detekt を利用しており、CI で non null assertion をしている箇所を怒るなどをしております。
https://zenn.dev/loglass/articles/51958a02455a10
springdoc-openapi
を利用しています。
https://springdoc.org/
SpringBoot を立ち上げると自動的に Swagger が立ち上がります。
以前まで SpringFox
を利用していましたが、開発が止まっているためこちらに乗り換え済みです。
https://springfox.github.io/springfox/
Loglass ではバックエンドの Controller の実装を正として開発を進めており、yml を書くことはしていないです。 こちらは特に気になる部分もなく、快適に使わせていただいています。
openapi-generator
を利用し、 typescript-axios
で実行しています。
https://openapi-generator.tech/
SpringBoot を起動した状態で専用の npm script を実行すると、API のスキーマを読みに行きクライアントコードを自動生成しています。
- 様々な generator があり、そこから選択して利用できる
- TypeScript から扱いやすい型で生成してくれる(enum が union types で生成される)
そもそも自動生成のフローを見直したほうがいいのでは? という気もしており、悩ましい部分です。
- クライアントコードが 1 ファイルにまとまるため、数万行のファイルになり重い
- namespace が区切られていないので、controller に生やすメソッドは全てユニークにする必要が出てくる
- 例えばリソースの名称が users であれば、コントローラーのメソッドは
create/find/list/delete/update
など単純なものにしたい - 他の controller の名称とメソッド名がかぶると list1, list2…のような連番でクライアントコードが生成されてしまう
- 例えばリソースの名称が users であれば、コントローラーのメソッドは
- SpringBoot を起動しないとクライアントコードを生成できない
- メンテナンスされていない generator がいくつかある
今回はバックエンドが中心のため割愛しますが、フロントエンドに関してはいくつかの解説記事がありますので、ぜひそちらを参照ください。
https://zenn.dev/yuitosato/articles/9db2a0fe90313e
https://zenn.dev/loglass/articles/abe85e10e229f3
Loglass の細かいライブラリ群まで深掘らせていただきました。
このように、ログラスの開発はまだまだ多くの伸びしろがあり、フロントエンド/バックエンドともに改善することでグッと開発者体験を上げていけると思っています。
「いっちょ直したる!」「そここうすると良くなるから教えてあげてもいいよ」という方がいらっしゃいましたらぜひお話させてください。
https://hrmos.co/pages/loglass/jobs/1813462408235663423 https://hrmos.co/pages/loglass/jobs/B0001