Spring Bootの入力チェックをどこでやるかを解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間でControllerとServiceの役割を学ぶイラスト。

Spring

Spring Bootの入力チェックはどこでやる?ControllerとServiceの役割

AIによる要約

入力チェックは、すべてControllerでif文を書くものではありません。必須、桁数、形式のようなチェックはFormやRequest DTOのBean Validationに寄せ、重複確認、権限、状態遷移のような業務ルールはServiceで判断する、と分けると現場コードが読みやすくなります。

新人SE
新人SE
入力チェックはControllerにまとめて書く方が分かりやすくないですか?
ポンコツSE
ポンコツSE
一見分かりやすいですが、業務ルールまでControllerに入れると肥大化します。形式チェックと業務チェックを分けると、修正範囲が追いやすくなります。

Spring Bootで登録画面やAPIを作ると、入力チェックをどこに書くかで迷います。

Controllerで全部if文を書く、Formのアノテーションだけに任せる、Serviceでまとめて見るなど、複数の選択肢があります。現場では、どのチェックをどこに置くかがレビュー対象になります。

この記事のポイント

  • 必須、桁数、形式はBean Validationに寄せる
  • 重複、権限、状態などの業務チェックはServiceで見る
  • Controllerは入力エラー時の画面/API応答を制御する
  • チェックの場所を分けるとレビューで説明しやすい

入力チェックには種類がある

入力チェックと一言で言っても、実際には種類があります。必須、桁数、形式のようなチェックと、業務ルールに関するチェックは分けて考えます。

  • 形式チェック: 必須、最大文字数、メール形式、数値範囲
  • 業務チェック: メールアドレスの重複、注文状態、権限、締切日

この2つを同じ場所に全部書くと、ControllerやServiceが読みづらくなります。

形式チェックはFormやRequest DTOに寄せる

必須、桁数、形式のようなチェックは、Bean ValidationでFormやRequest DTOに書くのが分かりやすいです。

public class UserRegisterForm {
    @NotBlank
    @Size(max = 50)
    private String name;

    @NotBlank
    @Email
    private String email;
}

このようなチェックは、業務ロジックというより入力の形の問題です。Formに書くことで、Controllerではエラーの有無を見て画面に戻せます。

Controllerはエラー時の戻し先を決める

Controllerの役割は、入力値を受け取り、形式チェックの結果を見て、次に進むか画面へ戻すかを決めることです。

@PostMapping("/users")
public String register(@Valid UserRegisterForm form,
                       BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "users/new";
    }

    userService.register(form.toCommand());
    return "redirect:/users";
}

ここで重複確認や権限判定までControllerに書き始めると、Controllerが業務ロジックの置き場になってしまいます。

業務チェックはServiceで行う

メールアドレスが既に使われているか、注文をキャンセルできる状態か、ログインユーザーに権限があるか。こうしたチェックは業務ルールなのでServiceに置くのが自然です。

@Service
public class UserService {
    public void register(UserRegisterCommand command) {
        if (userRepository.existsByEmail(command.getEmail())) {
            throw new DuplicateEmailException(command.getEmail());
        }

        User user = new User(command.getName(), command.getEmail());
        userRepository.save(user);
    }
}

このチェックは、画面から登録してもAPIから登録しても必要です。だからControllerではなくServiceに置く方が再利用しやすくなります。

Controllerに業務チェックを書くと何が困るか

@PostMapping("/users")
public String register(@Valid UserRegisterForm form,
                       BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "users/new";
    }

    if (userRepository.existsByEmail(form.getEmail())) {
        bindingResult.rejectValue("email", "duplicate", "既に使われています");
        return "users/new";
    }

    userService.register(form);
    return "redirect:/users";
}

このコードは一見親切ですが、ControllerがRepositoryを直接見て業務ルールを判断しています。API版の登録処理を作ったとき、同じ重複チェックをもう一度書くことになります。

Serviceの例外を画面でどう扱うか

Serviceで業務チェックを行う場合、Controller側で例外を受けて画面に戻す設計もあります。プロジェクトによっては、専用の例外ハンドラでまとめることもあります。

try {
    userService.register(form.toCommand());
} catch (DuplicateEmailException e) {
    bindingResult.rejectValue("email", "duplicate", "既に使われています");
    return "users/new";
}

return "redirect:/users";

ここで大事なのは、重複かどうかを判断するのはService、画面にどう表示するかを決めるのはController、という分担です。

現場レビューでの見られ方

// レビューコメント例
メール重複チェックは業務ルールなのでService側に寄せたいです。

// レビューコメント例
ControllerでRepositoryを直接呼んでいるため、責務が混ざっています。
Serviceにメソッドを用意してControllerは結果の戻し先を決める形にしましょう。

レビュー前のセルフチェック

確認リスト

  • 必須・桁数・形式チェックをFormやRequest DTOに書けているか
  • 重複・権限・状態チェックをControllerに書きすぎていないか
  • Serviceに置くべき業務ルールをControllerで判断していないか
  • 画面とAPIで同じ業務チェックを重複実装していないか

この記事とあわせて読みたい

まとめ

入力チェックは、Controllerに全部書けばよいものではありません。形式チェックはFormやRequest DTOのBean Validationへ、業務チェックはServiceへ置くと責務が整理されます。

新人のうちは、チェック内容を見たときに「入力の形の問題か」「業務ルールの問題か」を分けるだけで、Spring Bootの実装判断がかなり安定します。

-Spring
-, , , , ,