ServerSide Kotlinのアプリをマルチモジュール化した

## はじめに
ServerSide Kotlin(FWはSpring Boot)アプリのマルチモジュール対応について書きたいと思います。

:::note info
試した環境

Kotlin 1.7.22
Spring Boot 3.0
Gradle 7.5.1
IntelliJ IDEA Ultimate 2022.3
:::

## マルチモジュール化するモチベーション is 何？

現在の開発ではオニオンアーキテクチャを採用しているのですが、レイヤー間の不正なアクセスをモジュールの仕組みで防ぎたいというのがモチベーションになります。
以前担当していたプロダクトでは ArchUnit で検知する方法を取っていましたが仕組みで防ぐに越したことはありません。

```mermaid
graph TD;
    ui-->domain;
    application-->domain;
    infrastructure-->domain;
    ui-->application;
```

上記のようにモジュールを分けて依存関係の向きを一方向に定義することで、ui層でDBアクセスしたり、infrastrucure層のオブジェクトがapplication層に渡るといった意図しない実装をされる心配がなくなります。  
また、それぞれの層で必要なライブラリだけを依存関係に追加しますので、ライブラリのバージョンアップによる影響範囲が最低限かつ明確になります。

```mermaid
graph TD;
    ui-->WEBに関するライブラリ;
    infrastructure-->JDBCなどDB接続に必要なライブラリ;
    application-->ユースケースを実装するのに必要なライブラリ;
    domain-->ドメインロジックを実装するのに必要なライブラリ;
```

ビジネスロジックを実装するdomain層が沢山のライブラリに依存してしまうと辛いので、それを防げるのが良いです。

## Kotlin のマルチモジュールサポートについて

