Kotlin + Spring Boot + Doma2 の環境を構築する

私は「現職で利用されているから」という理由で Java の学習を始めた。しかし、現職のメインの開発言語は Java から Kotlin へ移行している。学習開始頃の私は、メイン言語である Kotlin の学習始めたが、Java 周辺技術やオブジェクト指向プログラミングの知識などが乏しく、とりあえず Java を触ろうと考えていたのだった。それを最近思い出した。

前回、書籍「ユースケース駆動開発実践ガイド」に登場する ICONIX プロセスを用いて、すごく簡単に JavaREST API の設計をしてみた。Java で作る前提で設計していたが、当初の目的に立ち返って Kotlin で作ることにした。

kazuhe.hatenablog.com

余談

記事タイトルから逸れるが、Java と Spring Boot を利用してプロトタイプを作った際に読んだ以下の書籍について、雑なメモが残っていたので思い出すために整理する。

www.shoeisha.co.jp

この書籍は、Spring Framework をはじめ、Spring Web や Spring Security など、Spring で扱う主なプロジェクトの紹介から始まる。その後、DI の必要性など、最低限必要な前提知識の説明があり、ハンズオン形式で Spring Boot を用いた Web アプリケーションを作成していく。

Spring 周辺のエコシステムは、長い歴史からあまりに大きいため、どのプロジェクトがどの様に関連しているのか、何をどの様に利用すればよいのか初学者には把握が難しいと思うが、この書籍を読むことで Starter の存在や、関連や仕組みなどを何となく把握できる。

一方で、Bean という概念についての説明はされていないが、「Spring Boot における Bean 定義について理解することは非常に重要だ」と書かれていて、そこから先を読むために、Bean って何やねんを別途理解する必要があった。Spring Boot が裏でいい感じにしてくれていると思いながら付与していた @Service アノテーション@Controller などは、クラスを Bean として登録するための @Component というアノテーションを利用している。つまり、@Service アノテーションを付与すると、その中で @Component を利用して Bean に登録され、Spring Boot アプリケーションの中で再利用できる様に ApplicationContext という箱に詰められ、私たち開発者は意識することなく利用できていたらしい(大まかにしか理解していない)。Spring Boot は便利すぎるため、知ろうとしないと中で何が起きているのか分かりにくいので、必要に応じて調べようと思う。

Kotlin 入門 + Doma2 の設定

Javaオブジェクト指向も Spring もよく分からない頃、とりあえず現職のバックエンド技術に合わせて読んだ書籍「Kotlin サーバーサイドプログラミング実践開発」を再度読んでみた。

gihyo.jp

当時は読んで書いてその通り動かすことはできたがわりと難しく感じた。Java や Spring を少し学習して改めて読むと以前と比較して難しさは感じていない。

とりあえず Kotlin の基本的な機能などの説明を読んで、試しに書いて動かして、本格的にアプリケーションを作成する 6 章の冒頭まで読んだ。このハンズオンでは O/R マッパーに MyBatis を利用しているのだが、私は Doma2 を利用したかったので、紹介されている構成と変えながら進めている。

Doma2 の公式ドキュメントには、サンプルプロジェクトのリンクがあるだけで、詳しいインストール方法は書かれていない。少し調べながら環境を作ったのでメモ的に簡単にまとめる。Node.js ベースの環境構築は慣れているが、Java や Kotlin の環境構築は分からないことが多い。

doma.readthedocs.io

私は今回 Kotlin, Spring Boot, Gradle の構成なので、README.md を見ながら build.gradle.kts に以下の依存を追加した。

dependencies {
    implementation("org.seasar.doma:doma-kotlin:2.53.3")
    kapt("org.seasar.doma:doma-processor:2.53.3")
}

ここで唐突に kapt が登場するが、plugins に kapt の宣言をする必要があるらしい。

plugins {
    kotlin("kapt") version "1.8.21"
}

よく見ると README.md の下の方の Related projectsdoma-spring-boot のリンクがあり、これの README.md を見て以下を追加した。

dependencies {
    ...
    implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.7.0")
}

ここまでの設定で利用できそうに思うが、正しく動作しなかった。私は DAO Style かつ SQL テンプレート?を利用する前提で、ChatGPT 先生や、Google 先生に質問すると、以下の様な記述が必要だと教えてくれた。

