株式会社ヘンリー エンジニアブログ

株式会社ヘンリーのエンジニアが技術情報を発信します

Gradleプロジェクトを分割するときに僕が考えたこと

株式会社ヘンリーでSREなどをやっている戸田(id:eller)です。

私は先日より複数のブログやプレゼンで「Gradleプロジェクトを分割したいけどできてない」と言っております。 Gradleプロジェクト分割にはコードの置き場所が明確になってプログラマの心理的な負担を下げ、Gradleプロジェクトという単位で依存を管理することで不正な依存が入る余地を減らす利点があります。またKotlinコンパイルひいてはGradleビルドの高速化にも繋がるため、プロダクト品質や開発体験を向上するにあたって重要な施策だと考えています。

そして……とうとうやりましたよ!プロジェクト分割を!!!🎉 ということで今回はGradleプロジェクトを分割する際に配慮したことをまとめておきます。

そもそもGradleプロジェクト分割は何が難しいのか

Gradleに限らずMavenでもAntでも良いのですが、プロジェクトをあとから分割することは至難の業です。それは多くの場合、クラスやパッケージの依存関係が複雑かつ循環していることが多いためです。

Javaのウェブアプリケーションは古くから3層アーキテクチャなどの構造が知られており、構造化が進んでいるはずなのですが、現実は厳しいものです。個人的にも ArchUnit を使ったりGradleのプロジェクト分割を使ったりして工夫してきましたが、新規プロジェクトはともかく既存プロジェクトは泥団子状態のことが多いです。

逆に言えば依存関係が整理されていれば、IDEやコンパイラを活用することでプロジェクト分割をスムーズに行えます。コイツが最初の山であり、ラスボスなのです。

import文ベースで粗く依存関係を見るところから着手

依存関係を見るのは、先のスライドで紹介した jdeps による解析が簡単です。これで循環を見つけられればしめたものです。ひとつひとつ見ていき、これをほどいていきましょう。

ほどきかたは本当にケース・バイ・ケースなので、勝ちパターンのようなものはありません。気合と根性!です。ですがリファクタリングの一種ではあるため、広く知られているリファクタリング手法が適用可能です。参考書籍として自分からは、だいぶ古いですが「レガシーソフトウェア改善ガイド」をおすすめしておきます。

とはいえケース・バイ・ケースと言われてもイメージが湧かないかと思いますので、弊社の事例から単純なものを紹介します。

クラスを別パッケージに移動した事例

たとえば gRPC API で使うデータ型のための extension が見た目の循環を生んでいることがありました。次の依存関係はパッケージのレベルは循環していますが、クラスのレベルでは循環していません:

graph LR
  subgraph service
    FooService --> BarService
    BarExtension
  end
  subgraph model
    FooModel --> BarModel
  end
  FooService --> FooModel
  BarService --> BarModel
  FooModel --> BarExtension

この場合は、 extension の置き場を調整して循環を断ち切れます:

graph LR
  subgraph service
    FooService --> BarService
  end
  subgraph model
    FooModel --> BarModel
    BarExtension
  end
  FooService --> FooModel
  BarService --> BarModel
  FooModel --> BarExtension

プロジェクトもきちんと切り分けられそうです:

graph LR
  subgraph foo
    subgraph foo.service
      FooService
    end
    subgraph foo.model
      FooModel
    end
  end
  subgraph bar
    subgraph bar.service
      BarService
    end
    subgraph bar.model
      BarModel
      BarExtension
    end
  end
  FooService --> BarService
  FooModel --> BarModel
  FooService --> FooModel
  BarService --> BarModel
  FooModel --> BarExtension

もちろん移動することに不都合がないかどうかは検討が必要ですが、多くの場合は問題になりにくいのではないかと思われます。またこの過程で internal 修飾子を削除した箇所がそこそこあり、その影響があるかどうかも確認していました。

ドメイン外の概念を見つけてプロジェクトから追い出す

もうひとつ弊社の事例を紹介します。今回は一枚岩のプロジェクトからひとつのドメインに属する概念を取り出すことを目的としていました。理想的には次のようになってほしかったわけです:

graph LR
  subgraph 元一枚岩
    FooService --> FooModel
  end
  subgraph 新プロジェクト
    BarService --> BarModel
  end
  FooService --> BarService
  FooModel --> BarModel

ところが実際には、新プロジェクトのクラスが依存する「新プロジェクトには属するべきではないクラス」が存在します。 これを新プロジェクト内に保持してしまうと、新プロジェクトがまた新たな一枚岩になってしまうリスクがあると感じました:

graph LR
  classDef hoge fill:orange;
  subgraph 元一枚岩
    FooService --> FooModel
  end
  subgraph 新プロジェクト
    BarService --> BarModel --> HogeEntity:::hoge --> HogeValueObject:::hoge
  end
  FooService --> BarService
  FooModel --> BarModel
  FooModel --> HogeEntity

よってこの「あるべきではないクラス」を切り出すための別のGradleプロジェクトを作成し、そこに該当するクラスを追い出しました:

graph LR
  classDef hoge fill:orange;
  subgraph 元一枚岩
    FooService --> FooModel
  end
  subgraph 新プロジェクト
    BarService --> BarModel
  end
  subgraph shared
    HogeEntity:::hoge --> HogeValueObject:::hoge
  end
  BarModel --> HogeEntity
  FooService --> BarService
  FooModel --> BarModel
  FooModel --> HogeEntity

この新しく作成した shared プロジェクトには、今回対象としなかったドメインに属するクラスが入ってきます。プロジェクト間で共有されるためのクラスやユーティリティであれば問題ないのですが、意図せず入ってきてしまったクラスもいくつか見つかりました。今後のリファクタリングで正しい居場所に動かす必要がありそうです。

Gradleプロジェクト分割による効果

当施策の定量的効果として、Gradleビルド高速化効果がどの程度あったのかを見てみました。1週間ほど観察しましたが、残念ながらGitHub Actionsにおけるビルド高速化効果は確認できませんでした。 理屈の上では、sharedプロジェクトないし今回切り出したプロジェクトに変更が入らなければ、それらに属するコードのコンパイルとテストを実施しなくて済むはずなんですけどね。

遠目に見ると20~30秒くらい早くなったかな?という気はしますが明らかな改善ではないですし、試行回数が少ないこともあるためしばらく様子を見たほうが良さそうです。

一方で定性的効果としては、プロジェクト分割による見通しの良さが評価されています。以前から package を使って整理を試みていましたが、package では依存関係の制約が緩く、意図しない依存が混ざることがありました。 プロジェクトを分割することで、こうした依存が明確に洗い出され、今後も意図しない依存の混入が起こりにくくなる効果が期待されています。

まとめ

以上で今回実施したGradleプロジェクト分割の概要を説明しました。依存の整理さえできれば問題ないこと、依存のほどきかたはケース・バイ・ケースだがリファクタリング手法が適用できること、プロジェクト分割によって新たなリファクタリング課題が見つかることについて触れました。

定量的効果であるビルド高速化効果はまだ確認できていませんが、定性的効果としての見通しの良さは package による整理よりも高い成果が得られたと考えています。 製品開発のスケールしやすさを確保するためにも、今後も分割を進めていこうと考えています。

ヘンリーでは各種エンジニア職を積極的に採用しています。医療ドメインに興味がある方も、GradleやSREに興味がある方も、ぜひカジュアル面談でお話させていただければと思います。よろしくお願いいたします!

jobs.henry-app.jp