[速習] Spring 第4回 ～実践的なWebアプリケーション構築～ (Spring MVCのコントローラ設計／フォームバリデーション実装)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3521573/b0c64133-8767-4735-8260-cc806a5090e1.png)

https://qiita.com/ShigemoriMasato/items/754363ffd4335251c438

前回までの記事では、Springのコア技術である**DI（依存性注入）**と**AOP（アスペクト指向）**について解説しました。今回は、これらの知識を活かしながら**Spring MVCを使った実践的なWebアプリケーション構築**について、コントローラ設計、リクエスト・レスポンス処理、フォームバリデーションを中心に解説します。

## Spring MVCアーキテクチャの全体像

Spring MVCは、Springフレームワークが提供するWebアプリケーション構築のためのMVC[^1]アーキテクチャです。リクエスト処理の責務を明確に分離することで、保守性と再利用性に優れたアプリケーションを実現します。

```mermaid
graph TB
    subgraph "Client Side"
        Browser[ブラウザ]
    end
    
    subgraph "Spring MVC Components"
        DS[DispatcherServlet<br/>フロントコントローラ]
        HC[HandlerMapping]
        Controller[@Controller<br/>プレゼンテーション層]
        Service[@Service<br/>ビジネスロジック層]
        Repository[@Repository<br/>データアクセス層]
        VR[ViewResolver]
        View[View<br/>Thymeleaf/JSP]
    end
    
    subgraph "Data Store"
        DB[(Database)]
    end
    
    Browser -->|HTTP Request| DS
    DS --> HC
    HC --> Controller
    Controller --> Service
    Service --> Repository
    Repository --> DB
    DB --> Repository
    Repository --> Service
    Service --> Controller
    Controller --> VR
    VR --> View
    View -->|HTTP Response| Browser
```

### 各コンポーネントの役割

- `DispatcherServlet`: すべてのHTTPリクエストを受け付けるフロントコントローラ
- `@Controller`: プレゼンテーション層でリクエストを処理（`@RestController`はREST API向け）
- `@Service`: ビジネスロジックを実装（トランザクション境界はここに配置することが多い）
- `@Repository`: データアクセス層を担当（Spring Data JPAなどと連携）
- `View`: レスポンスの生成（Thymeleaf、JSPなど）

この階層構造により、各レイヤーの責務が明確になり、変更の影響を局所化できます。

## コントローラの実装とリクエストマッピング

Spring MVCでは、`@Controller`アノテーションでコントローラクラスを定義し、各種マッピングアノテーションでURLとメソッドを紐付けます。

```java
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.demo.form.ProductForm;
import com.example.demo.service.ProductService;

import jakarta.validation.Valid;
import java.util.List;

@Controller
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    // Spring Boot 3.x以降は単一コンストラクタの場合@Autowired省略可能
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    // 商品一覧表示
    @GetMapping
    public String list(Model model) {
        List<Product> products = productService.findAll();
        model.addAttribute("products", products);
        return "product/list"; // templates/product/list.html
    }

    // 商品登録フォーム表示
    @GetMapping("/add")
    public String showAddForm(Model model) {
        model.addAttribute("productForm", new ProductForm());
        return "product/add";
    }

    // 商品登録処理
    @PostMapping("/add")
    public String addProduct(
            @Validated @ModelAttribute ProductForm form,
            BindingResult result,  // 必ず検証対象の直後に配置
            RedirectAttributes redirectAttributes) {
        
        if (result.hasErrors()) {
            return "product/add"; // エラー時は入力画面へ
        }
        
        Long productId = productService.register(form);
        redirectAttributes.addFlashAttribute("message", 
            "商品を登録しました（ID: " + productId + "）");
        
        return "redirect:/products";
    }
}
```

### ポイント解説

1. **BindingResultの位置**: Spring MVCの仕様上、`BindingResult`は必ず検証対象パラメータの直後に配置する必要があります
2. **RedirectAttributes**: リダイレクト時にフラッシュスコープでメッセージを渡せます
3. **Model[^4]**: コントローラからビューへデータを渡すためのコンテナ

https://qiita.com/ShigemoriMasato/items/4b6e69c98c6371698ff1

## Bean Validationによるフォームバリデーション

Spring BootではBean Validation（JSR-380）が自動構成され、アノテーションベースのバリデーションが使えます。

```java
package com.example.demo.form;

import jakarta.validation.constraints.*;

public class ProductForm {
    
    @NotBlank(message = "商品名は必須です")
    @Size(max = 100, message = "商品名は100文字以内で入力してください")
    private String name;
    
    @NotNull(message = "価格は必須です")
    @PositiveOrZero(message = "価格は0以上の値を入力してください")
    @Max(value = 1000000, message = "価格は100万円以下で入力してください")
    private Integer price;
    
    @Size(max = 500, message = "説明は500文字以内で入力してください")
    private String description;
    
    // getter/setter
}
```

### `@Validated`とValid の使い分け

- `@Valid`: 標準的なBean Validation（jakarta.validation）
- `@Validated`: Springの拡張アノテーション（グループバリデーション[^2]をサポート）