val compileKotlin: KotlinCompile by tasks
kapt {
    arguments {
        arg("doma.resources.dir", compileKotlin.destinationDirectory)
    }
}
tasks.register("copyDomaResources", Sync::class) {
    from(sourceSets.main.get().resources.srcDirs)
    into(compileKotlin.destinationDirectory)
    include("doma.compile.config")
    include("META-INF/**/*.sql")
    include("META-INF/**/*.script")
}
tasks.withType<KotlinCompile> {
    dependsOn(tasks.getByName("copyDomaResources"))
    ...
}

公式ドキュメントに同様の説明が見当たらなかったので、疑いながら調べていると doma-compile-plugin という素晴らしいプラグインを発見した。これは、上記の様なコンパイルの設定をしてくれるプラグインらしい。よく見るとこれも Related projects にリンクがある。ここまで調べるのに時間がかかった。これは気付けないよと思いながらプラグインとして追加した。

plugins {
    ...
    id("org.domaframework.doma.compile") version "2.0.0"
}

最終的な build.gradle.kts は以下の様になった。

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.7.11"
    id("io.spring.dependency-management") version "1.0.15.RELEASE"
    id("com.diffplug.spotless") version "6.18.0"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"

    // Doma のために追加した
    kotlin("kapt") version "1.8.21"
    id("org.domaframework.doma.compile") version "2.0.0"
}

group = "x"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")

    // Doma のために追加した
    implementation("org.seasar.doma:doma-kotlin:2.53.3")
    kapt("org.seasar.doma:doma-processor:2.53.3")
    implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.7.0")

    implementation("mysql:mysql-connector-java:8.0.32")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

spotless {
    kotlin {
        ktlint()
    }
}

諸事情により Java11 を利用しているが、17 やそれ以降も使えるらしい。

Spring Boot を BootRun すると、Docker を利用して立ち上げた MySQL から値を取得することができた。Doma2 を利用するために、複数のプラグインや依存が必要なのが難しく感じた。

これから

書籍「Kotlin サーバーサイドプログラミング実践開発」の 6 章以降を参考に、前回作った設計の実装を進めていく。

そういえば、JavaVSCode で書けたけが、さすがに Kotlin を書くには限界を感じたので IntelliJ IDEA に移行した。

ICONIX プロセスによる Java 製 REST API の設計

先日 Java と Spring Boot を使って REST API のプロトタイプを作った。さらに、書籍「ユースケース駆動開発実践ガイド」で登場する ICONIX プロセスを参考にユースケース記述の工程までを試してみた。

kazuhe.hatenablog.com

ICONIX プロセスを用いた設計も Java も Spring Boot も学習中なので、いろいろ雑だと思うが学びの記録として続きの工程を簡単にまとめてみる。ICONIX プロセスは工程を進めながらイテレーティブに各図を洗練されていく様なので、上記記事でのアウトプットも一部変更して再度この記事に貼り付ける。

紙芝居

作ろうとしているアプリケーションの画面構成を簡単に表現している。

機能要求

アプリケーションができることは何かを定義する。

  • ユーザーは node を登録できなければならない
    • node には名前を登録できなければならない
    • root 以外の node は 1 つの parent node を登録しなければならない
    • node は child node を任意個数登録できなければならない
    • node と node は edge で結ばれていなければならない
  • ユーザーは node の内容を編集できなければならない
  • ユーザーは node を削除できなければならない
  • ユーザーは tree を閲覧できなければならない

用語集

機能要求やユースケースで登場する用語の説明をする。

用語 説明
node ・システムに登録することができる最小単位の要素
edge ・node と node を結び付ける要素
tree ・node と edge で構成される node の集合体
root ・最上流に位置する node
parent node ・ある node と edge で結ばれ上流側の node
・各 node にとって parent node は単一となる
child node ・ある node とedge で結ばれた下流側の node
・各 node には任意個数の child node を持つことができる

ドメインモデル

用語集に登場する概念らの関係を整理・視覚化する。

ユースケース

このアプリケーションのユーザーが、このアプリケーションを使用して何ができるのかを表現している。

ユースケース記述

