5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI時代の設計ガードレール ~崩れないSpring Boot設計ルール実践~

5
Last updated at Posted at 2026-04-09

本記事は、2026/5/30開催の「JJUG CCC 2026 Spring」の登壇内容に関連した技術記事シリーズです。
AIエージェント(Copilot / Claude Code / Cursor)の普及により、Java開発の前提は大きく変わりました。
本シリーズでは「AI時代における設計の変化」をテーマに、実務視点で整理していきます。

【シリーズ一覧】

  1. AI時代にJava設計はどう変わったのか
  2. カッペリーニコードとは何か
  3. Java17/21は設計をどう変えたか
  4. AIは設計できるのか
  5. AI時代の設計ガードレール

はじめに

AIエージェントがコードを書く時代になった。
GitHub Copilot、Claude Code、Cursorを使えば、DTOやRepositoryといった定型コードは驚くほど高速に生成できる。

しかし現場では、こんな問題が静かに進行している。

・Service層が消えてControllerがRepositoryを直接呼ぶ
・API単位でDTOが爆発的に増殖する
・モジュール境界を無視したimportが広がる
・Utilityクラスがドメインロジックを吸収し始める

コードはきれいなのに、構造が壊れていく。

これはAIの問題ではなく、ガードレールがない開発環境の問題だ。
本記事では、AI時代のSpring Boot開発に必要な設計ガードレールを、実務で使える具体策として整理する。

なぜガードレールが必要か

AIは「局所最適」が得意で「全体設計」が苦手

AIエージェントは目の前のタスクを解くことに長けている。
「このエンドポイントを実装して」と指示すれば、動くコードを返す。

問題は、AIが既存の設計文脈を理解しないまま実装することだ。

次のコードを見てほしい。

@RestController
@RequiredArgsConstructor
public class OrderController {

    private final UserRepository userRepository;  // ← Controller が Repository を直接参照
    private final OrderRepository orderRepository;

    @PostMapping("/orders")
    public OrderResponse create(@RequestBody CreateOrderRequest request) {

        User user = userRepository.findById(request.userId())
                .orElseThrow(() -> new NotFoundException("User not found"));

        Order order = new Order(user, request.items());
        orderRepository.save(order);

        return new OrderResponse(order.getId());
    }
}

AIが生成したこのコードは動く。しかし設計は壊れている。

  • Controller → Repository の直接依存(レイヤ違反)
  • トランザクション境界がController側に滲み出る
  • ビジネスロジックがControllerに混入している

これが1ファイルなら修正できる。しかしAI生成を繰り返すと、この「パターン」がコードベース全体に広がる

ガードレールがない開発で起きること

症状 原因
Service層の消失 ControllerがRepositoryを直接呼ぶ
DTO爆発 AI がAPI単位でRecordを生成し続ける
Utilityの増殖 共通処理をUtilに逃がすAIの癖
モジュール境界崩壊 他featureのRepositoryを直接importする
トランザクション散在 @Transactional をController/Repositoryに書く

これらはいずれもAIがコードを書くと発生しやすいパターンだ。
防ぐには、構造を事前に固定するしかない。

設計ルール

パッケージ構造ルール

レイヤー型ではなくfeature型を採用する。

レイヤー型は一見整理されているが、機能が増えるとfeature間の依存が制御しにくくなる。

# NG: レイヤー型(機能が増えると依存が複雑化する)
com.example
  ├── controller
  │   ├── UserController.java
  │   └── OrderController.java
  ├── service
  │   ├── UserService.java
  │   └── OrderService.java
  └── repository
      ├── UserRepository.java
      └── OrderRepository.java
# OK: feature型(モジュール境界が明確)
com.example
  ├── user
  │   ├── controller
  │   │   └── UserController.java
  │   ├── service
  │   │   └── UserService.java
  │   ├── domain
  │   │   └── User.java
  │   ├── repository
  │   │   └── UserRepository.java
  │   └── dto
  │       ├── CreateUserRequest.java
  │       └── UserResponse.java
  └── order
      ├── controller
      │   └── OrderController.java
      ├── service
      │   └── OrderService.java
      ├── domain
      │   └── Order.java
      ├── repository
      │   └── OrderRepository.java
      └── dto
          ├── CreateOrderRequest.java
          └── OrderResponse.java

この構造のポイントはfeature間のRepository直接参照を禁止できることだ。

