Java未経験エンジニアなので、Spring Bootとやらに触れてみる
Spring BootをMaster Boot RecordとかGRUBの類だと思っていたよ〜んです。
Javaを書いたことはありませんが、まぁPHPみたいなクラス型の言語でしょ、という軽いノリで進めていきます。
よく聞くDIやレイヤードアーキテクチャがコード上で何をしているのかも読んでいきます。
環境構築#
開発環境にはDocker Composeを使います。
やはり手こずるのが環境構築。初めての言語、初めてのフレームワークぐらい手でやった方がいいです。(辛面白いので)
とか思っていましたが、便利そうなWebツールがありました。
Spring Initializrです。
今回の設定 Spring Initializr
ここら辺の開発者体験はいいですね。
pom.xml is 何者なのか#
pom.xmlがパッケージ管理をしているっぽいですね。
可愛いファイル名とか思っていましたが、POMは Project Object Model の略でした、なかなかイカついすね。
JavaScriptでいうpackage.jsonに近い立ち位置ですが、依存関係だけでなくビルド方法なども含めてMavenがプロジェクトを管理するファイルということらしいです。
まっさらな状態から始める場合は、先にSpring Initializrでpom.xml、mvnw、src/などを生成し、その後にDocker環境を作るのが分かりやすそうです。
起動してみる#
appディレクトリで起動します。
docker compose up
最初にアクセスしたときは、Whitelabel Error Pageが出ました。

これはアプリケーションが落ちているのではなく、/に対応するControllerもHTMLもまだないための404でした。
Actuatorのヘルスチェックを見ると、Spring Boot自体は起動しています。

