[Spring Boot] Controller層の設計と責務分離

## はじめに

Spring Bootでの長期インターンで得た経験をもとに、Controller層の設計と分離に関するTipsを紹介します。スタッフを管理するようなシステムを例に、「可読性・再利用性・保守性」を意識したコーディングと、実務で用いられていた典型的な工夫を紹介します（あくまでインターン生として感じた工夫です）。


### 自己紹介

* 商学部出身
* 兵庫の情報系大学院生（27卒予定）
* サーバーサイド大好き人間
* 趣味はハッカソン
* バックエンドエンジニア志望
* 長期インターンにてSpring Bootを使用（半年以上）

※初記事になるため、温かい目で読んでいただけると幸いです。


## 背景と目的

多くのチュートリアルや入門書では、Controller層にビジネスロジックを直書きしてしまいがちですが、実務ではそれではスケーラビリティに欠け、保守が困難になります。本記事では、長期インターンで学んだ設計観点として、以下を紹介します：

* ロジックの粒度分割
* DTOの導入
* リダイレクトやバリデーションの設計指針


## ロジックの粒度分割：Controllerは「薄く」

Controller層では、受け取り・委譲・返却の3点に集中させ、ロジックはServiceに任せます。こうしたサービス層との責務分離は、単一責任の原則（SRP）の観点から重要です。

```java
@GetMapping("")
public String list(Model model) {
    // Serviceからスタッフ一覧を取得
    List<StaffDto> staffs = staffService.findAll();

    // Viewに渡すため、Modelにデータを格納
    model.addAttribute("staffs", staffs);

    // staff/list.html を表示
    return "staff/list";
}
```
> ControllerはService呼び出しの「仲介役」に徹し、ロジックは書かない。


## DTOの導入：出力形式の明示と安定化

EntityをViewに直接渡すのは避け、DTOに変換して表示用に使います。

```java
public class StaffDto {
    private String name;
    private String email;
}
```
> Entityの構造に依存しないことで、将来的な拡張・変更に強くなる。

### ※ DTO / Form / Entity の役割と違い

| 種類     | 役割        | 使用場所              | 備考              |
| ------ | --------- | ----------------- | --------------- |
| Entity | DB構造      | Repository層       | JPAと密結合         |
| DTO    | 出力表示用     | Controller → View | セキュリティ・公開制御にも有効 |
| Form   | 入力バインド・検証 | View → Controller | バリデーション担当       |

### NGパターン：DAOをControllerで直接呼び出す

```java
@GetMapping("")
public String list(Model model) {
    List<Staff> staffs = staffRepository.findAll(); // NG
    model.addAttribute("staffs", staffs);
    return "staff/list";
}
```
> ViewにEntityを直渡しすると、仕様変更に弱く、セキュリティリスクも高い。


## リダイレクトとバリデーションの設計指針

リダイレクトでは、Flash Attributeで一度限りのメッセージを表示できます。

```java
@PostMapping("/create")
public String create(@ModelAttribute StaffCreateForm form, RedirectAttributes redirectAttributes) {
    // フォームデータをService層に渡して新規登録処理
    staffService.create(form);

    // Flash Attributeを利用して、一度限りの成功メッセージをリダイレクト先に渡す
    redirectAttributes.addFlashAttribute("successMessage", "登録完了");

    // 一覧画面へリダイレクト（PRGパターン：Post-Redirect-Get）
    return "redirect:/staffs";
}
```
> `RedirectAttributes.addFlashAttribute()` は `addAttribute()` のようにクエリパラメータにはならず、よりセキュアに情報を渡せる。

また、バリデーションについて、Springでは `@Valid` と `BindingResult` を使ってバリデーション制御ができます。

```java
public class StaffCreateForm {
    @NotBlank  // 空文字・nullを拒否
    private String name;

    @Email     // メールアドレス形式をチェック
    private String email;
}

@PostMapping("/create")
public String create(@Valid @ModelAttribute StaffCreateForm form,
                     BindingResult result,
                     RedirectAttributes redirectAttributes) {
    // 入力エラーがある場合、フォーム画面を再表示
    if (result.hasErrors()) return "staff/form";

    // バリデーションOKなら、Service経由で登録処理
    staffService.create(form);

    // Flashメッセージをセットし一覧画面へリダイレクト
    redirectAttributes.addFlashAttribute("successMessage", "登録完了");
    return "redirect:/staffs";
}
```
> バリデーションに失敗したら同じ画面を返し、成功時はリダイレクトする構成でUXと安全性を両立する。


## ※ テスト性向上の実践例（MockMvc）

###  Controller単体テスト

```java
@WebMvcTest(StaffController.class)
class StaffControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private StaffService staffService;

    @Test
    void testList() throws Exception {
        // Serviceの戻り値をMockで定義
        when(staffService.findAll()).thenReturn(List.of(new StaffDto("山田", "yamada@example.com")));

        // GET /staffs にアクセスし、ビューやモデルの検証を行う
        mockMvc.perform(get("/staffs"))
            .andExpect(status().isOk())
            .andExpect(view().name("staff/list"))
            .andExpect(model().attributeExists("staffs"));
    }
}
```
> `@WebMvcTest` はController層だけをテスト対象にし、他の依存は `@MockBean` で注入してテストできる。


## まとめ

* Controllerは「薄く」保ち、Service層にロジックを集約
* DTO / Form / Entityの役割を明確に
* バリデーションやリダイレクトを設計に組み込む
* ＋ テスト性も見越したシンプルな構成を意識する

## おわりに

今後も、実務で得た細かい知見をTipsとして紹介していきます。質問や改善のフィードバックがあれば、ぜひコメントで教えてください！