まず、[言語仕様](https://kotlinlang.org/spec/packages-and-imports.html)に以下のような記述があるように、
Kotlin には Java のような言語標準のモジュールシステムは備えられておらず、ビルドシステムや IDE の仕組みを使う必要があります。


> 簡単な例では、モジュールとは、あるプロジェクトで同時にコンパイルされたファイルの集合のことです。
>
> 1 回の Kotlin コンパイラ呼び出しでコンパイルされるファイル群
> Maven モジュール
> Gradle プロジェクト

私は IntelliJ IDEA と Gradle で開発しているので、それらを使用してマルチモジュール化を行いました。

## やったこと

### サブプロジェクトの作成

先ほど挙げたレイヤーごとにGradleのサブプロジェクトを作成していきます。
IntelliJ IDEA上でNew -> Moduleすれば、settings.gradle.ktsの設定や、フォルダを作ってbuild.gradle.ktsを置いてところは自動でやってくれるので楽です。

```kotlin:settings.gradle.kts
// 自動で挿入される
include("infrastructure")
include("domain")
include("application")
include("usecase")
```

完成するとこのようなフォルダ構成になります。

```
┗ application
  ┗ src/  
  ┗ build.gradle.kts  
┗ domain
  ┗ src/  
  ┗ build.gradle.kts  
┗ infrastructure
  ┗ src/  
  ┗ build.gradle.kts  
┗ ui
  ┗ src/  
  ┗ build.gradle.kts 
src/ 
build.gradle.kts
settings.gradle.kts
```

### モジュールの依存関係を定義

次に、それぞれのモジュールのbuild.gradle.ktsに依存関係を設定していきます。
下記はui層の例ですが、application層とdomain層だけ参照するようにしています。
外部ライブラリもui層で必要なものだけに絞って定義します。

```kotlin:/ui/build.gradle.kts
dependencies {
    implementation(project(":application"))
    implementation(project(":domain"))
    implementation("org.springframework.boot:spring-boot-starter-web")
}
```

## 困ったことや工夫したところ

### 各プロジェクトで共通の設定はまとめる

build.gradle.ktsがモジュールごとに作られますが、
保守性を考えると重複する設定は極力書きたくないので、ルートプロジェクトのbuild.gradle.ktsにまとめて設定を行いました。

```kotlin:build.gradle.kts
allprojects {
    // どのモジュールもmavencntralを見る
    repositories {
        mavenCentral()
    }
    // Kotlinのコンパイルオプションも同じで良い
    tasks.withType<KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "17"
        }
    }
    // 必須で必要なプラグイン
    apply(plugin = "kotlin")
    apply(plugin = "org.jetbrains.kotlin.plugin.spring")
    apply(plugin = "io.spring.dependency-management")
    // テストは全てのケースで書きますので設定しておく
    tasks.withType<Test> {
        useJUnitPlatform()
    }
    // どのモジュールでも必要なライブラリを設定しておく
    dependencies {
        testImplementation("io.kotest:kotest-runner-junit5-jvm:5.5.4")
    }
}
```

### サブプロジェクトでは Spring Bootプラグインはapplyしない

単純にSpring Bootプラグインを設定するとサブプロジェクトもSpring Bootアプリケーションと扱われて正しく動作しません。
そこでサブプロジェクトでは`apply false`でプラグインを適用すれば依存関係の解決だけにプラグインを使用することが出来ます。
エラーメッセージが分かりにくいので知らないとハマると思います。

```kotlin
plugins {
    id("org.springframework.boot") apply false
}

// BOMの指定も必要
dependencyManagement {
    imports {
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    }
}
```

### プラグインのバージョンをまとめて指定したい

各サブモジュールのbuild.gradle.ktsにバージョンを書いてしまうと、バージョンアップ時に大変なので一箇所で定義したいです。
最初はGradleのextプロパティで指定しようとしましたが、pluginsブロックには変数が設定できないようでダメでした。  
正解かどうか良くわかりませんが、settings.gradle.ktsに使用するプラグインをすべて定義して、サブプロジェクト側ではバージョンを指定しないようにしました。

```kotlin:settings.gradle.kts
pluginManagement {
    repositories {
        maven { url = uri("https://repo.spring.io/milestone") }
        gradlePluginPortal()
    }
    plugins {
        id("org.springframework.boot") version "3.0.0"
        id("io.spring.dependency-management") version "1.1.0"
        id("io.gitlab.arturbosch.detekt") version "1.22.0"
        id("nu.studer.jooq") version "8.0"
        id("org.flywaydb.flyway") version "9.10.1"
        kotlin("jvm") version "1.7.22"
        kotlin("plugin.spring") version "1.7.22"
    }
}
```

サブプロジェクト側ではこのようにバージョンを指定しません。

```kotlin:/infrastructure/build.gradle.kts
plugins {
    id("nu.studer.jooq")
    id("org.flywaydb.flyway")
}
```

### Jacoco のマルチモジュール対応

標準のjacocoプラグインはサブプロジェクトに対応していないのですが、`jacoco-report-aggregation`を使ってレポートの集約を行いました。

https://docs.gradle.org/current/userguide/jacoco_report_aggregation_plugin.html

```kotlin
plugins {
    id("jacoco-report-aggregation")
}
```

ちょっと手を入れたこととしては、`testCodeCoverageReport`タスクを動かせばJacocoのレポートが出力されるのですが、JOOQの自動生成コードは対象外にしたかったので下記のような設定を加えました。

```kotlin
// Jacocoのレポートからjooqの自動生成コードを除外する
tasks {
    testCodeCoverageReport {
        classDirectories.setFrom(files(
            subprojects.map {it.fileTree(mapOf("dir" to "${it.buildDir}/classes", "exclude" to arrayOf("**/jooq/**")))}
        ))
    }
}
```

また、GitHub ActionsでPull Request時に修正したコードのカバレッジを出すようにしていますが、標準のjacocoプラグインの設定からxmlのパスを書き換えるぐらいの対応で済みました。

```yaml:.github/workflows/build.yml
name: Java CI with Gradle
on:
  pull_request:
permissions:
  contents: read
  pull-requests: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'corretto'
    - run: ./gradlew ci --no-daemon
    - name: Add coverage to PR
      id: jacoco
      uses: madrapps/jacoco-report@v1.3
      with:
        paths: ${{ github.workspace }}/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml
        token: ${{ secrets.GITHUB_TOKEN }}

```
![スクリーンショット 2022-12-22 18.37.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387/87e8e705-51a4-6b59-0ee5-eda7df3e1440.png)

### イマイチなところ

#### application層からspring-txへの依存関係を指定する必要がある

`@Transactional`を使うために本来あまり依存したくないspring-txを読み込む必要がありました。
別のレイヤーからリフレクションで設定するなど回避方法はありますが、どうするのが一番キレイなのか考え中です。

```kotlin
@Service
class HogeService(val hogeRepository: HogeRepository) {
    @Transactional(readOnly = true)
    fun findById(id: Long): Hoge {
        return hogeRepository.findById(id)
    }
}
```


#### Spring Bootに依存したテストがルートプロジェクトでしか動かせない

テストコードもサブプロジェクトごとにきれいに配置したいのですが、`@SpringBootTest`を使用するようなテストがルートプロジェクトに置かないと動かない問題があります。
`@SpringBootApplication`を付与したmainクラスをルートプロジェクトに置いているので、それはそうなるよなという感じですが今のところ解決できていません。
（アドベントカレンダーまでに解決させたかったが間に合いませんでした...）

##### 追記
後日、テストを動かしたいサブプロジェクトのbuild.gradle.ktsにtestRuntimeOnlyでmainクラスが置かれているルートプロジェクトをテスト実行時に参照するようにしたところうまく行きました。

```kotlin
testRuntimeOnly(rootProject)
```

## まとめ
終わってみれば Kotlin 要素がほとんどなく、Gradle のサブプロジェクトの話が中心になってしまいました。
さらっと書いてますがGradle自体難しいですし、Springがエラーになったときは原因を掴むのは経験値がないとちょっと辛いかもしれないので、
Kotlin本体がモジュール対応してくれることをサンタさんにお願いしときました🎄