ユースケース図をより具体化するためにユーザーとアプリケーションの対話を記述する。本来ならユースケースの代替コース(例外)も記述するべきだが面倒なので省略する。

  1. node を登録する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Add child node」ボタンを押下する
    6. システムは child node を作成し、作成された child node の名前を入力するための input 要素にフォーカスする
    7. ユーザーは node の名前を入力しフォーカスアウトする
  2. node の名前を編集する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Edit」ボタンを押下する
    6. システムは node の名前を入力するための input 要素にフォーカスする
    7. ユーザーは node の名前を変更しフォーカスアウトする
  3. parent node を編集する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Change parent node」ボタンを押下する
    6. システム parent node にしたい node の選択(クリック)を促すメッセージを表示する
    7. ユーザーは任意の node をクリックする
    8. システムは選択された parent node と当該 node を結びつける
  4. node を削除する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Delete」ボタンを押下する
    6. システム node を削除し、child node がある場合は削除した node の parent node と child node 結びつける
  5. tree の参照
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する

アーキテクチャ

オニオンアーキテクチャの思想をベースにしている。

レイヤ 役割
Domain Entity ビジネスの概念やルールを表現したドメインオブジェクト
Repository ドメインオブジェクトの取得や更新を表現したインターフェース
Service Service ユースケースを実現するアプリケーション固有のロジック
UI Controller リクエストを解釈しレスポンスを生成するオブジェクト
Infrastructure Repository いわゆる Data Access Object

ロバストネス図

ユースケースとオブジェクトを関連付ける。この記事の最後にも感想を書いたが、この工程が難しいと感じた。次の工程であるシーケンス図の作成にあまり役にたたなかった。

1.node を登録する

2.node の名前を編集する

3.parent node を編集する

4.node を削除する

5.tree の参照

クラス図

アーキテクチャをベースにクラスに対する責務を割り当てる。

シーケンス図

ユースケースの振る舞いを、オブジェクトがどの様に達成するのかを示す。すなわち、ユースケース毎にシーケンス図を書くのが望ましいが、面倒になってきた参照と登録さえ設計できていれば、他のユースケースも同じ様に実装できそうなので一部省略している。

5.tree の参照

1.node を登録する

データベース設計

約 2 年ほど前に購入して読んでいた書籍「達人に学ぶDB設計 徹底指南書 ~初級者で終わりたくないあなたへ」を読んでテーブルに格納する値の例と ER 図を考えてみた。

www.shoeisha.co.jp

このアプリケーションは木構造でデータを管理するのだが、この書籍を読んでリレーショナデータベースが木構造でデータを格納するのが苦手なことを知った。

tree

tree_id(主キー) tree_name
TREE1 動物
TREE2 Foo

node

node_id(主キー) node_name parent_node_id tree_id user_id
NODE-A1 動物 TREE1 USER1
NODE-A2 脊椎動物 NODE-A1 TREE1 USER1
NODE-A3 無脊椎動物 NODE-A1 TREE1 USER1
NODE-A4 哺乳類 NODE-A2 TREE1 USER1
NODE-B1 Bar TREE2 USER2

user

すぐに利用する予定はないが、システムとして当然必要なので作成しておく。

user_id(主キー) user_name
USER1 tarou
USER2 jirou

ER 図

感想

書籍「ユースケース駆動開発実践ガイド」には、先にシーケンス図から作成し、その後(その都度?)クラス図を整理する様に書かれていたが、私は先にクラス図を作成する方が好みだった。これは今までの経験に依存するのかもしれない。

ロバストネス分析が難しい。私が書いたロバストネス図は、アプリケーションのユースケースの細かい振る舞いは表現している。しかし、ここで作りたいのはバックエンドアプリケーションなので、「ボタンを押下する」などのコントローラーはシーケンス図を作成するためには不要だった。ロバストネス図からシーケンス図がかなり飛躍しているので、うまく活用できていないのだろう。正直ロバストネス分析は不要だと感じている。ICONIX プロセスに関する記事などを読むと、ロバストネス分析ICONIX の肝である様に書かれてあったが、個人的には、以下のアウトプットでも(もちろん開発対象によるとは思うが)充分だと感じた。

  1. 紙芝居の作成
  2. ドメインモデルの作成
  3. ユースケース図の作成
  4. ユースケース記述の作成
  5. アーキテクチャの検討
  6. クラス図の作成
  7. シーケンス図の作成
  8. 実装
  9. テスト

どんなに小規模なアプリケーションであっても、ドメインモデルからアーキテクチャの検討までは実施したい。考慮できていない仕様に気付くことができるので有用だと思う。

これから

本当は書籍「Web APIの設計」を読んで API の設計をしたいのだが、お仕事が忙しかったのと体調不良で、プロトタイプ作成から時間があき過ぎている。そろそろ Java と Spring Boot の知識が脳内から消えていきそうなので実装に入ろうと思っている。

