株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
はじめに
Claude Codeを使い始めると、最初の数日は感動します。「ログイン機能作って」と頼むだけで動くコードが出てくるからです。
ところが慣れてくると、別の壁にぶつかります。
- 出てきた実装が「自分が頭の中で思っていた仕様」と微妙に違う
- 後から「あ、この条件も必要だった」と気づいて何度も作り直す
- 数日後に見返すと「結局この機能の正しい仕様って何だっけ?」がどこにも残っていない
私はSES・受託開発を本業にしつつ、副業でiOSアプリを複数本Claude Codeで開発しています。両方の現場で痛感したのは、AIの能力ではなく「仕様の渡し方」が成果物の質を決めるということでした。
この記事では、抽象論ではなく 1つの機能(パスワード強度バリデーション)を題材に、要件定義 → 仕様書化 → 実装 → レビュー → 仕様への反映 までを一気通貫で回す手順を、実際に動くコードとともに解説します。
別記事(仕様駆動開発の考え方とCLAUDE.md設計)では「なぜ仕様が重要か」という概念を扱いましたが、本記事は 手を動かして1周回す実践編 です。
この記事の対象者
- Claude Code(あるいは他のAIコーディングツール)を使い始めた1〜5年目のエンジニア
- 「AIに頼むと毎回ブレる・手戻りする」悩みがある人
- 受託・個人開発で再現性のあるワークフローが欲しい人
仕様駆動開発(Spec-Driven Development)の全体像
仕様駆動開発とは、実装の前に「何を作るか」を検証可能な形で確定させ、その仕様を起点にAIに実装させる進め方です。Claude Codeで回す場合のループは次の5ステップになります。
[1] 要件定義 … ふわっとした要望をClaudeと壁打ちして言語化
↓
[2] 仕様書化 … 受入条件(Acceptance Criteria)まで落とす
↓
[3] 実装 … 仕様をコンテキストとして渡してコード生成
↓
[4] レビュー … 仕様 vs 実装の差分をチェック(コミット前)
↓
[5] 仕様への反映 … 実装で判明した変更点を仕様書に書き戻す
↑__________________________________________|
(次の変更でまた[1]へ戻る)
ポイントは [5] で終わらず [1] に戻るループであることです。実装中に必ず「仕様の穴」が見つかります。それを仕様書に書き戻すことで、仕様書が常に「現在の正」を保ちます。
以下、各ステップを題材に沿って進めます。
ステップ1:要件定義 — ふわっとした要望をClaudeと言語化する
題材は「ユーザー登録時のパスワード強度チェック」です。最初の要望はこの程度の解像度しかないことがほとんどです。
パスワードが弱すぎたら登録させたくない。
このままClaude Codeに「パスワードチェック作って」と頼むと、Claudeが勝手に「8文字以上、記号必須、…」と仕様を補完してしまい、後で「記号は必須にしたくなかった」と手戻りします。そこで 実装させる前に、壁打ちで要件を引き出します。
実際に使うプロンプト例です。
パスワード強度チェック機能を作りたい。実装の前に、仕様を確定させたい。
いきなりコードは書かず、決めるべき論点を質問形式で挙げてほしい。
- 抜け漏れがちなエッジケースも論点に含めること
- 各論点に「よくある選択肢」を添えること
「いきなりコードは書かず」「質問形式で」と明示するのがコツです。Claudeは次のような論点を返してきます(抜粋)。
- 最小・最大文字数は?(最大を設けないと長大入力でハッシュ計算が重くなる)
- 文字種の要件は?(大文字・小文字・数字・記号のうちどれを必須にする)
- 空白文字の扱いは?(許可するか、トリムするか、拒否するか)
- null・空文字の扱いは?
- エラーは「最初の1件」か「違反全部」を返すか?
ここで初めて、自分の頭の中の要件が明確になります。
ステップ2:仕様書化 — 受入条件まで落とす
壁打ちで決まった内容を 検証可能な受入条件(Acceptance Criteria) に落とします。受入条件とは「この条件を満たせば仕様どおり」と機械的に判定できる粒度の記述です。曖昧さが残っていると、ステップ4のレビューとステップ5の反映が回らなくなります。
今回の題材で確定した仕様書(Markdown)の例です。
# 仕様: パスワード強度バリデーション
## 目的
ユーザー登録時に脆弱なパスワードを拒否する。
## 受入条件(Acceptance Criteria)
- AC1: null または空文字は不合格とし「未入力」を返す
- AC2: 8文字以上 64文字以下であること(範囲外は不合格)
- AC3: 英大文字・英小文字・数字をそれぞれ1文字以上含むこと
- AC4: 半角スペースを含む場合は不合格
- AC5: 不合格時は、違反した条件すべてを理由のリストとして返す
(ただしAC1に該当する場合は他を評価せず未入力のみ返す)
## 対象外(やらないこと)
- 記号(!?@など)の必須化はしない(任意で使えるが必須にはしない)
- パスワード使い回し・漏洩チェックは本機能のスコープ外
「対象外(やらないこと)」を明記するのが重要です。Claudeは親切ゆえに頼んでいない機能まで足しがちなので、スコープの境界を仕様書に書いておくと暴走を防げます。
補足:チーム開発であれば、この仕様書をHTMLに変換して仕様書ポータル(社内/個人のGitHub Pages等)に公開しておくと、スマホからでも参照でき、レビュー時の「正」が一意になります。本記事ではMarkdownのままローカルに置く前提で進めます。
ステップ3:実装 — 仕様をコンテキストとして渡してコード生成させる
仕様書が固まったら、それを そのままコンテキストとして渡して実装させます。プロンプトはこうなります。
添付の仕様書(受入条件AC1〜AC5)を満たすJavaクラス PasswordPolicy を実装してください。
- 各ACにコメントで対応を明記すること
- 戻り値は「合否」と「違反理由のリスト」を持つ型にすること
- 外部ライブラリは使わず標準APIのみで実装すること
仕様を起点にすると、生成されるコードは「ACという根拠」を持ちます。レビューで「なぜこの分岐があるのか」を仕様まで辿れるのが利点です。
生成されたコードの例(コンパイル・実行確認済み)です。
import java.util.ArrayList;
import java.util.List;
/**
* パスワード強度ポリシーの検証クラス。
* 仕様(受入条件AC1〜AC5)に対応して各ルールを実装している。
*/
public final class PasswordPolicy {
/** 検証結果。合格可否と違反理由のリストを保持する。 */
public record Result(boolean valid, List<String> violations) {}
private static final int MIN_LENGTH = 8;
private static final int MAX_LENGTH = 64;
/**
* パスワードがポリシーを満たすか検証する。
*
* @param raw 検証対象のパスワード(null許容。nullは未入力として扱う)
* @return 検証結果
*/
public Result validate(String raw) {
List<String> violations = new ArrayList<>();
// AC1: 未入力(null/空)は不合格。他は評価しない
if (raw == null || raw.isEmpty()) {
violations.add("パスワードが入力されていません");
return new Result(false, violations);
}
// AC2: 8文字以上64文字以下
if (raw.length() < MIN_LENGTH) {
violations.add(MIN_LENGTH + "文字以上にしてください");
}
if (raw.length() > MAX_LENGTH) {
violations.add(MAX_LENGTH + "文字以下にしてください");
}
// AC3: 英大文字・英小文字・数字をそれぞれ1文字以上含む
if (raw.chars().noneMatch(Character::isUpperCase)) {
violations.add("英大文字を1文字以上含めてください");
}
if (raw.chars().noneMatch(Character::isLowerCase)) {
violations.add("英小文字を1文字以上含めてください");
}
if (raw.chars().noneMatch(Character::isDigit)) {
violations.add("数字を1文字以上含めてください");
}
// AC4: 半角スペースを含まない
if (raw.indexOf(' ') >= 0) {
violations.add("半角スペースは使用できません");
}
// AC5: 違反全件をまとめて返す
return new Result(violations.isEmpty(), violations);
}
public static void main(String[] args) {
PasswordPolicy policy = new PasswordPolicy();
String[] samples = {
"Passw0rd", // 合格: 8文字・大小数字すべて含む
"short1A", // 不合格: 7文字(長さ不足)
"alllowercase1", // 不合格: 大文字なし
"Pass w0rd", // 不合格: スペースを含む
"" // 不合格: 未入力
};
for (String s : samples) {
Result r = policy.validate(s);
System.out.println("入力: \"" + s + "\" => "
+ (r.valid() ? "合格" : "不合格 " + r.violations()));
}
}
}
main を実行すると、各受入条件に対応した出力が得られます(実行確認済み)。
入力: "Passw0rd" => 合格
入力: "short1A" => 不合格 [8文字以上にしてください]
入力: "alllowercase1" => 不合格 [英大文字を1文字以上含めてください]
入力: "Pass w0rd" => 不合格 [半角スペースは使用できません]
入力: "" => 不合格 [パスワードが入力されていません]
record を使うことで結果の構造が一目で分かり、Character::isUpperCase 等のメソッド参照で各ACが宣言的に読めます。
ステップ4:レビュー — 受入条件をテストに落として「仕様 vs 実装」を突き合わせる
ここが仕様駆動開発の心臓部です。受入条件をそのままテストケースに変換すれば、仕様と実装の整合性を機械的に検証できます。「ACがそのままテスト名になる」のが理想形です。
JUnit 5でのテスト例(全件パス確認済み)です。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PasswordPolicyTest {
private final PasswordPolicy policy = new PasswordPolicy();
@Test
void 受入条件_有効なパスワードは合格() {
assertTrue(policy.validate("Passw0rd").valid());
}
@Test
void 受入条件_8文字未満は不合格() { // AC2
PasswordPolicy.Result r = policy.validate("short1A");
assertFalse(r.valid());
assertTrue(r.violations().contains("8文字以上にしてください"));
}
@Test
void 受入条件_大文字なしは不合格() { // AC3
assertFalse(policy.validate("alllowercase1").valid());
}
@Test
void 受入条件_スペースを含むと不合格() { // AC4
assertFalse(policy.validate("Pass w0rd").valid());
}
@Test
void 受入条件_未入力は不合格() { // AC1
assertFalse(policy.validate("").valid());
assertFalse(policy.validate(null).valid());
}
}
実行すると次のように全テストが成功します(junit-platform-console-standalone で実行確認済み)。
[ 5 tests successful ]
[ 0 tests failed ]
Claude Codeに対しては、こう依頼するとレビューが自動化できます。
仕様書のAC1〜AC5それぞれに対応するJUnit5テストを書いてください。
テストメソッド名にどのACに対応するかが分かるようにすること。
仕様にあるのにテストが無いACがあれば指摘してください。
最後の一文(仕様にあるのにテストが無いACの指摘)が効きます。これにより「AC2の最大文字数(64文字超)のテストが抜けている」といった仕様カバレッジの穴をClaudeが洗い出してくれます。コミット前にこの突き合わせを習慣にすると、レビューの再現性が一気に上がります。
ステップ5:仕様への反映 — 実装で判明した変更を書き戻す
実装・テスト中には、必ず「仕様の言い忘れ」が見つかります。例えば今回なら、レビュー中にこんな疑問が出るはずです。
全角スペースは? AC4は「半角スペース」しか書いていない。
これは仕様の穴です。ここで実装だけ直して終わらせると、仕様書とコードが乖離し、次に触る人(数週間後の自分を含む)が混乱します。正しい手順は 仕様書に書き戻してから実装を直すことです。
仕様書のACを更新します。
- AC4: 半角スペースおよび全角スペース(U+3000)を含む場合は不合格
そのうえで実装を仕様に追従させます。
// AC4: 半角・全角スペースを含まない
if (raw.indexOf(' ') >= 0 || raw.indexOf(' ') >= 0) {
violations.add("スペースは使用できません");
}
最後にテストも追加し、「仕様 → テスト → 実装」の三者が常に一致した状態を保ちます。この 三者一致のループこそが仕様駆動開発の本質で、ループが回り続ける限り「結局正しい仕様は何だっけ?」が起きなくなります。
ワークフローを定着させる3つの仕組み
ここまでの手順を毎回手作業でやるのは大変です。私は実務・個人開発で次の3つに仕組み化しています。
1. CLAUDE.md に「規約」を集約する
プロジェクトルートの CLAUDE.md に、毎回守ってほしいルール(命名規約、テストの書き方、「やらないこと」など)を書いておくと、Claude Codeが全リクエストで参照します。仕様書には「その機能固有の受入条件」、CLAUDE.mdには「プロジェクト共通の規約」を置く、と役割を分けるのがコツです。
# CLAUDE.md(抜粋・例)
## コーディング規約
- バリデーション系クラスは検証結果を record で返す
- equals をオーバーライドしたら hashCode も必ず実装する
## レビュー方針
- 機能実装時は対応する受入条件を必ずテストに落とす
- 仕様にあってテストの無いACがあれば実装前に指摘する
2. エージェントで工程を分業する
Claude Codeのサブエージェント機能を使い、「要件を壁打ちするエージェント」「コードレビューするエージェント」のように工程ごとに役割を分けると、各工程で観点がブレません。レビュー専任エージェントには「仕様カバレッジの穴を探す」ことだけに集中させる、といった使い分けができます。
3. 仕様書をHTMLで公開し「正」を一意にする
仕様書をMarkdownのままローカルに置くと埋もれます。チームや複数端末で共有するなら、HTMLに変換してGitHub Pages等に公開し、URLで参照できる状態にしておくと、レビュー時の「正」が一意になります。スマホからも確認でき、仕様の認識ズレが減ります。
まとめ
| ステップ | やること | アウトプット |
|---|---|---|
| 1. 要件定義 | 壁打ちで論点を洗い出す | 確定した要件 |
| 2. 仕様書化 | 受入条件まで落とす | 検証可能な仕様書 |
| 3. 実装 | 仕様を渡してコード生成 | ACに紐づくコード |
| 4. レビュー | ACをテストに変換し突合 | 仕様カバレッジ済テスト |
| 5. 仕様反映 | 判明した変更を書き戻す | 三者一致の仕様書 |
Claude Codeの価値は「速くコードを書くこと」だけではありません。仕様という根拠を起点にすることで、生成物に再現性と説明責任を持たせられる点にあります。「とりあえず作って」をやめて、まず1機能でこのループを回してみてください。手戻りが体感で減るはずです。
参考
- Claude Code 公式ドキュメント
- CLAUDE.md(メモリ)の設定 — Claude Code Memory
- Claude Code サブエージェント
- JUnit 5 User Guide
- java.lang.Character (Java SE 21 API)
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!