画面の次は Service ― 詳細設計 Excel から業務ロジックを組み立てる全体設計
この記事は生成AIとの対話をもとにまとめたものです。
前編: 設計書を「データ」にして画面を自動生成する ― Spring Boot + Thymeleaf で人が HTML を書かない開発
前編では、画面 HTML・Form・Controller を IR(中間表現)から 決定論的に 生成する話をしました。最後に ポート という窓口だけを残し、「業務ロジックは人+AIが書く」と書きました。前編が未読の方は、少なくとも「ポート」と「失敗の4分類」の節に目を通しておくと、本記事が読みやすくなります。
この記事はその続きです。Service 層をどう設計・製造するか の全体設計をまとめます。ここでも方針は同じです。
- 詳細設計 Excel の自然言語を、そのまま Java に翻訳しない
- 一度 IR に正規化し、決定論的に作れる部分と人が書く部分を分ける
- 生成された Service の骨格も、画面と同様に 直接編集しない
対象は、ウォーターフォールで 詳細設計書(Excel) が納品物として残る、Spring Boot + Thymeleaf の業務システムです。
前編からの位置づけ
前編の完成形は次のとおりでした。
specs/screens/(画面 IR)= 正本
│
├─ ScreenEmitter ─→ HTML / Form / Controller
└─ Port インターフェース ─→ 実装は人+AI
Phase 2(本記事)では、詳細設計 Excel のうち Controller 1メソッド = 1シート の処理定義を IR に落とし、Controller の中身・Service の骨格・Repository / Dao の配線までを生成します。
specs/screens/ ─ ScreenEmitter ─→ 画面・Controller 骨格・Port IF
specs/flows/ ─ FlowEmitter ─→ Controller メソッド本体・Service 骨格
specs/data/ ─ DataEmitter ─→ Entity / Migration / Repository
specs/domain/ ─ (将来) ─ ルール・ユースケース
詳細設計 Excel ──→ パーサ(移行時のみ・一度きり)──→ 上記 IR 群
└──→ DocEmitter ──→ 設計書(納品用)
矢印の向きは前編と同じです。Excel からコードを作るのではなく、IR から設計書とコードの両方を作ります。
用語ミニ辞典(Phase 2 追加分)
前編の用語に加えて、本記事で使う語です。
| 用語 | 意味 |
|---|---|
| フロー IR | 1つの handler(=Controller の1メソッド)に対応する処理ステップの定義。主に YAML |
| handler ID | 画面 IR の actions[].handler とフロー IR の handler を結ぶ共通キー。例: customerEdit.confirm
|
| step type | 詳細設計の1行を表す定型パターン。例: validateForm, call, findById
|
| manual step | 定型化できないと人が判断した業務処理の step。手実装クラスへの委譲として生成される |
| data IR | テーブル定義・項目・PK/FK など、データ構造の正本 |
| domain IR | 判断表・計算式・業務ルールなど(将来拡張) |
| Service 骨格 | メソッドシグネチャ・依存注入・トランザクション境界が入った生成クラス。業務判断は manual step へ委譲し、中身を直接書かない |
| 段階 A/B/C | 自動化の成熟度。A=定型ステップのみ、B=ルール IR 化、C=残りを人+AI |
なぜ「詳細設計をそのまま AI に読ませない」のか
画面のときと理由は同じです。
- 揺れ — 同じ Excel でも、毎回違う Service が出てくる
- 検査のしにくさ — 「この行はバリデーション?業務チェック?単なるメモ?」が機械的に追えない
- 責務の混在 — 1シートに Controller の話と Service の話と DB の話が混ざっている
だから、詳細設計の内容を 種類ごとに IR に分けます。
| 詳細設計の種類 | 例 | 行き先 IR | 自動化の見込み |
|---|---|---|---|
| データ定義 | テーブル、項目、PK/FK | specs/data/ |
ほぼ 100% |
| 処理定義 | フロー、分岐、トランザクション |
specs/flows/, specs/domain/
|
骨格+定型 |
| 説明・背景 | 備考、経緯 | docOnly |
設計書再生成のみ |
自然言語の備考をそのまま if 文にしない。 昇格できないものは docOnly とテスト骨子に残します。これは前編のポートの話の延長です。
接続の要:handler ID
画面と業務処理をつなぐのは、URL でもクラス名でもなく handler ID です。
画面 IR:
actions:
- id: confirm
handler: customerEdit.confirm # ← この文字列がキー
next: customer-confirm
フロー IR(別ファイル):
handler: customerEdit.confirm # ← 画面と同じ ID
screenId: customer-edit
transaction: required
この1本の文字列が、次を結びます。
- どの画面のどのボタンか
- 詳細設計のどのシート(1メソッド1シート)か
- 生成される Controller のどのメソッドか
- Port のどのメソッドか
handler ID の命名規則はプロジェクトで1つに固定する。 例: {画面camelCase}.{アクション}。シート名と handler を一致させるかは未決定ですが、どちらにせよ 対応表は機械が持つ のが前提です。
詳細設計 Excel と IR の対応
多くの現場では、1画面あたり次のようなシート構成です。
A. 画面の処理シート → 画面 IR(前編)
| シートの章 | 画面 IR |
|---|---|
| 画面表示の内容 | layoutId, fields, messages |
| 部品の使い方 | widgetId |
| 表示制御 | visibleWhen 等 |
| イベント処理 | actions(handler, next) |
B. Controller 1メソッド = 1シート → フロー IR(本記事の中心)
設計書には、だいたい次の順で書かれています。
Controller → Service → Repository → Dao
バリデーション
→ Service 引数作成
→ Service 呼び出し
→ Repository 呼び出し
→ Dao DB 処理
→ 結果処理
→ コード値名称変換
→ Model 設定・画面遷移
これを step の配列 に落とします。1行が1ステップではなく、意味のまとまり が1ステップです。
handler: customerEdit.confirm
screenId: customer-edit
transaction: required
input:
form: CustomerEditForm
output:
model: [customerName, statusName]
steps:
- layer: controller
type: validateForm
- layer: controller
type: buildServiceInput
mapping:
customerId: form.id
email: form.email
- layer: service
type: call
method: updateCustomer
- layer: repository
type: findById
entity: Customer
notFound: customer.notFound # 該当なしは BusinessException(生成器の既定規約)
- layer: dao
type: update
table: M_CUSTOMER
- layer: service
type: manual
id: mailNotification
doc: メール変更時は確認メールを送信する
- layer: service
type: mapCodeToName
codeTable: M_STATUS
- layer: controller
type: setModelAndRedirect
next: customer-confirm
設計書の「縦の流れ」が、IR では「横のリスト」になります。
入れ子の規則: リストはフラットですが、生成されるコードの入れ子は layer で決まります。service の call が現れたら、次に controller へ戻るまでの service / repository / dao ステップは、その Service メソッドの 内側 に展開されます。上の例なら findById から mapCodeToName までが updateCustomer の中身になります。
なお、フラットなリストで無理なく表せるのは 分岐の浅い定型フロー までです。多段分岐やループを steps で無理に表現せず、その部分は manual step(後述の段階 C)に回します。
step type カタログ ― 詳細設計の「部品集」
画面に UI カタログがあるように、フローには step type カタログ があります。詳細設計に繰り返し現れる行を、閉じた型にマッピングします。
| 設計書に書かれがちなこと | type | layer |
|---|---|---|
| バリデーションチェック | validateForm | controller |
| Service 引数作成 | buildServiceInput | controller |
| Service 呼び出し | call | service |
| Repository 検索・更新 | findById / save / delete | repository |
| Dao SQL 処理 | select / insert / update / delete | dao |
| 結果の分岐・加工 | mapResult / branch | service |
| コード→名称変換 | mapCodeToName | service |
| 定型化できない業務処理(人が判断済み) | manual | service |
| Model へ値を載せる | setModelAttribute | controller |
| 画面遷移 | setModelAndRedirect / redirect | controller |
初期案は 10種前後。自社の Excel サンプルで「この行はどの type か」を洗い出し、docs/step-type-catalog.md に固定します。迷いやすい type は layer を選択制にせず固定します(例: mapCodeToName は service)。同じ IR からは必ず同じコードが出る、が原則です。
カタログにない行は unknown として止める。 画面の未知 widget と同じ扱いです。流れは3手です。
- パーサ・スキーマ検証が
unknownを検出し、CI が止まる - 人が判断する — 定型なら step type を1つ増やす。業務判断なら
manualstep として IR に明示する -
manualstep は手実装への委譲として生成される(後述の段階 A)
無理に AI に Java を書かせない、判断は必ず人が挟む、という点は画面と同じです。
バリデーションは二重に書かない
詳細設計と画面 IR の両方に「必須チェック」が書いてあると、生成物が二重になります。役割を分けます。
| 種類 | 担当 | 生成元 |
|---|---|---|
| 単項目(必須・桁数) | Bean Validation | 画面 IR |
| 相関(A 入力時は B 必須) | validateForm ステップ | 画面 IR or フロー IR の rules |
| 業務チェック(与信・在庫など) | Service 内 | フロー IR の call 先メソッド |
設計書に「単項目チェック」とあっても、画面 IR に既にあれば フロー IR には載せません。 設計書再生成時に両方から出力すれば、納品物としては矛盾しません。
data IR ― いちばん先に自動化する層
Service を語る前に、足元の データ定義 を固めます。ここは自然言語が少なく、自動化率が高いです。
# specs/data/M_CUSTOMER.yaml(イメージ)
table: M_CUSTOMER
columns:
- name: CUSTOMER_ID
type: bigint
primaryKey: true
- name: EMAIL
type: varchar(256)
nullable: false
DataEmitter が決定論的に出すもの:
- JPA Entity または MyBatis のマッピング
- Flyway マイグレーション
- Repository インターフェース(定型 CRUD)
- SQL が設計書に明示されていれば
@Queryまたは MyBatis XML
JPA か MyBatis かはプロジェクトで1つ決める。 IR 上は設計書どおり Repository → Dao の論理層を保ち、Emitter が物理層に畳みます。詳細設計の用語とソースの用語を一致させるためです。
Service は「全部自動」ではなく「骨格+段階的実装」
業務ロジックのすべてを step の組み合わせだけで表現するのは現実的ではありません。だから 3段階 で進めます。
段階 A:定型だけ生成、残りは委譲スタブ
- step type カタログに収まる行だけコード化
-
manualstep は 手実装用インターフェースへの委譲呼び出し として生成する。生成 Service の中に業務判断は書かない - 手実装側の初期スタブは
UnsupportedOperationExceptionを投げ、@Disabledテスト骨子が付く - コンパイルは通るが、未完成箇所は実行時に落ちる — これはバグではなく、仕様上の「未着手マーカー」
// 生成される Service 骨格のイメージ(生成物・編集禁止)
@Service
@Transactional
public class CustomerEditService {
private final CustomerRepository customerRepository;
private final CustomerEditManualSteps manualSteps; // 手実装への窓口(コンストラクタ注入は省略)
public void updateCustomer(UpdateCustomerCommand cmd) {
// generated: findById(notFound: customer.notFound)→ update(定型)
Customer entity = customerRepository.findById(cmd.getCustomerId())
.orElseThrow(() -> new BusinessException("customer.notFound"));
// ... update ...
// generated: manual step "mailNotification" は手実装へ委譲
manualSteps.mailNotification(cmd, entity);
}
}
// 生成される手実装インターフェース(生成物・編集禁止。Javadoc は IR の doc から)
public interface CustomerEditManualSteps {
/** メール変更時は確認メールを送信する */
void mailNotification(UpdateCustomerCommand cmd, Customer entity);
}
// domain/ 配下の手実装クラス(初回だけスタブを生成。以後は人+AI が編集してよい唯一の場所)
@Component
public class CustomerEditManualStepsImpl implements CustomerEditManualSteps {
@Override
public void mailNotification(UpdateCustomerCommand cmd, Customer entity) {
// TODO(domain): メール変更時は確認メールを送信する
throw new UnsupportedOperationException("customerEdit.confirm: step mailNotification");
}
}
@Test
@Disabled("未実装: フロー IR に未対応の業務ステップ")
@DisplayName("メールアドレス変更時は確認メールを送信する")
void sendConfirmationMailWhenEmailChanged() { }
前編のポートの @Disabled テスト骨子と同じ思想です。設計書の備考が、実装の TODO リストになる。
段階 B:判断表・計算式を rule IR 化
「金額が◯◯以上なら承認必要」のような記述は、将来 specs/domain/ の rule IR に落とし、決定論的に実行します。DSL は小さく始めます(閾値比較、コード値マッチ、加算程度)。
段階 C:残りだけ人+AI が manual step を実装
- 1 manual step = 手実装クラスの1メソッド。この単位を崩さない
- 生成された Service 骨格・インターフェースは一切編集しない
- 手実装は
domain/配下の manual step 実装クラスのみ
段階 C が画面数と同じペースで増え続けるなら、step type 不足か rule IR 不足の信号です。撤退基準にもします(後述)。
FlowEmitter が出すもの
1つの handler について、おおよそ次が生成されます。
| 入力 | 出力 |
|---|---|
| steps(controller 層) | Controller の該当メソッド本体 |
| steps(service 層) | Service クラスのメソッド骨格 |
| steps(repository/dao 層) | 呼び出し配線・トランザクション境界 |
| steps(manual) | 手実装インターフェース+初期スタブ(スタブ生成は初回のみ) |
| transaction |
@Transactional の付与 |
| input / output | 引数マッピング・Model 属性 |
前編で生成した Controller は メソッドのシェル だけ持ち、Phase 2 で 中身が埋まる イメージです。どちらも生成物であり、手編集しません。
Port との関係 ― 境界は維持する
前編の Port はそのまま残ります。
画面(生成)→ Port IF(生成)→ Port 実装(生成骨格)→ Service(生成骨格)→ manual step 実装(手書き)
フロー IR の call ステップは、Port 実装の内側で Service を呼ぶ形に展開します。Port 実装の骨格も FlowEmitter が生成します(後述の導入順 5)。チーム分業のときは次のように割り切ります。
| チーム | 触るもの |
|---|---|
| 画面 | 画面 IR、UI カタログ |
| 配線・骨格 | フロー IR、FlowEmitter、DataEmitter |
| 業務 | manual step 実装クラス、@Disabled テストを緑にする |
生成された Service 骨格は、どのチームも直しません。 業務の変更はフロー IR か manual step 実装に反映し、必要なら再生成します。
失敗時の分類(Phase 2 版)
前編の4分類を、そのまま拡張します。
| 分類 | 意味 | 直す場所 |
|---|---|---|
| 仕様の不備 | IR の矛盾・不足 | specs/ |
| 生成器の不備 | IR にあるのに出てこない | codegen/ |
| カタログ不足 | step type または widget がない | カタログ定義 |
| 業務の話 | ルールが step 化できない | domain 手実装(別ループ) |
生成された Service に1行足してテストを通す のは、前編と同じく禁止です。金型(FlowEmitter)が直っていません。
移行:画面のときと同じ手順
既存の詳細設計 Excel がある場合も、画面 Phase 1 と同型です。
- AI 等で Excel → フロー IR / data IR(一度きり)
- 逆翻訳チェック: IR → 再生成 Excel → 元と差分比較
- 差分なしの handler から承認
- 元 Excel 凍結、変更は IR 経由で設計書再生成
画面と業務で 承認の単位 を揃えると運用が楽です。おすすめは 1画面+その画面の handler 群を1セット で ir-managed に切り替えることです。
リポジトリ構成(Phase 2 追加分)
specs/
screens/ # 前編どおり
flows/ # 1 handler = 1 yaml
data/ # テーブル定義
domain/ # ルール・ユースケース(将来)
schema/ # JSON Schema
codegen/
emitters/
ScreenEmitter
FlowEmitter # 追加
DataEmitter # 追加
src/main/java/.../
web/ # Controller, Form(生成)
port/ # Port IF + 実装
domain/ # Service 骨格(生成)+ manual step 実装(手書き領域)
infrastructure/ # Entity, Repository, Dao(生成中心)
docs/
step-type-catalog.md # 自社 Excel 行 → type 対応表
legacy/ # 凍結 Excel
generated/ # 再生成設計書
導入順(Phase 2)
いきなり全 handler を IR 化しません。
- data IR で主要テーブル1〜2個 → Entity / Migration が通る
- handler 対応表 — 画面 IR の handler と詳細設計シートの対応を機械が検証
- フロー IR スキーマ + step type カタログ(10種前後)
-
垂直スライス —
initとconfirmの2メソッドだけ FlowEmitter 通過 - Port 実装骨格 +
@Disabledテスト骨子が揃う - 詳細設計パーサは 移行時のみ。恒常パイプラインにしない
撤退基準
次のどれかに当てはまったら、方式を見直します。
- 詳細設計の行の 5割超 が step type 化できない
- 段階 C の手実装が 画面数と同じペース で増え続ける
- handler ID と Excel シートの対応がチームごとにバラバラで統一できない
やってはいけないこと(Phase 2 版)
| やりがちなこと | 何が起きるか |
|---|---|
| 詳細設計 Excel を毎回 LLM に読ませて Service 全文生成 | 揺れ・検査不能・レビュー不能 |
| 生成 Service にビジネスロジックを直書き | 再生成で消える。業務判断は manual step 実装クラスへ |
| 画面 IR とフロー IR を1ファイルに混ぜる | 画面変更が Service 生成に波及し、影響が読めない |
| バリデーションを画面とフローの両方に書く | 二重チェック・矛盾 |
備考の自然言語をそのまま if 化 |
誤読が本番に入る。昇格できないものは docOnly + テスト骨子 |
| Repository と Dao を設計書なしに手書きで増やす | data IR を迂回し、DB 定義がずれる |
小さく始める:1画面・2 handler で試す
前編の垂直スライス(一覧・編集・確認・完了)に、次を足します。
-
customer-listのinit— 検索条件の表示だけ(Service はほぼ空) -
customer-editのconfirm— 更新フロー一式(validate → update → 遷移)
ゴールは 「IR に1ステップ足すと、再生成だけでテストの期待が変わる」 ことです。画面 Phase 1 と同じゲート(コンパイル・テスト・生成物編集禁止)を、そのまま Phase 2 に拡張します。
まとめ ― Service 層の全体設計
- 詳細設計 Excel は data IR / フロー IR / docOnly に分ける。自然言語を直接 Java にしない
- 画面と業務は handler ID で結ぶ。1メソッド1シートは
specs/flows/{handler}.yaml - 処理の定型は step type カタログ に閉じる。未知は
unknownで止め、カタログを育てる - data IR はほぼ全自動、Service は骨格を自動+業務は段階 A→B→C
- 備考の業務ルールは @Disabled テスト骨子 へ。前編のポートと同じ思想
- 生成された Service 骨格も 直接編集しない。手で書いてよいのは
domain/の manual step 実装クラスだけ。失敗は4分類して上流を直し再生成 - 移行は画面と同じく 逆翻訳差分承認 と Excel 凍結
前編が「人が HTML を書かない」話だったなら、Phase 2 は「人が配線コードを書かない。書くのは業務判断だけ」という話です。配線は IR と生成器に任せ、人と AI の精力は テストを緑にすること に集中させる。それが Service 層の全体設計です。