www.shoeisha.co.jp

途中で変更するかもしれない。ChatGPT 先生に頼ることになりそうだ。

Java 製 REST API のプロトタイピング

先日 Java の開発環境と CI 環境を作ったので、次のステップとして Java と Spring Boot を使って REST API のプロトタイプを作ってみた。

kazuhe.hatenablog.com

また、書籍「ユースケース駆動開発実践ガイド」で説明されている ICONIX プロセスを参考にアプリケーションの機能要求やユースケースなどを洗い出した。プロトタイプを作りながらアーキテクチャも検討したので学びの記録として簡単にまとめてみる。

Java も Spring Boot も REST APISQL も学習中なので、突っ込みどころは多くあるだろうが実際のプロトタイプのリンクを貼っておく。

github.com

紙芝居

作ろうとしているアプリケーションの画面構成を簡単に表現した。

機能要求

システムができることは何かを定義する。

  • ユーザーは node を登録できなければならない
    • node には名前を登録できなければならない
    • root 以外の node は 1 つの parent node を登録しなければならない
    • node は child node を任意個数登録できなければならない
    • node と node は edge で結ばれていなければならない
  • ユーザーは node の内容を編集できなければならない
  • ユーザーは node を削除できなければならない
  • ユーザーは tree を閲覧できなければならない

用語集

機能要求やユースケースで登場する用語の説明をする。

用語 説明
node ・システムに登録することができる最小単位の要素
edge ・node と node を結び付ける要素
tree ・node と edge で構成される node の集合体
root ・最上流に位置する node
parent node ・ある node と edge で結ばれ上流側の node
・各 node にとって parent node は単一となる
child node ・ある node とedge で結ばれた下流側の node
・各 node には任意個数の child node を持つことができる

ドメインモデル

これは用語集に登場する概念らの関係を視覚化した図で、この図を作る作業はドメインモデリングと呼ばれている。

ユースケース

このアプリケーションのユーザーが、このアプリケーションを使用して何ができるのかを表現している。

ユースケース記述

ユースケース図をより具体化するためにユーザーとアプリケーションの対話を記述する。

本来ならユースケースの代替コース(例外)も記述するべきだが面倒なので省略する。

  1. node を登録する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Add child node」ボタンを押下する
    6. システムは child node を作成し、作成された child node の名前を入力するための input 要素にフォーカスする
    7. ユーザーは node の名前を入力しフォーカスアウトする
  2. node の名前を編集する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Edit」ボタンを押下する
    6. システムは node の名前を入力するための input 要素にフォーカスする
    7. ユーザーは node の名前を変更しフォーカスアウトする
  3. parent node を編集する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Change parent node」ボタンを押下する
    6. システム parent node にしたい node の選択(クリック)を促すメッセージを表示する
    7. ユーザーは任意の node をクリックする
    8. システムは選択された parent node と当該 node を結びつける
  4. node を削除する
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する
    3. ユーザーは node の右上の「・・・」ボタンを押下する
    4. システムはドロップダウンメニューを表示する
    5. ユーザーは「Delete」ボタンを押下する
    6. システム node を削除し、child node がある場合は削除した node の parent node と child node 結びつける
  5. tree の参照
    1. ユーザーはページへアクセスする
    2. システムは tree を表示する

アーキテクチャ

オニオンアーキテクチャの思想をベースにした。

Node.js のフレームワークである Express を利用して似た構成で簡単なアプリケーションを作ったことがあるが、それに比べると DI(dependency injection)が魔法のように簡単に実現できて感動した。

レイヤ 役割
Domain Entity ビジネスの概念やルールを表現したドメインオブジェクト
Repository ドメインオブジェクトの取得や更新を表現したインターフェース
Service Service ユースケースを実現するアプリケーション固有のロジック
UI Controller リクエストを解釈しレスポンスを生成するオブジェクト
Infrastructure Repository いわゆる Data Access Object

これから

この後どの様にアプリケーション開発を進めていくか予定を立てる。

書籍「達人に学ぶDB設計 徹底指南書 ~初級者で終わりたくないあなたへ」を読んで DB 設計をする。この書籍は約 2 年ほど前に購入して読んでいるが、あまり活用する機会がなく忘れているのでもう 1 度読む。

www.shoeisha.co.jp

