Amanity
AI駆動開発でMVCは必要か——本当に重要なのはパターンより「明文化」だった
ブログエンジニア向け2026-06-28約10分

AI駆動開発でMVCは必要か——本当に重要なのはパターンより「明文化」だった

対象読者: エンジニア・技術選定に関わる開発者

AIコーディングツールの登場で、開発スピードが劇的に上がりました。Cursor、Claude Code、GitHub Copilot——これらのツールを使えば、かつては2〜3日かかっていた機能実装が半日で終わることも珍しくありません。

しかし同時に、「AIに任せたら設計がカオスになった」という声もよく聞きます。

UIコンポーネントの中にDBアクセスが書かれています。同じロジックが3か所に散在しています。テストが書きにくい構造になっています——。

この記事では、なぜAIが「読みにくいコード」を生成しがちなのか、そしてMVCのような設計パターンが本当に必要なのかを掘り下げます。

結論だけ先に言っておきます。「AIのためにコーディングがルール化されているか」——問うべきはこれだけです。

AIはルールなしでコードを書くと何をするか

典型例——UIロジック・ビジネスロジック・DB呼び出しが1ファイルに混在

まず、現実に起きている問題を具体的に見てみましょう。

「UIとDBが同じファイルに?フロントとバックが混在しているということ?」と思った方もいるかもしれません。従来のアーキテクチャ(React + Express など)では、ブラウザ側のコードとサーバー側のコードは物理的に分離されていたため、1ファイルに混在しようがありませんでした。

ところが Next.js の App Router では、page.tsx はサーバーサイドで動く React Server Component です。サーバー上で実行されるため、Prisma(DBクライアント)を直接 import して呼び出すことができます。これにより「APIエンドポイント」という強制的な境界が消え、DBアクセス・ビジネスロジック・UI描画をひとつのファイルに書けてしまう状況が生まれました。

次のコードは、「あるエンジニアがCursorに頼んで書いてもらった注文詳細ページ」の実物に近いパターンです(Next.js App Routerを使用)。

typescript
// app/orders/[id]/page.tsx — AIが「ひとまず動く」として生成したコード
import { prisma } from '@/lib/prisma';

export default async function OrderPage({ params }: { params: { id: string } }) {
  // DBを直接呼ぶ
  const order = await prisma.order.findUnique({
    where: { id: params.id },
    include: { items: true, user: true },
  });

  if (!order) return <div>注文が見つかりません</div>;

  // ビジネスロジックもここに書く
  const subtotal = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const discount = order.user.isPremium ? subtotal * 0.1 : 0;
  const tax = (subtotal - discount) * 0.1;
  const total = subtotal - discount + tax;

  // さらにメール送信の副作用まで混入
  if (order.status === 'pending' && !order.reminderSent) {
    await fetch('/api/mail/send', {
      method: 'POST',
      body: JSON.stringify({ to: order.user.email, orderId: order.id }),
    });
    await prisma.order.update({
      where: { id: order.id },
      data: { reminderSent: true },
    });
  }

  return (
    <div>
      <h1>注文 #{order.id}</h1>
      <p>小計: {subtotal}</p>
      {discount > 0 && <p>割引: -{discount}</p>}
      <p>消費税: {tax}</p>
      <p>合計: {total}</p>
    </div>
  );
}

動きます。テストしてみると要件も満たしています。だが、このファイルは以下の4つの仕事を1か所でやってしまっています。

  1. DBアクセスprisma.order.findUnique
  2. ビジネスロジック(割引計算・消費税計算)
  3. 副作用(メール送信・DB更新)
  4. UI描画(JSXのreturn)

これが「1ファイルに何でも書く」問題です。初回はこれで動きます。しかし次の追加機能を依頼したとき、AIはこのファイルに同じスタイルで書き足します。気づけば責務が混在したコードが増え続け、同じ割引ロジックが別のページにも別の実装で書かれています。

なぜこうなるか——AIに「暗黙の了解」は通じない

エンジニア同士のチームなら、「pageコンポーネントにビジネスロジックを書かない」は暗黙の了解として機能します。コードレビューで指摘されれば直りますし、ベテランのコードを読めば「こう書くものだ」と学べます。

AIにはこの暗黙の了解が通じません。

AIは「現在のコンテキスト」から最も自然に見えるコードを生成します。もし既存のコードにすでにビジネスロジックがUIと混在していれば、AIはその「文体」に合わせて続きを書きます。もし何も制約がなければ、「その場で動く、最短のコード」を選択する傾向があります。

