はじめに
Spring Bootでの長期インターンで得た経験をもとに、Controller層の設計と分離に関するTipsを紹介します。スタッフを管理するようなシステムを例に、「可読性・再利用性・保守性」を意識したコーディングと、実務で用いられていた典型的な工夫を紹介します(あくまでインターン生として感じた工夫です)。
自己紹介
- 商学部出身
- 兵庫の情報系大学院生(27卒予定)
- サーバーサイド大好き人間
- 趣味はハッカソン
- バックエンドエンジニア志望
- 長期インターンにてSpring Bootを使用(半年以上)
※初記事になるため、温かい目で読んでいただけると幸いです。
背景と目的
多くのチュートリアルや入門書では、Controller層にビジネスロジックを直書きしてしまいがちですが、実務ではそれではスケーラビリティに欠け、保守が困難になります。本記事では、長期インターンで学んだ設計観点として、以下を紹介します:
- ロジックの粒度分割
- DTOの導入
- リダイレクトやバリデーションの設計指針
ロジックの粒度分割:Controllerは「薄く」
Controller層では、受け取り・委譲・返却の3点に集中させ、ロジックはServiceに任せます。こうしたサービス層との責務分離は、単一責任の原則(SRP)の観点から重要です。
@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に変換して表示用に使います。
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で直接呼び出す
@GetMapping("")
public String list(Model model) {
List<Staff> staffs = staffRepository.findAll(); // NG
model.addAttribute("staffs", staffs);
return "staff/list";
}
ViewにEntityを直渡しすると、仕様変更に弱く、セキュリティリスクも高い。
リダイレクトとバリデーションの設計指針
リダイレクトでは、Flash Attributeで一度限りのメッセージを表示できます。
@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
を使ってバリデーション制御ができます。
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単体テスト
@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として紹介していきます。質問や改善のフィードバックがあれば、ぜひコメントで教えてください!