次に書籍「Web APIの設計」を読んで API の設計をしてみる。

www.shoeisha.co.jp

その後、ICONIX プロセスの「ロバストネス分析」以降の工程を定型的になぞって開発を進めてみようと思う。

Gradle に Java ライブラリを追加すると cannot be resolved と怒られた

build.gradledependenciesSpring Boot Starter Validation を追加した。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

これを import jakarta.validation.constraints.NotBlank; してみると依存を解決できないと怒られた。

The import jakarta.validation cannot be resolvedJava(268435846)

Spring Boot Starter Validation を追加すると、jakarta.validation.constraints.XX が利用できるようだが、私の環境では上手く利用できないので調べてみる。

仮説

Spring Boot Starter Validation が Jakarta Bean Validation に依存しているから、私たち利用する側は明示的に追加する必要がないものだと勝手思っている。

であれば、追加したのに依存が解決されていない原因は大きく分けると以下の 2 つであると考えた。

  1. プロジェクトに正しくライブラリを追加できていない
  2. エディタがライブラリを認識できていない

検証

1 から順に確認する。

1. プロジェクトに正しくライブラリを追加できていない

依存をリフレッシュするコマンドを実行した。

./gradlew build --refresh-dependencies

この後、以下のコマンドで依存関係を表示してみた。

./gradlew dependencies 

すると、Spring Boot と同じバージョンの org.springframework.boot:spring-boot-starter-validation -> 3.0.2 がツリーの中に表示されいたので、おそらく正しくプロジェクトには追加されているのだろう。

また、仮説の通り Jakarta Bean Validation と思われるライブラリも表示されている。

compileClasspath - Compile classpath for source set 'main'.
| # ..省略
\--- org.springframework.boot:spring-boot-starter-validation -> 3.0.2
     +--- org.springframework.boot:spring-boot-starter:3.0.2 (*)
     +--- org.apache.tomcat.embed:tomcat-embed-el:10.1.5
     \--- org.hibernate.validator:hibernate-validator:8.0.0.Final
          +--- jakarta.validation:jakarta.validation-api:3.0.2
          +--- org.jboss.logging:jboss-logging:3.4.1.Final -> 3.5.0.Final
          \--- com.fasterxml:classmate:1.5.1

ここで追加したライブラリはどこに追加されているのか気になったので調べてみた。

Gradle の公式ドキュメントを参照すると、$USER_HOME/.gradlemodules-2 に依存ライブラリが格納されていると書かれている。

├── caches 
│   ├── 4.8 
│   ├── 4.9 
│   ├── ⋮
│   ├── jars-3 
│   └── modules-2 # <-

docs.gradle.org

私は Docker の中に開発環境を構築していたので、Docker Container に接続して確認してみると、確かにライブラリが追加されていた。

ls ~/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-validation/
# 3.0.2

プロジェクトにライブラリは追加されている様だ。

2. エディタがライブラリを認識できていない

VSCode拡張機能 Extension Pack for Java によって表示されている JAVA PROJECTS の Project and External Dependencies を見てみると、パスがローカル PC を指していることに気付いた。

ソースコードを Docker Container にバインドマウントした状態で、Docker Container に接続して Gradle のコマンドを実行していたので、あたかも Docker 内で開発している気分だったが、VSCode は Container には接続していないので当然 Conainer の中のライブラリは参照できなかったのだ。

今まではローカル PC に同じバージョンの Spring Boot 等のライブラリがたまたま存在していたから、VSCode 上でも補完が効いていたのだろうか。そして今回ローカル PC に存在しないライブラリを追加したタイミングで初めて問題になったのだろうか。

解決

原因が分かったので VSCode を Docker Container に接続するために、今まで存在は知っていたが必要ないだろうと思っていた VSCode Dev Containers を設定することにした。

code.visualstudio.com

docker-compose.ymlDockerfile をそのまま利用できてすぐに環境を作ることができた。

とりあえず最小限の設定を書いた。

// .devcontainer/devcontainer.json
{
    "name": "plantree-dev-container",
    "dockerComposeFile": [
        "../docker-compose.yml"
    ],
    "service": "workspace",
    "remoteUser": "devuser",
    "workspaceFolder": "/home/devuser/workspace",
    "customizations": {
        "vscode": {
            "extensions": [
                "vscjava.vscode-java-pack",
                "vscjava.vscode-gradle"
            ]
        }
    }
}