これはAIの欠陥ではありません。AIは指示されたことをやっているに過ぎません。問題は、「どこに何を書くか」というルールが指示として存在していないことです。

MVC / Clean Architecture / Vertical Slice——3つのパターンが共通して言っていること

MVC / Clean Architecture / Vertical Slice 比較図

それぞれの「境界線の引き方」を並べる

設計パターンの話になると、宗教論争のように白熱します。「MVCで十分だ」「いや、Clean Architectureが正解だ」「Vertical Sliceのほうが実用的だ」——それぞれの主張には根拠があります。

3つのパターンを横に並べて比較してみましょう。

MVC(Model-View-Controller)

Rails、Laravel、Djangoなどが採用する古典的なパターンです。

  • View: テンプレート・画面描画
  • Controller: リクエスト受付・Modelへの橋渡し
  • Model: DB操作・ビジネスロジック(広義)

シンプルですが、Modelが肥大化する「Fat Model問題」が起きやすいです。

Clean Architecture(クリーンアーキテクチャ)

Uncle Bobが提唱しています。関心の分離を徹底するために4つのレイヤーを設けます。

  • Entities: ビジネスルール(外部依存ゼロ)
  • Use Cases: アプリケーション固有のビジネスロジック
  • Interface Adapters: フレームワーク・DBとの変換
  • Frameworks & Drivers: Next.js、Prismaなど

依存の方向を内側に向けること(Dependency Rule)が核心です。テスタビリティが高いですが、ボイラープレートが多くなります。

Vertical Slice Architecture(垂直スライス)

機能単位(ユースケース単位)で完全に縦に分割します。features/order/配下にUI・ロジック・DB処理を全部入れ、機能間の依存を最小化します。水平分割(レイヤー)より垂直分割を優先します。

共通の本質——「どこに何を書くか、誰が何を知ってよいか」が明文化されているか否か

3つのパターンは表面上まったく違います。しかし共通して言っていることは1つです。

「この責務はここに書く。この層はあの層のことを知ってはいけない」というルールが存在し、明文化されている。

MVCなら「ビジネスロジックはModelに書く。ViewはModelを直接更新しない」。Clean Architectureなら「Use CaseはFrameworkのことを知らない」。Vertical Sliceなら「features/order/は features/payment/の内部を直接importしない」。

どれも境界線依存の方向を定めています。パターン名は関係ありません。ルールが存在するか否か——これだけが問題です。

現場の実態——ほとんどのプロジェクトはフレームワーク任せ

フレームワーク任せとは何か

Railsを使えばMVCのディレクトリ構造が最初からあります。Next.js App Routerを使えば app/components/lib/ という分割が自然に生まれます。

多くのプロジェクトはここで止まります。「app/にページを書き、components/にコンポーネントを書く。それ以上のルールは書かなくていい」——というスタンスです。

フレームワークが提供するのはディレクトリ構造です。「このディレクトリに何を入れてはいけないか」「依存の方向はどちらか」は、フレームワークは一切定義しません。

たとえばNext.jsは app/page.tsx から prisma を直接呼ぶことを技術的には禁止していません。components/ の中でAPIフェッチを書くことも禁止していません。

フレームワークが言っているのは「このディレクトリを使いなさい」であり、「ここには何を書いてはいけない」は言っていません。

人間同士ならそれで機能する。AIはうまく機能しない理由

経験のあるエンジニア5人のチームなら、フレームワーク任せでも問題なく動きます。

暗黙の了解があります。「pageコンポーネントはUIだけ」「DBアクセスはrepositoryに切り出す」「ビジネスロジックは純粋関数で書く」——口頭のコーディング規約や過去の議論から、チームメンバーはこれを知っています。コードレビューがフィルタリングします。

AIにはこれが通じません。

AIが参照するのはプロンプトと、コンテキストウィンドウ内の既存コードです。「うちのチームでは暗黙の了解でビジネスロジックはservice層に書く」という情報は、どこにも書いていなければAIには届きません。

結果として、AIは「ひとまず動く最短コード」か、「既存コードのパターンを踏襲したコード」を生成します。前者は設計を無視し、後者は既存の悪いパターンを増殖させます。

AIが増やすのは、すでにそこにあるものです。

良いルールが書いてあれば良いコードを増やします。ルールがなければ、アドホックなコードを増やします。

意識的な人がやっていること——コーディングをルール化する実例

AIが読めるルールファイルに書く