Todoアプリの仕様#
Webアプリケーション界のHello World、超シンプルなTodoアプリケーションを作ります。
Todoの登録、取得、更新、削除と、未完了・完了の切り替えができれば一旦完成とします。
APIはこの形です。
| Method | Path | 用途 |
|---|---|---|
POST | /api/todos | Todoを登録する |
GET | /api/todos | Todoを一覧表示する |
GET | /api/todos/{id} | Todoを1件取得する |
PUT | /api/todos/{id} | タイトルなどを更新する |
PATCH | /api/todos/{id}/status | 完了・未完了を切り替える |
DELETE | /api/todos/{id} | Todoを削除する |
レイヤードアーキテクチャで分ける#
アーキテクチャは、あるあるのレイヤードアーキテクチャで組みます。(まぁClean Architectureが好みなのですが)
Todoを登録するときは、ざっくり次の順番で処理されます。
sequenceDiagram
autonumber
actor Client as ブラウザ / Postman
participant MVC as Spring MVC / Controller
participant Service as TodoService
participant Entity as Todo
participant Repository as TodoRepository
participant DB as PostgreSQL
Client->>MVC: POST /api/todos(JSON)
Note over MVC: Request DTOへ変換してValidation
MVC->>Service: create(...)
Service->>Entity: Todo.create(...)
Entity-->>Service: Todo
Service->>Repository: saveAndFlush(todo)
Repository->>DB: INSERT
DB-->>Repository: 保存結果
Repository-->>Service: Todo
Service-->>MVC: Todo
Note over MVC: Response DTOへ変換
MVC-->>Client: 201 Created(JSON)各レイヤーに責務を分けることで、ControllerへSQLを書いたり、EntityへHTTPレスポンスの都合を書いたりせずに済みます。
プロジェクトの主な構成は次のようになりました。
src/
├── main/
│ ├── java/com/example/todo/
│ │ ├── TodoApplication.java
│ │ ├── config/ApplicationConfig.java
│ │ ├── controller/
│ │ │ ├── TodoController.java
│ │ │ ├── ApiExceptionHandler.java
│ │ │ └── dto/
│ │ ├── entity/
│ │ ├── repository/
│ │ └── service/
│ └── resources/
│ ├── application.properties
│ ├── db/migration/V1__create_todos.sql
│ └── static/index.html
└── test/java/com/example/todo/
TodoApplication.javaをcom.example.todoに置き、それ以下へControllerやServiceを置いています。@SpringBootApplicationが付いたクラスのパッケージが、SpringがコンポーネントやEntityを探す基準になるようです。
参考 Structuring Your Code :: Spring Boot
実装を読んでいく#
気になったところだけ記事に載せていきます。
ControllerとDTO#
ブラウザから送ったJSONはTodoControllerが受け取り、CreateTodoRequestへ変換します。
@NotBlank(message = "title is required")
@Size(max = 200, message = "title must be 200 characters or fewer")
String title
Javaのrecordは、値を運ぶクラスを短く書くための機能らしいです。ここではAPIから受け取る項目とValidationをまとめています。
JSONをEntityへ直接変換せずDTOで受けるため、利用者からcreatedAtなどを勝手に指定されることもありません。
入力チェックはDTOとEntityとDBにありますが、それぞれ守っている境界が違います。
- DTOは外部入力を検証し、400レスポンスへつなげる
- EntityはHTTP以外から呼ばれても不正なTodoを作らせない
- DBはアプリケーションを経由しない書き込みも拒否する
ここは私が知ってるなんちゃってクリーンアーキテクチャと違うので、なんかモヤる。
参考 Request Body :: Spring Framework
Serviceとトランザクション#
Serviceは「Todoを作って保存する」というユースケースをまとめています。HTTPのステータスやJSONは知らず、Entityの作成とRepositoryへの保存を調整する役です。
@Transactionalを付けると、Springのプロキシがメソッド呼び出しを包みます。正常ならコミット、例外ならロールバックする処理が、Serviceのコードとは別に差し込まれる感じです。
アノテーションが魔法でDBを操作していると思っていましたが、間にいるプロキシが頑張っていました。
参考 Understanding the Spring Framework’s Declarative Transaction Implementation
Dependency Injection#
Springでよく聞くDIはDependency Injection、依存性注入です。
TodoServiceはTodoを保存するためにTodoRepositoryを、現在時刻を得るためにClockを必要とします。しかし、Service自身はそれらをnewしていません。
public TodoService(TodoRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
必要なものをコンストラクターの外から渡してもらっています。これが今回のDIです。
flowchart LR
subgraph Production["本番環境"]
direction TB
Spring["Spring Container"]
Repository["Spring Data JPA Proxy<br/>TodoRepository"]
SystemClock["Clock.systemUTC()"]
ProductionService["TodoService"]
Spring -->|"実装を生成・管理"| Repository
Spring -->|"インスタンスを生成"| ProductionService
Repository -->|"コンストラクターへ注入"| ProductionService
SystemClock -->|"コンストラクターへ注入"| ProductionService
end
subgraph Test["テスト環境"]
direction TB
TodoServiceTest["TodoServiceTest"]
MockRepository["Mock TodoRepository"]
FixedClock["Clock.fixed(...)"]
TestService["TodoService"]
TodoServiceTest -->|"new"| TestService
MockRepository -->|"コンストラクターへ注入"| TestService
FixedClock -->|"コンストラクターへ注入"| TestService
end本番ではSpringがRepositoryとClockを渡します。テストではMockのRepositoryと固定したClockを渡します。TodoService自体は変わりません。
ここら辺はLaravelのテストコードを書くときにも使いますね。依存先をMockへ差し替えたり、現在時刻を固定したりする考え方は、SpringでもLaravelでも同じっぽいです。
もしServiceの中でInstant.now()を直接呼んでいたら、同じ時刻を期待するテストが難しくなります。DIはSpringらしい書き方というだけでなく、依存先を明示して交換できるようにする設計でした。
参考 Dependencies and Configuration in Detail :: Spring Framework
Entityとステート#
TodoはJPAのEntityであると同時に、Todoの状態と振る舞いを持つクラスです。
Setterを何でも公開せず、完了・未完了の切り替えはchangeStatusへまとめました。
public void changeStatus(TodoStatus newStatus, Instant now) {
Objects.requireNonNull(newStatus);
Objects.requireNonNull(now);
if (status == newStatus) {
return;
}
status = newStatus;
completedAt = newStatus == TodoStatus.COMPLETED ? now : null;
updatedAt = now;
}
ControllerやServiceがstatusとcompletedAtを別々に書き換える設計だと、「状態は完了なのに完了日時がない」というTodoが生まれかねません。
状態と、それを変えるルールをEntityの中へまとめる理由がここで理解できました。
@Versionも付けて楽観ロックを使っています。ただし、現在のAPIはクライアントからversionを受け取っていません。古い画面からの更新まで厳密に拒否するなら、versionをリクエストへ含めるか、HTTPのETagとIf-Matchを使う必要がありそうです。
参考 Version (Jakarta EE Platform API)
Repository#
Repositoryはめっちゃ薄いす。
List<Todo> findAllByOrderByCreatedAtDesc();
List<Todo> findByStatusOrderByCreatedAtDesc(TodoStatus status);
JpaRepository<Todo, UUID>を継承すると、基本的な保存・検索・削除はSpring Data JPAが用意します。
さらに、findByStatusOrderByCreatedAtDescというメソッド名から「statusで絞り込み、createdAtの降順で並べる」クエリまで作ってくれます。
メソッド名がSQLみたいになっていて面白いですが、条件が複雑になるとつらそうです。その場合は@Queryなどへ切り替えるらしいです。
参考 JPA Query Methods :: Spring Data JPA
FlywayとHibernateの役割#
DBのテーブルはV1__create_todos.sqlをFlywayが実行して作ります。
spring.jpa.hibernate.ddl-auto=validate
spring.flyway.enabled=true
FlywayがDB変更を担当し、HibernateはEntityとテーブルの対応が破綻していないか検証する役にしました。両方にテーブルを作らせない構成です。
SQLにはCHECK制約も入れています。Entityと同じルールに見えますが、DBを直接操作されても不正な状態を保存させないためです。
参考 Database Initialization :: Spring Boot
動かしてみる#

アクセスすると、CSSなしのTodo画面が表示されます。

Todoの登録、一覧、状態変更、削除まで動きました。
Spring BootがHTMLを配信し、そのJavaScriptから自分のAPIを呼ぶところまで一旦完成です。
Java未経験エンジニアによる気になったポイント#
src/mainってフォルダをなぜ切るの?#
イケてないルールではなく、Mavenの標準でした。
src/main/javaは本番用のJavaコードsrc/main/resourcesは設定、SQL、HTMLなどのリソースsrc/test/javaはテストコードtargetはコンパイル結果や作成したjar
参考 Introduction to the Standard Directory Layout – Maven
DTOの置き場所どこにすればええねん#
今回のDTOはHTTPリクエストとレスポンス専用なので、controller/dtoへ置いてみました。
主流はdtoを単体でディレクトリを切ることでした。
importがまじで多い#
多いです。Laravelとかでもまぁこうなるが、それにしても多いw
Javaではクラスがどのパッケージにあるのかをimportで明示します。
エディタ側で整理を任せるので体験が悪いとかそういうのはないですが、多いなぁという感想です、
まとめ#
おそらく会社ごとに記法や設計規則がありそうなので、Java経験・未経験という括り自体、あんまり意味がなさそうな気がしました。
まぁ、でも触ってよかったなという感じです。
次はこれをPostmanからテストしてみます。