Dev Container を起動した状態で JAVA PROJECTS とやらを確認すると、パスが Docker Container の中のライブラリを指していた。また、ローカル PC には無かった Spring Boot Starter Validation を認識していた。

エディタ上で jakarta.validation.constraints.NotBlank を import することができた。さらに、Dev Container を利用することで、Gradle のタスクを VSCode から実行できるようになった。すごく便利だ。

かなり初歩的なことが原因だったが、Java や Gradle について分からないことが多いので問題の切り分けに時間がかかってしまった。

Java の開発環境と CI 環境を構築する

先日、2023 年の抱負として Rust を学習しながら Web アプリケーションを作ると宣言をした。この時はやるべきよりやりたいを優先しようと思っていた。

kazuhe.hatenablog.com

記事を書いた数日後に現職でバックエンド開発チームへ転向できる可能性があることを聞いた。マネジメント層に対して以前からバックエンド開発に関心があることは伝えており、日々の業務でもある程度評価していただいている様でチャンスがあるとのことだった。

そのため Rust 学習計画は中断し、チャンスに備えて現職で利用されている Java を用いた Web アプリケーションを作る計画に変更した。2023 年の抱負を書いて早々に興味ドリブンで学習をする方針を変更することになったが、業務で経験できるチャンスを逃さないためにできる準備をしておこうと思う。

昨年、書籍「プロになる Java」を読んで、 JVMJDK などを含む Java 自体の基礎知識と基本的な文法を学習し、手元の PC にインストールした H2 Database とやりとりをする簡単な ToDo アプリケーションを作成している。なので、次のステップとしてもう少しマシなアプリケーションを作りながら、自分が知らないことをその都度学習することをしばらくの目標にする。

Java の開発環境を作ってみる

開発環境に対する理解は必須だと思うので、まずは環境を作成して開発の準備から始めようと思う。

まず周辺ツールのトレンドが気になったので調べてみた。

www.jetbrains.com

これは JetBrains 社が Java 開発者に対して行った調査で、Java のどのバージョンを利用しているか、何のビルドシステムを利用しているか等の調査結果が記載されている。

ツールそれぞれ良し悪しがあるのだろうが、調べているとキリがなさそうだったので、とりあえず以下の構成にした。

ちなみにエディタは VSCode を利用しようと思っている。Java 開発には IntelliJ IDEA が最も多く利用されている様で、私も書籍「プロになる Java」を読むタイミングで利用を始めたが、昔から使い慣れている VS Code の方が書きやすかったのが理由だ。

以下の VS Code 拡張機能を入れたが、初学者の私にとってはインテリセンスなど IntelliJ IDEA と比較しても遜色ない様に感じている。Web で提供されている Spring Initializr を利用しなくても VS Code 上で簡単にプロジェクトを作成できて感動した。

VS Code拡張機能などについては WEB+DB PRESS Vol.124 を参考にした。

gihyo.jp

ローカルの開発環境は Ubuntu をベースに Docker イメージを作成した。ユーザ追加など諸々を行った後に SDKMAN! CLI をインストールして、SDKMAN! で Java と Gradle をインストールしている。

# docker/workspace/Dockerfile
FROM ubuntu:22.04

ARG USER_NAME=default
ARG USER_GROUP_NAME=workspace

ARG PKG="git vim curl unzip zip sudo"

SHELL ["/bin/bash", "-c"]

RUN apt update \
  && apt install -y ${PKG} \
  && groupadd ${USER_GROUP_NAME} \
  && useradd --shell /bin/bash -m ${USER_NAME} \
  && echo %${USER_GROUP_NAME} ALL=\(ALL\) NOPASSWD:ALL > /etc/sudoers.d/${USER_GROUP_NAME} \
  && chmod 0440 /etc/sudoers.d/${USER_GROUP_NAME}

ARG JAVA_VERSION=17.0.5-ms
ARG GRADLE_VERSION=7.5

RUN su ${USER_NAME} --command \
  'curl -s "https://get.sdkman.io" | bash \
  && source "${HOME}/.sdkman/bin/sdkman-init.sh" \
  && sdk install java "${JAVA_VERSION}" \
  && sdk install gradle "${GRADLE_VERSION}"'

Docker Compose はとりあえず最低限書いた。

# docker-compose.yml
version: "3.9"