グループバリデーションを使わない場合は`@Valid`でも十分ですが、将来の拡張性を考慮して`@Validated`を使うことを推奨します。

## Thymeleafテンプレートとの連携

ビュー層では、Thymeleafを使ってサーバーサイドの値を表示します。

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>商品登録</title>
</head>
<body>
    <h1>商品登録</h1>
    
    <form th:action="@{/products/add}" th:object="${productForm}" method="post">
        <!-- CSRF対策トークン（Spring Securityで自動生成） -->
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        
        <div>
            <label for="name">商品名：</label>
            <input type="text" id="name" th:field="*{name}" />
            <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" 
                  style="color: red;">エラーメッセージ</span>
        </div>
        
        <div>
            <label for="price">価格：</label>
            <input type="number" id="price" th:field="*{price}" />
            <span th:if="${#fields.hasErrors('price')}" th:errors="*{price}" 
                  style="color: red;">エラーメッセージ</span>
        </div>
        
        <button type="submit">登録</button>
    </form>
</body>
</html>
```

## エラーメッセージのカスタマイズ

Spring Bootでは`src/main/resources/messages.properties`にメッセージを定義すると、自動的にMessageSourceが構成されます。

```properties
# messages.properties
product.name.required=商品名を入力してください
product.price.invalid=正しい価格を入力してください

# messages_en.properties（英語版）
product.name.required=Product name is required
product.price.invalid=Please enter a valid price
```

## 例外ハンドリングとAOPの活用

`@ControllerAdvice`を使うと、横断的な例外処理を実装できます。これもAOPの考え方を活用した仕組みです。

```java
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DataAccessException.class)
    public String handleDatabaseError(DataAccessException e, Model model) {
        model.addAttribute("error", "データベースエラーが発生しました");
        return "error/database";
    }
    
    @ExceptionHandler(BusinessException.class)
    public String handleBusinessError(BusinessException e, Model model) {
        model.addAttribute("error", e.getMessage());
        return "error/business";
    }
}
```

## DI/AOPを活用したアーキテクチャ

Spring MVCのレイヤー構造では、DIによる疎結合とAOPによる横断的関心事の分離が実現されています。

```mermaid
classDiagram
    class ProductController {
        -ProductService productService
        +list(Model): String
        +showAddForm(Model): String
        +addProduct(ProductForm, BindingResult): String
    }
    
    class ProductService {
        -ProductRepository productRepository
        +findAll(): List~Product~
        +register(ProductForm): Long
    }
    
    class ProductRepository {
        +findAll(): List~Product~
        +save(Product): Product
    }
    
    class TransactionAspect {
        <<AOP>>
        +transactionAdvice()
    }
    
    class LoggingAspect {
        <<AOP>>
        +loggingAdvice()
    }
    
    ProductController --> ProductService: DI
    ProductService --> ProductRepository: DI
    TransactionAspect ..> ProductService: 適用
    LoggingAspect ..> ProductController: 適用
    LoggingAspect ..> ProductService: 適用
```

※上図は、DIによる依存関係とAOPによる横断的処理の適用を表しています。Mermaidが描画されない環境では、コントローラ→サービス→リポジトリの依存関係と、それらに対するトランザクション・ロギングのアスペクト適用をイメージしてください。

## まとめ

今回は、Spring MVCを使った実践的なWebアプリケーション構築について解説しました。

### 本記事で学んだこと
- Spring MVCのアーキテクチャとコンポーネントの役割
- コントローラの実装とリクエストマッピング
- Bean Validationによる入力値検証
- Thymeleafテンプレートとの連携
- 例外ハンドリングとAOPの活用

これらの要素が、前回学んだDI/AOPと密接に連携して動作することで、Spring MVCの強力かつ柔軟なWebアプリケーション開発が可能になります。

次回は「**Springのデータアクセス（Spring Data JPA）とトランザクション管理**」について、より実践的な内容を解説する予定です。

この記事が、Spring MVCを使った実践的なWebアプリケーション開発の第一歩となることを願っています！

https://qiita.com/ShigemoriMasato/items/6311a8362f0fd27e6af6

---

## 参考文献
- [Spring Framework Documentation](https://docs.spring.io/spring-framework/reference/index.html)
- [Spring Boot Reference Documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)
- [Thymeleaf Documentation](https://www.thymeleaf.org/documentation.html)
- [Bean Validation specification](https://beanvalidation.org/2.0/spec/)
- [Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発](https://www.shoeisha.co.jp/book/detail/9784798142470)

---

[^1]: MVC(Model-View-Controller) - アプリケーションを「モデル（データ・ビジネスロジック）」「ビュー（表示）」「コントローラ（制御）」の3つに分離する設計パターン。責務の分離により保守性が向上する。
[^2]: グループバリデーション - 登録時と更新時で異なる検証ルールを適用するなど、状況に応じてバリデーションを切り替える仕組み。`@Validated`でグループを指定できる。
[^3]: BindingResult - Spring MVCにおいてフォームのバインディング結果と検証エラーを保持するオブジェクト。必ず検証対象の直後に配置する必要がある。
[^4]: Model - Spring MVCでコントローラからビューへデータを渡すためのコンテナ。addAttributeメソッドで値をセットする。