# OK: featureをまたぐ場合はServiceを経由する
orderService → userService.findById()

# NG: featureをまたいでRepositoryを直接参照する
orderService → userRepository.findById()

レイヤー責務ルール

各レイヤーの責務を明文化し、チーム全員が参照できる場所に置く。

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    @PostMapping
    public UserResponse create(@RequestBody @Validated CreateUserRequest request) {
        return userService.create(request);
    }
}
  • 依存を許可するのはServiceのみ
  • DTOのみ扱い、Entityを返さない
  • ビジネスロジックは一切書かない
  • @Transactional書かない

Service

@Service
@Transactional
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public UserResponse create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.email())) {
            throw new DuplicateEmailException(request.email());
        }
        User user = User.create(request.name(), request.email());
        userRepository.save(user);
        return UserResponse.from(user);
    }
}
  • トランザクション境界はServiceが持つ(クラスレベルに @Transactional
  • ビジネスロジック・不変条件チェックはServiceに集約する
  • 依存を許可するのは同一feature内のRepositoryまたは他featureのService

Repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);

    boolean existsByEmail(String email);
}
  • DBアクセスのみ
  • ビジネスロジックを書かない
  • クエリが複雑な場合は@Queryを使い、ロジックは書かない

Domain(Entity)

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;

    // ファクトリメソッドで生成制約を表現
    public static User create(String name, String email) {
        User user = new User();
        user.name = Objects.requireNonNull(name, "name must not be null");
        user.email = Objects.requireNonNull(email, "email must not be null");
        return user;
    }
}
  • 不変条件はEntityに閉じ込める
  • Controller層への依存は絶対に持たない
  • @Setter は原則使わない(状態変更はメソッドで表現する)

命名規約

命名が曖昧だとAIが誤ったパターンで補完する。明文化することでAIへのヒントにもなる。

種別 命名パターン
Controller {Feature}Controller UserController
Service {Feature}Service UserService
Repository {Feature}Repository UserRepository
Entity {FeatureName} User, Order
リクエストDTO {動詞}{Feature}Request CreateUserRequest, UpdateUserRequest
レスポンスDTO {Feature}Response UserResponse
詳細レスポンス {Feature}DetailResponse UserDetailResponse
例外 {原因}Exception DuplicateEmailException

禁止命名

XxxUtil / XxxUtils    → ドメインに寄せる
XxxHelper             → 責務が不明確
XxxManager            → Serviceに統合する
XxxCommon             → featureごとに分ける

実装例:ArchUnitで自動検証する

設計ルールは文書化するだけでは守られない。ArchUnitでテストとして実装する。

依存関係

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.3.0</version>
    <scope>test</scope>
</dependency>

レイヤー違反の検出

@AnalyzeClasses(packages = "com.example")
public class LayerArchitectureTest {

    @ArchTest
    static final ArchRule controller_should_not_depend_on_repository =
            noClasses()
                    .that().resideInAPackage("..controller..")
                    .should().dependOnClassesThat()
                    .resideInAPackage("..repository..")
                    .because("ControllerはRepositoryを直接参照してはいけません。Serviceを経由してください。");

    @ArchTest
    static final ArchRule controller_should_not_depend_on_domain =
            noClasses()
                    .that().resideInAPackage("..controller..")
                    .should().dependOnClassesThat()
                    .resideInAPackage("..domain..")
                    .because("ControllerはEntityを直接扱ってはいけません。DTOを使用してください。");

    @ArchTest
    static final ArchRule repository_should_not_depend_on_service =
            noClasses()
                    .that().resideInAPackage("..repository..")
                    .should().dependOnClassesThat()
                    .resideInAPackage("..service..")
                    .because("Repositoryは上位レイヤーに依存してはいけません。");
}

Transactionalの配置検証

@AnalyzeClasses(packages = "com.example")
public class TransactionalArchitectureTest {

    @ArchTest
    static final ArchRule transactional_should_be_on_service_only =
            noClasses()
                    .that().resideOutsideOfPackage("..service..")
                    .should().beAnnotatedWith(Transactional.class)
                    .because("@TransactionalはService層にのみ使用してください。");
}

モジュール境界(feature間のRepository直接参照禁止)

@AnalyzeClasses(packages = "com.example")
public class ModuleBoundaryTest {