services:
  workspace:
    container_name: plantree-workspace
    env_file: .env
    build:
      context: .
      dockerfile: docker/workspace/Dockerfile
      args:
        USER_NAME: ${USER_NAME}
        USER_GROUP_NAME: ${USER_GROUP_NAME}
    tty: true
    volumes:
      - type: bind
        source: .
        target: /home/${USER_NAME}/workspace
    working_dir: /home/${USER_NAME}/workspace
    ports:
      - 8080:8080

docker compose up からの docker compose execワークスペースへ接続して、ユーザーを切り替えた後 ./gradlew build でプロジェクトをビルドできる様にした。これがベストプラクティスなのかは分からないが、他の PC でも同じ環境でビルドや実行をできる様になった。

Java アプリケーションの CI 環境を作ってみる

継続的インテグレーション(以下 CI と呼ぶ)を実施するために書籍「[改訂第3版]Jenkins実践入門―ビルド・テスト・デプロイを自動化する技術」を読んだ。

gihyo.jp

冒頭は CI の概念やメリット・デメリットなどが解説されている。これはフロントエンドにもバックエンドにも共通する知識で、すでにある程度意識して業務で実践できていた部分なのでスラスラと読んだ。

Jenkins を使って Java アプリケーションの CI を実現するために、テストやインスペクションなどで広く利用されているツールも紹介されていた。

しかし、今回はもう少しお手軽に CI を実現したかったので、Jenkins は利用せず GitHub Actions を利用することにした。以下を参考にした。

zenn.dev

そもそも Java プロジェクトのビルドはどの様なタスクで構成されているのか簡潔に説明されており、Java におけるビルドの具体的な役割イメージすることができる良い書籍だった。 また、CI の旨味なども丁寧に説明されていたので、先の Jenkins 本を買わずに無料で読めるこちらだけ読んでも充分な気がする。

ほとんど書籍そのままだが、Workflow の実行タイミングや、uses: XX のバージョンなど少しだけ変えた。

# .github/workflows/build.yml
name: Build

on:
  pull_request:
    branches:
      - "**"
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v3
      - uses: gradle/wrapper-validation-action@v1
      - name: Set up Java 17
        uses: actions/setup-java@v2
        with:
          distribution: microsoft
          java-version: 17
          cache: gradle
      - run: ./gradlew build

実際に GitHub Actions の上でビルドが成功した。取り急ぎなので必要に応じてチューニングしていこうと思う。

github.com

以上でシンプルな開発環境と CI 環境を構築できたので、ついに開発を始めていこうと思う。Java 初学者が調べながら書いた文書なので不適切な説明などあるかもしれない。

見積りをお願いされた時のはなし

「とあるシステムのフロントエンド部分を ● 月 ● 日までにリリースしたいけど、できますか?」と相談された。 実際にどの様な観点でどの様に見積りを実施したかまとめる。

前提

  • 新しいプロジェクト
    • フロントエンドは 1 スプリントのみが終わったタイミング
  • 弊チームのスプリントは 2 週間としている
    • 2 週間毎にリリース or デプロイをしている
  • タスク管理には GitHub を利用している

ベロシティの計測を始めたばかりで有用な平均値を出せていない状態ではあるが、システム全体の計画に関わるため不確実性が高くても見積りをしてほしいとのことだった。

※ 以下の手順は文書用に加工しているので実際に行った見積りとは少々異なる。

手順

ざっくりとまとめると、機能を実現するために必要な作業を細分化し、それに対してストーリーポイントを付与する。

過去のベロシティを元にマイルストーンに当てはめていくと、何がいつまでに終わるか見積りができる。

大分類となる機能 issue を作成

機能(= ページ)が 9 つあったので、大分類となるそれぞれの issue を作成した。

.
├── 機能 A
├── 機能 B
└── ...etc

各機能の UI コンポーネントを定義

それぞの機能毎に、必要なコンポーネントを定義し、コンポーネント実装用の issue を作成する。 今回は既に作成していた紙芝居を元に定義を行った。

この時点で機能の優先順をステークホルダーに確認しており、優先度の高い機能から実施した。

機能毎に大幅に UI が変わらない場合は、後に着手する機能になるほどコンポーネントの定義数は減ってくるはず。

コンポーネント実装 issue にストーリーポイントを付与した。

弊プロジェクトでは、1 度目のスプリントで作成したシンプルなコンポーネント実装のタスクを 1 ポイントとした。これについては作業量・複雑さの共通認識がとれていた。と思っている。 ちなみに、ストリーポイントはフィボナッチ数で粗いポイントにしている。