主要なAIコーディングツールには、プロジェクトのルールを読み込ませるファイルがあります。

ツールルールファイル
Cursor.cursor/rules/*.mdc(推奨)または .cursorrules(レガシー)
Claude CodeCLAUDE.md または .claude/ 配下の参照ファイル
GitHub Copilot.github/copilot-instructions.md
ツール非依存ARCHITECTURE.md 等を作成し、各ツールのルールファイルから参照させる

重要なのは「どのファイルに書くか」ではなく、「AIが読める場所に書いてあるか」です。CLAUDE.mdや.cursorrulesが肥大化するなら、別ファイル(ARCHITECTURE.md 等)に設計ルールを書き、ルールファイルからそのパスを参照させる運用でも構いません。

以下は「Next.js + Prisma」プロジェクト向けの設計ルール最小テンプレートです。どのツールのルールファイルにも同じ内容を書けます。

text
## アーキテクチャルール

### ディレクトリの責務(必ず守ること)

| パス | 書いてよいもの | 書いてはいけないもの |
|------|--------------|-------------------|
| app/ | ルーティング・ページ構成・認証ガード | DB直接アクセス・ビジネスロジック |
| lib/*/repository.ts | DB・外部APIとの入出力のみ | 計算ロジック・バリデーション |
| lib/*/service.ts | ユースケース(複数リポジトリを組み合わせる処理) | UI知識・Next.js固有の型 |
| lib/*/domain/*.ts | 純粋なビジネスロジック(副作用なし・外部依存なし) | import { NextRequest } 等 |
| components/ | 表示・インタラクション | prisma 直接使用・fetch(SWRを除く) |

### 依存の方向(逆転禁止)

app/ → lib/*/service.ts → lib/*/repository.ts → DB / 外部API
app/ → components/(UIのみ)

### 新しいファイルを作るときのチェック

1. 「このファイルは何をするファイルか」をひとことで言えるか?
2. 「UI描画 / データアクセス / ビジネスロジック」のどれか1つだけか?
3. 2つ以上の責務が混在するなら、ファイルを分割してから実装する

### 禁止パターン(コード生成時も守ること)

- app/ 配下のファイルに prisma.〇〇.findXxx() を直接書かない
- lib/*/service.ts に import { NextRequest } from 'next/server' を書かない
- lib/*/repository.ts に割引計算・税計算などのビジネスロジックを書かない
- components/ から直接 fetch('/api/...') を書かない(SWRか Server Component 経由にする)

このルールをAIのルールファイルに書いておくだけで、AIが生成するコードは大きく変わります。

「ルールがある」と「ルールがない」でAIの出力がどう変わるか

同じ「注文詳細ページを実装してください」という依頼に対して、ルールなし・ルールありで出力がどう変わるかを見てみましょう。

After(ルールあり)

上記のアーキテクチャルールをルールファイルに書いた状態で同じ依頼をすると、次のようなコードに分割されます。

typescript
// lib/order/repository.ts — DBアクセスのみ
import { prisma } from '@/lib/prisma';

export async function getOrderById(id: string) {
  return prisma.order.findUnique({
    where: { id },
    include: { items: true, user: true },
  });
}

export async function markReminderSent(orderId: string) {
  return prisma.order.update({
    where: { id: orderId },
    data: { reminderSent: true },
  });
}
typescript
// lib/order/domain/pricing.ts — 純粋なビジネスロジック(副作用なし)
import type { OrderItem, User } from '@prisma/client';

export function calculateOrderTotals(items: OrderItem[], user: User) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const discount = user.isPremium ? subtotal * 0.1 : 0;
  const tax = Math.floor((subtotal - discount) * 0.1);
  const total = subtotal - discount + tax;
  return { subtotal, discount, tax, total };
}
typescript
// lib/order/service.ts — ユースケース(リポジトリ + ドメインを組み合わせる)
import { getOrderById, markReminderSent } from './repository';
import { calculateOrderTotals } from './domain/pricing';

export async function getOrderWithTotals(id: string) {
  const order = await getOrderById(id);
  if (!order) return null;

  const totals = calculateOrderTotals(order.items, order.user);

  // 副作用(メール送信)もUIではなくここで担う
  if (order.status === 'pending' && !order.reminderSent) {
    await fetch('/api/mail/send', {
      method: 'POST',
      body: JSON.stringify({ to: order.user.email, orderId: order.id }),
    });
    await markReminderSent(order.id);
  }

  return { order, totals };
}
typescript
// app/orders/[id]/page.tsx — UIのみ(10行以下)
import { getOrderWithTotals } from '@/lib/order/service';

export default async function OrderPage({ params }: { params: { id: string } }) {
  const result = await getOrderWithTotals(params.id);
  if (!result) return <div>注文が見つかりません</div>;

  const { order, totals } = result;

  return (
    <div>
      <h1>注文 #{order.id}</h1>
      <p>小計: {totals.subtotal}</p>
      {totals.discount > 0 && <p>割引: -{totals.discount}</p>}
      <p>消費税: {totals.tax}</p>
      <p>合計: {totals.total}</p>
    </div>
  );
}

calculateOrderTotals は純粋関数なので、import するだけでユニットテストが書けます。getOrderById をモックすればservice層のテストも書けます。page.tsxはUIだけなので、どのロジックが変わっても影響しません。

同じ要件、同じAI、違うのはルールファイルに書いたルールだけです。

よくある誤解と回答

Q: MVCじゃないとダメ?

ダメではありません。

この記事で一貫して言っていることは「MVCを採用せよ」ではなく「境界線を明文化せよ」です。Vertical Sliceでも、Clean Architectureでも、あなた独自の命名規則でも構いません。「UIはUI、DBはDB、ビジネスロジックはビジネスロジック」という分割の意図が文章として書かれていれば、AIはそれに従います。

「うちは独自の設計をしている」という場合も、その設計の境界線と禁止パターンをAIのルールファイルに書けばよいです。

Q: フレームワークのディレクトリ構造があれば十分では?

十分ではありません。

Next.jsの app/components/lib/ という分割は「どのディレクトリを使うか」を定めていますが、「そのディレクトリに何を書いてはいけないか」は定めていません。

AIが参照するのは制約です。「app/ にDBアクセスを書いても技術的には動く」状態で、AIはより短い(責務を分離しない)実装を選ぶことがあります。「禁止事項」を明示することが重要であり、ディレクトリ構造は「ここに書く」を示しますが「ここには書かない」を示しません。

Q: ルールが長くなると指示遵守率が落ちると聞いた

正しい観察です。ただし、問題はルールの長さではなく構造にあります。

現代のLLM(Claude Sonnetなど)は、適切に構造化されたルールであれば数百行でも遵守できます。問題になるのは「すべてが散文で書かれており、何が禁止で何が推奨かが区別できない」テキストです。

有効な対策は2つあります。

  1. 禁止事項を箇条書きで明示する(「〇〇を書かない」「〇〇を import しない」)
  2. 理由を1行添える(「なぜか」があるとAIはルールを文脈で解釈できる)

ルールが長くなってきたら、まず「本当に全プロジェクトで守るべきか」を見直します。汎用ルールと特定機能のルールを分けて管理するのも有効です。

結論

MVCである必要はありません。Clean Architectureである必要も、Vertical Sliceである必要もありません。

問うべきはただ1つです。

AIのためにコーディングがルール化されているか。

「コーディングのルール」とは、スタイルガイドや命名規則のことだけではありません。「どこに何を書くか」「誰が何を知ってよいか」「何を書いてはいけないか」——責務・依存関係・境界線といった設計全体に関わるルールのことです。

このルールが明文化されていれば、AIは良い設計のコードを生成します。明文化されていなければ、AIはその場で動く最短コードを生成し、既存の悪いパターンを増幅させます。

設計パターンの価値は、そのパターン名にあるのではありません。「境界線が定義され、ルールが書かれている」という状態にあります。AIとともに開発するチームにとって、これはかつてないほど重要になっています。

まとめ・次のアクション

今日からできる1つのアクションを提示します。

使っているAIツールのルールファイル(.cursorrulesCLAUDE.md.github/copilot-instructions.md 等)に「アーキテクチャセクション」を追加しましょう。

最初は3行でよいです。「app/ にprismaを直接書かない」「ビジネスロジックはlib/配下の純粋関数に書く」「依存の方向はapp → lib → DB」——これだけ書いてAIに次のタスクを依頼してみてください。出力の変化がすぐに分かります。

設計とは、チームメンバーへの説明文です。AIがチームに加わった今、その説明文はAIにも読めるように書く必要があります。

Amanity

株式会社Amanity

福岡を拠点に、AIを活用したモバイル・Web・LINEアプリの受託開発を行っています。AI駆動開発の設計相談・ルールファイル整備についてお気軽にご相談ください。

無料で相談する