はじめに
本記事は、Codexを「プログラミング学習メンター」として活用する取り組みをまとめたシリーズの第7回です。
まだ記事をご覧になっていない方は、第1回:コード生成させないプロンプト設計からご覧ください。
前回は、Flywayで作成したDBテーブルに対応するEntityと、DBアクセスを担当するRepositoryを、自分で記述しながら学びました。
今回はその続きとして、画面の選択肢に使う固定マスタ情報の取得を題材に、DTO・Service・Controllerを作成しました。前回のEntity・Repositoryに処理をつなぎ、各層が何を担当するのかを確認していきます。
今回も、Codexに完成コードを丸投げするのではなく、各クラスの責務やJavaの構文を自分で調べて記述し、分からない点の説明とレビューを依頼しました。
なお、単体テストについては、テスト対象や期待結果の整理が十分にできていないため、次回以降に着手することとします。
ただし、本来は各機能の実装と並行してテストを作成することも必要なため、今回作成した固定マスタ取得処理についても、Repositoryからの取得結果やDTOへの変換処理などを対象に、単体テストを追加していく予定です。
今回作成したもの
今回作成した対象は、画面の選択肢などに使用する固定マスタです。
- プロジェクト内ロール
- タスクのステータス
- タスクの優先度
これらをまとめて返す処理を想定し、以下のファイルを作成しました。
| 分類 | ファイル | 目的 |
|---|---|---|
| DTO | ProjectRoleResponse.java | プロジェクトロールの返却項目を定義する |
| DTO | TaskStatusResponse.java | タスクステータスの返却項目を定義する |
| DTO | TaskPriorityResponse.java | タスク優先度の返却項目を定義する |
| DTO | MasterDataResponse.java | 3種類の固定マスタのリストをまとめる |
| Service | MasterDataService.java | Repositoryの呼び出しとDTO変換を記述する |
| Controller | MasterDataController.java | HTTPの受付先とServiceの呼び出しを定義する |
本プロジェクトはあくまで学習目的で実装を行っているため、各メソッドや処理には細かくコメントを付記し、後で見返した時に「この処理は何のために記述しているのか」を思い出しやすくしています。
また、今回は固定マスタのDTO・Service・Controllerに範囲を絞ります。UserService、共通例外処理、単体テストは今後扱う予定です。
EntityとDTOの違い
最初に確認したのは、Entityが存在する状態で、さらにDTOを作成する理由です。
EntityにはDBのカラムに対応する項目があります。しかし、DBに保存する情報と、外部へ返す情報が一致するとは限りません。
| 種類 | 役割 |
|---|---|
| Entity | DBのテーブルに対応するJavaのデータ構造 |
| DTO | APIなどの境界で受け渡す項目を定義するデータ構造 |
例えばUserのEntityには、パスワードハッシュなどの認証用情報が含まれます。これをユーザー情報のレスポンスへそのまま含めることは適切ではありません。
固定マスタについても、画面に必要な項目とDB内部の管理項目を分けて考えました。今回はID・コード・名称を基本とし、ステータスと優先度には表示順を含める方針です。作成日時・更新日時は固定マスタのレスポンスには含めません。
DTOを用意する意味は、同じ項目を別のファイルへ複製することではなく、受け渡す情報の範囲を明確にすることだと整理しました。
また、実装対象のDTOが設計書で個別に定義されていない箇所もありました。項目を推測して追加するのではなく、必要な内容を確認し、設計書と実装の対応を整理してから作業を進めました。
学習面では以下のキーワードで検索し、理解を深めました。
- Java DTO Entity 違い
- Spring Boot Response DTO
- Entityを直接返さない理由
recordを使ったDTOの作成
DTOは当初classで記述していました。その過程で、フィールドを宣言することと、外部からその値を取得できるようにすることは別であると確認しました。
そこで、Javaのrecordについて調べ、固定マスタDTOをrecordで記述する方針にしました。
recordは、データをまとめて扱うためのJavaの仕組みです。宣言した項目に対応するコンストラクタやアクセサ、equals、hashCode、toString などが自動的に用意されます。通常のGetterと異なり、アクセサは name() のような形式になる点も確認しました。
例えば、ProjectRoleResponse.javaは下記のように記述しました。
package com.example.devsupport.master.dto.response;
public record ProjectRoleResponse(
Long id,
String code,
String name) {
}
通常のクラスでフィールドやコンストラクタ、Getterなどを記述する場合と比べ、recordでは少ない記述量でDTOを定義できることが分かりました。
今回のDTOは値の受け渡しが中心であり、recordを使うことで項目定義を簡潔に表現できます。
一方、recordが参照するオブジェクトまで自動的に変更不能になるわけではありません。例えばListを保持する場合、そのListの内容を変更できるかどうかは別途考える必要があります。
今回の項目は次のように整理しました。
| DTO | 項目 |
|---|---|
| ProjectRoleResponse | id、code、name |
| TaskStatusResponse | id、code、name、sortOrder |
| TaskPriorityResponse | id、code、name、sortOrder |
| MasterDataResponse | projectRoles、taskStatuses、taskPriorities |
個別の固定マスタDTOと、それらをまとめるDTOを分けて作成しています。
Serviceの実装
ServiceとRepositoryの責務の整理
RepositoryはDBアクセスを担当します。Serviceでは、その検索結果をどのように扱い、呼び出し元へ返すかを組み立てます。
今回のMasterDataServiceでは、以下の処理を記述しました。
- 各Repositoryの検索メソッドを呼び出す。
- 取得したEntityを対応するResponse DTOへ変換する。
- 3種類のリストをMasterDataResponseへまとめる。
固定マスタの取得では、複雑な業務判断はまだありません。それでも、検索を担当するRepositoryと、変換・集約を担当するServiceを分けて考えることで、各層の役割を整理できました。
並び順についても、仕様を確認しました。
| 固定マスタ | 意図する並び順 |
|---|---|
| プロジェクトロール | IDの昇順 |
| タスクステータス | sortOrderの昇順 |
| タスク優先度 | sortOrderの昇順 |
Repositoryのメソッド名に並び順を指定する方針にしました。ただし、メソッドを定義しただけでは、DBから期待した順序で返ることを検証できていません。実行時の確認は今後行います。
コンストラクタ注入とprivate・finalの確認
Serviceの記述では、Repositoryを利用するための構造も調べました。
今回使用したのは、Repositoryをコンストラクタで受け取り、フィールドへ保持するコンストラクタ注入です。
| 要素 | 役割 |
|---|---|
| private | フィールドへアクセスできる範囲をクラス内に制限する |
| final | 一度設定したフィールドへ別の参照を再代入できないようにする |
| コンストラクタ | Serviceが必要とするRepositoryを受け取る |
| this | 現在のインスタンスを参照する |
finalは、Repositoryの内部状態をすべて変更不能にするという意味ではありません。フィールドへ保持した参照を再代入できないという意味です。
構文をそのまま覚えるだけではなく、「このServiceを利用するためには何が必要か」をコンストラクタで表している、と捉えるようにしました。
学習面では以下のキーワードで検索し、理解を深めました。
- Service自身がRepositoryを生成しない理由
- Springが依存するオブジェクトを渡す仕組み
- フィールドとコンストラクタ引数を区別する方法
Stream APIによるDTO変換
EntityのリストをDTOのリストへ変換する部分では、Stream APIを使用しました。
複数のメソッドが連続している記述を理解するため、各段階で何を扱っているかを確認しました。
| 記述 | 今回の処理での意味 |
|---|---|
| stream() | リストの要素を処理するためのStreamを得る |
| map(...) | 各Entityを対応するDTOへ変換する |
| toList() | 変換した要素をListへまとめる |
mapへ渡す変換処理は、Service内のメソッドとして記述しました。
このとき、this. と this:: の違いも確認しました。this. は現在のインスタンスのメンバーにアクセスするときに使い、this:: は現在のインスタンスのメソッドを参照するときに使います。
重要なのは、構文の短さよりも、変換前と変換後の型を把握することでした。
Repositoryから受け取るのはEntityのリストであり、Serviceから返すのはDTOを含むレスポンスです。この対応を意識すると、変換メソッドの引数と戻り値を考えやすくなりました。
また、stream・map・toListは以前Javaゴールドの資格勉強をしていた際に理解が追い付かず苦戦した記憶がありました。
例えば、上記各メソッドが何のために使用されるのかがイメージしづらかったのですが、今回はstream()で処理対象を取得し、map()で各要素を変換し、toList()で結果をまとめるという流れを一つずつ確認することで、処理の意味を実際のコードを踏まえて理解することができました。
読み取り専用トランザクション
MasterDataServiceはデータ取得を担当するため、読み取り専用トランザクションの指定(@Transactional(readOnly = true))を記述しました。
ここでは、アノテーションを付けること自体を目的にせず、対象処理がDBを更新するのか、参照だけを行うのかを確認しました。
読み取り専用の指定は、あらゆる更新を必ず禁止する仕組みと同じではありません。実行環境や利用する技術との関係も含めて理解する必要があります。
今回は初期MVPの範囲として、固定マスタの取得機能のみを実装します。
そのため、MasterDataServiceの取得処理には @Transactional(readOnly = true) を指定しました。
将来的には固定マスタの登録・編集・削除機能も検討していますが、その際は取得処理と更新処理の責務を分け、更新用のServiceメソッドでは適切なトランザクション設定を行う方針です。
Controllerの実装
APIとControllerの違い
Controllerの作成に進む前に、APIという言葉が具体的に何を指すのかを確認しました。
今回対象としているWeb APIでは、URL、HTTPメソッド、入力、返却内容、認証条件などを取り決めます。Controllerは、そのHTTPリクエストを受け付ける入口をJavaで実装するクラスです。
固定マスタ取得では、以下の仕様を想定しています。
| 項目 | 設計内容 |
|---|---|
| HTTPメソッド | GET |
| パス | /api/master-data |
| 入力 | リクエスト本文なし |
| 返却内容 | MasterDataResponse |
| 返却形式 | JSON |
| 最終的な認証条件 | 認証必須 |
これは設計・コード上の定義であり、実行結果を示す表ではありません。
また、master/ 配下にあるすべてのファイルが、そのままAPIとして公開されるわけではありません。今回のディレクトリにはDTO・Service・Controllerを配置し、前回作成したEntity・Repositoryを利用する構成です。
全ファイルや全フィールドを外部へ公開するのではなく、Controllerで定義した受付先から、必要な情報を返す構成です。
Serviceの呼び出し
MasterDataControllerには、受付先のパスとHTTPメソッドを定義し、MasterDataServiceを呼び出して結果を返す処理を記述しました。
Controller内でRepositoryを直接操作したり、DTO変換をすべて行ったりする構成にはしていません。
各層の関係は、次のように整理しました。
| 層 | 主な役割 |
|---|---|
| Controller | HTTPの入口を定義し、Serviceを呼び出す |
| Service | 処理を組み立て、EntityをDTOへ変換する |
| Repository | DBアクセスを担当する |
| Entity | DBのデータ構造に対応する |
| DTO | 受け渡す項目を定義する |
この構成で意図しているのは、Controllerから呼び出した処理がRepositoryを通じてデータを取得し、ServiceでDTOへ変換した結果を、HTTP応答として返すことです。
ただし、ファイルを作成しただけで、この一連の動作を確認できたことにはなりません。API呼び出しによる検証は、今後の作業として区別しています。
作成したファイルの配置
今回取り上げたファイルの配置は以下です。EntityとRepositoryは、前回作成したものを利用する想定です。
backend/src/main/java/com/example/devsupport/
└─ master/
├─ dto/
│ └─ response/
│ ├─ ProjectRoleResponse.java
│ ├─ TaskStatusResponse.java
│ ├─ TaskPriorityResponse.java
│ └─ MasterDataResponse.java
├─ service/
│ └─ MasterDataService.java
└─ controller/
└─ MasterDataController.java
ファイルを分ける目的は、数を増やすことではありません。各クラスの責務と、変更した場合の影響を整理するためです。
package宣言、配置場所、importの対応についても、レビューで確認しました。
Codexによるレビューと改善
今回も、理解が不十分な部分を質問し、自分で記述したファイルをレビューしてもらう方法で進めました。
主に確認した内容は以下です。
- DTOとEntityを分ける理由
- recordを使用する利点と制約
- Serviceが担当する処理
- コンストラクタ注入の考え方
- private final、this、メソッド参照の意味
- Streamによる変換前後の型
- APIとControllerの違い
- 設計書と実装の対応
Codexの説明を受けた後も、分からない箇所は用語や処理単位に分けて検索して理解を深めるようにしました。実装方針を理解してから記述し、レビュー指摘を受けて修正する流れです。
また、設計書に不足がある場合は、実装だけを進めずに関連する仕様を確認し、必要に応じて加筆修正しました。AIの提案であっても、既存設計との対応を確認する必要があると考えています。
今回の到達点
今回整理できた内容は以下です。
- Entityとは別にDTOを用意する理由
- 固定マスタDTOの項目構成
- recordによるDTOの記述方法
- ServiceにおけるRepository呼び出しとDTO変換
- コンストラクタ注入とJavaの基本構文
- ControllerとAPI仕様の関係
- クラスごとの責務とファイル配置
DTO・Service・Controllerの関係を、実際のファイルを作成しながら整理することができました。
次回以降の作業
今後は、UserServiceや共通例外処理についても学習内容を整理し、作成した処理の単体テストへ進めます。
今後は実装単位ごとにテスト対象を整理し、単体テスト、Springの起動・結合確認、API呼び出しを段階的に行う予定です。HTTP応答やJSONを確認する際には、認証設定やDB接続などの前提も含めて検証します。
実装したファイル、確認した条件、未検証の範囲を分けて記録し、作業状況を正確に把握できるようにしていきます。
おわりに
今回は、固定マスタ取得を題材に、DTO・Service・Controllerの役割を調べながら各ファイルを作成しました。
EntityとDTO、RepositoryとService、APIとControllerなど、関連していても同じ役割ではない要素を区別することが主な学習内容でした。
コードの記述方法だけでなく、入力と出力、責務、配置、設計書との対応を確認することで、各ファイルを分ける意図が少しずつ明確になりました。
次回以降はService層での業務処理や例外処理、テストにも少しずつ取り組みます。引き続きCodexには説明・調査支援・レビューを依頼し、自分で「なぜこの設計にしたのか」を説明できる範囲を増やしていきたいと思います。