.
├── 機能 A
│   ├── Button コンポーネント(1pt)
│   ├── List コンポーネント(1pt)
│   ├── SelectBox コンポーネント(1pt)
│   ├── CheckBox コンポーネント(1pt)
│   └── ...etc
├── 機能 B
│   ├── Pagenation コンポーネント(2pt)
│   └── Accordion コンポーネント(2pt)
└── ...etc

各機能のページ実装を考える

次に各機能のページを表示するために必要な実装 issue を作成した。このタスクの責任はページを表示することで、以下の様な TODO を持っている。 (実際に作業に着手する前に issue を細かく分割して対応する)

  • UI コンポーネントを組み合わせる
  • API を実行してデータをコンポーネントに注入する
  • 状態を持つ必要があればそれを管理するためのロジックを実装する
  • Cypress で自動テストをしているので、それの実装をする

例によって、各機能毎に必要な TODO を洗い出した上でストーリーポイントを付与した。

.
├── 機能 A
│   ├── Button コンポーネント(1pt)
│   ├── List コンポーネント(1pt)
│   ├── SelectBox コンポーネント(1pt)
│   ├── CheckBox コンポーネント(1pt)
│   ├── ページ実装(2pt)
│   └── ...etc
├── 機能 B
│   ├── Pagenation コンポーネント(2pt)
│   ├── Accordion コンポーネント(2pt)
│   └── ページ実装(3pt)
└── ...etc

機能を実現するために必要なロジックを想像する

UI コンポーネント実装とページ実装以外に必要なロジックの実装がないかを機能毎に洗い出した。

多くの機能は以下の様な TODO になった。

  • 詳細設計をする
    • ページやコンポーネントが期待する型を定義する
    • パッケージ間のインターフェースを決める
    • パッケージ間の依存関係を整理する
  • API を実行し、ページやコンポーネントが期待する型へコンバートする関数を作成する
  • 機能特有のバリデーションロジックを実装する

これもストーリーポイントを付与した。

.
├── 機能 A
│   ├── Button コンポーネント(1pt)
│   ├── List コンポーネント(1pt)
│   ├── SelectBox コンポーネント(1pt)
│   ├── CheckBox コンポーネント(1pt)
│   ├── ページ実装(2pt)
│   ├── ロジック実装(3pt)
│   └── ...etc
├── 機能 B
│   ├── Pagenation コンポーネント(2pt)
│   ├── Accordion コンポーネント(2pt)
│   ├── ページ実装(3pt)
│   └── ロジック実装(3pt)
└── ...etc

開発環境の改善タスクを考える

現時点である不満や、プロジェクトを効果的に進めるために改善したいと思っている作業を issue として起票した。

直接的な成果物はないが、プロジェクトを効果的に進めるために必要だと思うタスク。

.
├── 機能 A
│   ├── Button コンポーネント(1pt)
│   ├── List コンポーネント(1pt)
│   ├── SelectBox コンポーネント(1pt)
│   ├── CheckBox コンポーネント(1pt)
│   ├── ページ実装(2pt)
│   ├── ロジック実装(3pt)
│   └── ...etc
├── 機能 B
│   ├── Pagenation コンポーネント(2pt)
│   ├── Accordion コンポーネント(2pt)
│   ├── ページ実装(3pt)
│   └── ロジック実装(3pt)
├── パッケージ更新の自動化(1pt)
├── リリースフローの改善(2pt)
└── ...etc

マイルストーンに落とし込む

本来ならスプリントを複数回経験し、ある程度平均値が確認できたベロシティをもとに決めたいが... 以下の様にスプリント毎に対応範囲を決定した。

※ チームが 1 回のスプリントで約 5pt 分消化できると予想している(実際のベロシティとは異なる)

※ 「..etc」としているところは考慮していない

ここまでしておけば、

「とあるシステムのフロントエンド部分を ● 月 ● 日までにリリースしたいけど、できますか?」

の ● 月 ● 日がスプリント 3 回目のリリース日であったとしても、「機能 A は対応できるが、機能 B は間に合いそうにない」と回答できる(実際に近い対応を行なった)。

頑張って間に合わせます!ではなく、現状のリソースでできることを伝えて、ステークホルダーが対応範囲を縮小するか・期限をのばずか等の検討をするための材料を共有することができた。

今後

2 週間毎に見積りを見直して、適切に共有・相談していこうと思う。

参考