    @ArchTest
    static final ArchRule order_should_not_access_user_repository =
            noClasses()
                    .that().resideInAPackage("com.example.order..")
                    .should().dependOnClassesThat()
                    .resideInAPackage("com.example.user.repository..")
                    .because("featureをまたぐRepository参照は禁止です。Serviceを経由してください。");
}

Utilityクラスの禁止

@AnalyzeClasses(packages = "com.example")
public class NamingArchitectureTest {

    @ArchTest
    static final ArchRule no_utility_classes =
            noClasses()
                    .should().haveSimpleNameEndingWith("Utils")
                    .orShould().haveSimpleNameEndingWith("Util")
                    .orShould().haveSimpleNameEndingWith("Helper")
                    .because("Utilityクラスは禁止です。ロジックはドメインまたはServiceに配置してください。");
}

運用方法

AIへのプロンプト制御

AIエージェントに対して、プロジェクトのルールを明示的に伝えることで生成品質を上げられる。

CLAUDE.md / .cursorrules / Copilot Instructions に記述する

# Architecture Rules

## Package Structure
- Use feature-based package structure: `com.example.{feature}.{layer}`
- Layers: controller / service / domain / repository / dto

## Layer Dependencies
- Controller → Service only (never Repository or domain directly)
- Service → Repository (same feature only)
- Cross-feature access: always via Service, never via Repository

## Naming Conventions
- Request DTO: `{Verb}{Feature}Request` (e.g. CreateUserRequest)
- Response DTO: `{Feature}Response` (e.g. UserResponse)
- Never create Util / Helper / Manager / Common classes

## Transaction
- @Transactional is allowed only in Service layer (class level)
- Never put @Transactional in Controller or Repository

## Entity Rules
- Always use factory methods instead of public constructors
- Never use @Setter on Entity fields
- Never return Entity from Controller (always convert to DTO)

このファイルを置くことで、AIエージェントがプロジェクトの設計ルールを文脈として参照しながらコードを生成するようになる。

CIパイプラインへの組み込み

ArchUnitテストはユニットテストとして実行できるため、CIに組み込むだけで設計違反を自動検出できる。

# GitHub Actions の例
- name: Run Architecture Tests
  run: ./mvnw test -Dtest=*ArchitectureTest,*ModuleBoundaryTest,*NamingArchitectureTest

CIが落ちたら設計違反、というルールにすることで、レビュー前に機械的に検出できる。

PRレビューの観点シフト

AIがコードを書く時代のPRレビューは、構文ではなく構造を見ることに集中すべきだ。

従来のレビュー観点 AI時代のレビュー観点
変数名が適切か レイヤー依存が正しいか
ロジックのバグがないか モジュール境界を越えていないか
コードが読みやすいか @Transactional の位置が正しいか
テストが書かれているか EntityがControllerに漏れていないか

AIが書いたコードのバグはAIが直せる。しかし構造の問題はレビュアーが指摘しなければ残り続ける

AIに任せる領域・任せない領域

AI生成に向いている領域と、人間が設計すべき領域を明確にしておく。

AIに任せる(構造が決まっていれば高品質に生成できる)

  • DTOのRecord定義
  • JPA Repositoryのクエリメソッド
  • ControllerのCRUD実装
  • @ControllerAdvice の例外ハンドラー
  • JUnit / Mockitoを使ったテストコード

人間が設計する(AIには任せない)

  • feature分割の境界設計
  • Aggregate境界とEntityの不変条件
  • トランザクション境界の設計
  • 例外設計とエラーレスポンスの体系
  • パッケージ構造とモジュール依存の方針

まとめ

AIはコードを書く能力において、定型的な実装では人間を大きく上回り始めている。
しかし構造を設計する主体は人間だ、という事実は変わっていない。

本記事で紹介した設計ガードレールを整理すると次のようになる。

ガードレール 手段
パッケージ構造の固定 feature型構造を標準化する
レイヤー依存の制約 ArchUnitでテスト化してCIに組み込む
命名規約の明文化 チームドキュメント + CLAUDE.mdに記述
AIプロンプトの制御 .cursorrules / CLAUDE.md にルールを書く
レビュー観点のシフト 構文ではなく構造を見るレビューへ

AIと共存する開発において、アーキテクトに求められるのはコードを書く力ではなく構造を守る力だ。

AIは実装を担当する。人間は構造を設計する。

この役割分担を仕組みとして整備することが、AI時代の設計ガードレールの本質だ。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?