エキサイト株式会社メディアの開発担当の佐々木です。
弊社では、現在CMSの再開発をSpringBoot使って構築しています。現時点では人数とかも少数な為、モノリスな構成になっていますが、将来的にはマイクロサービスにはする予定で、最近少し聞くようになってきたモジュラモノリスの構成を意識して、プロジェクトを作成しています。Gradleのマルチプロジェクトを使用してモジュールごとにプロジェクトを分けて開発を行っています。下記にて概要を解説します。
一般的なSpringBootの構成
一般的なSpringBootのディレクトリ構成は下記のようになります。
├── gradle
│ └── wrapper
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── controller
│ │ ├── persistence
│ │ ├── repository
│ │ └── service
メリット
- 構成がシンプル
- 見通しがいい
デメリット
- パッケージごとにアクセスしてほしいものを管理できない(controllerからpersitenceとかが参照できてしまう)
- 機能分割がかなり大きい単位になってしまう
controller
,repository
,service
などが、1つのプロジェクトの配下に存在し、とてもシンプルな構成です。とても小さいアプリケーションであれば、何も問題がありませんが、肥大化していくと、機能ごとにわけるのは難しくなってきます。
Gradleのマルチプロジェクトを使ったパッケージごとの構成変更
Gradleは標準でマルチプロジェクト構成をカバーします。上記のSpringBootアプリケーションをパッケージごとにマルチプロジェクト構成するとこのようになります。
├── HELP.md
├── build.gradle
├── persistence
├── repository
├── service
└── web
persistence
,repository
,service
,web
がそれぞれ別プロジェクトになりました。ある程度独立して管理できるようになります。依存関係もGradleを定義すると下記のようになります。
rootProject.name = 'demo'
include 'web'
include 'core'
include 'service'
include 'repository'
include 'persistence'
plugins {
id 'org.springframework.boot' version '2.3.7.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
allprojects {
repositories {
mavenCentral()
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
}
subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
bootJar { // 基本的にはSpringBootアプリケーションとしては起動しないようにする
enabled = false
}
jar {
enabled = true
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// 共通の依存関係を定義する
}
}
project(":persistence") {
}
project(":repository") {
dependencies {
implementation project(":persistence")
}
}
project(":service") {
dependencies {
implementation project(":persistence")
implementation project(":repository")
}
}
project(":web") {
bootRun {
sourceResources sourceSets.main
}
bootJar { // SpringBootアプリケーションとして起動できるかを定義
enabled = true
}
dependencies {
implementation project(":service")
implementation project(":repository")
}
}
test {
useJUnitPlatform()
}
メリット
- プロジェクト同士の依存関係を定義できる
- プロジェクトの独立性が高まる
デメリット
- フラットなときより見通しは悪い
- 考慮するべきことが少し多くなる
Javaの世界では、ただのパッケージでしたが1つのプロジェクトにすることにより独立性が高まります。上記のプロジェクトに外部用API
を提供したいとなったときに、下記のように書き直すとservice
、repository
はそのままにAPIのエンドポイントを増やせます。
include 'api' // 追加
//
// 省略
//
project(":api") {
bootRun {
sourceResources sourceSets.main
}
bootJar { // SpringBootアプリケーションとして起動できるかを定義
enabled = true
}
dependencies {
implementation project(":service")
implementation project(":repository")
}
}
//
// 省略
//
これを追加することで、外部用API
のプロジェクトが作成できます。service層
とrepository層
は、いままで使用していたものをそのまま使えます。model等を共有できるようになります。
Gradleのマルチプロジェクトを使ったモジュラモノリス構成
上記の構成だと、1つのアプリケーションの中でモジュールの共通化を依存関係を定義することによって、実装上は注意しなくてもある程度疎結合なアプリケーションを構築できるようになります。しかし、マイクロサービスとして分離できるかというと、service
の中が癒着していたり、切り出すときに悩む部分がまだ多いと思います。
Gradleは、マルチプロジェクトを階層化できますので、それを使ってより、こまかくプロジェクトを切っていきます。
├── core
├── mediaA
│ ├── persistence
│ ├── repository
│ ├── service
│ └── web
├── mediaB
│ ├── persistence
│ ├── repository
│ ├── service
│ └── web
rootProject.name = 'demo'
include 'core' // 主要なビジネスロジックが集まっているところ
include 'mediaA:service'
include 'mediaA:repository'
include 'mediaA:persistence'
include 'mediaB:service'
include 'mediaB:repository'
include 'mediaB:persistence'
// 略
project(":mediaA:persistence") {
}
project(":mediaA:repository") {
dependencies {
implementation project(":mediaA:persistence")
}
}
project(":mediaA:service") {
dependencies {
implementation project(":account:service")
implementation project(":mediaA:persistence")
implementation project(":mediaA:repository")
}
}
project(":mediaA:web") {
bootRun {
sourceResources sourceSets.main
}
bootJar {
enabled = true
}
dependencies {
implementation project(":mediaA:service")
implementation project(":mediaA:repository")
}
}
project(":mediaB:repository") {
dependencies {
implementation project(":mediaB:persistence")
}
}
project(":mediaB:service") {
dependencies {
implementation project(":mediaB:persistence")
implementation project(":mediaB:repository")
}
}
project(":mediaB:web") {
bootRun {
sourceResources sourceSets.main
}
bootJar {
enabled = true
}
dependencies {
implementation project(":mediaB:service")
implementation project(":mediaB:repository")
}
}
// 略
メリット
- 複数のサービスがモノレポで存在してもサービスレベルで依存しない設定が可能
- 汎用的なモジュールのみを依存定義ができるので、開発効率はあがる
デメリット
- 1つのリポジトリが肥大化する
- 見通しは少しずつ悪くはなっていく
- 大きくなっていくとビルドが少しずつ遅くなる(フルビルドで20秒くらい)
こんな形になります。mediaAもmediaBも同じような構成で似たようなサービスになります。しかし、DB等は分けたいし、でもcore
プロジェクトに主要なビジネスロジックは集まっているので、同期したいとなったとき等にこの構成が使えます。
mediaA
とmediaB
のお互いのプロジェクトを干渉しあわずに、しかしビジネスロジックは共有できるというのは、小さいプロジェクトが多いと割と嬉しいことが多いです。これが成長していったときには、mediaA
とmediaB
とは干渉し合ってないので、core
の部分だけ気にすれば、1つのサービスとして切り出せます。それであればマイクロサービスにすればいいじゃんという意見もありますが、アクティブなプロジェクトを少人数で運営している場合、リポジトリが分かれていたり、環境が少しずつ異なるのはかなりのストレスになります。
最後に
簡単ですが、Gradleのマルチプロジェクトを使用してモジュラモノリスの実現方法の紹介をさせていただきました。マルチプロジェクトをデフォルトでサポートしてるのはMavenやGradle以外にあまり見当たらなかったです。マイクロサービス人気がかなり加熱していますが、開発の体力的にも結構大変かと思いますので、一旦モノリスで作り、成熟したときに切り出しやすいようにしておくのがいいと判断しています。しかし、単なるモノリスだと規約だけでは、紐解くのが大変になるので、プロジェクトごとわけられるSpringBoot + Gradleはいい選択肢だとおもっています。
エキサイト株式会社では、自社サービス開発ができるまたはやりたいエンジニアを広く募集しております。
連絡は下記からお願いいたします!