Spring Boot バリデーション設計で実務レビューされやすいポイント

Spring Bootのバリデーション設計でDTO、Service層、業務ルールを整理する図

バリデーション設計は入力チェックの置き場所で差が出る

バリデーション設計で迷う場面は、Spring Boot実務ではかなり多いです。DTOにアノテーションを書くべきか。Controllerでチェックするべきか。Service層で業務ルールを見るべきか。こうした判断が曖昧なままだと、入力チェックが散らばります。

結論から言うと、Spring Bootのバリデーションは形式チェック、相関チェック、業務ルール、状態チェックを分けて設計します。すべてをBean Validationに寄せる必要はありません。一方で、すべてをService層に丸投げする設計も保守しにくくなります。

この記事では、Java/Spring Bootの実務でレビューされやすい観点を整理します。Request DTO、@Valid、Controller、Service層、業務ルール、エラーレスポンスの分担を扱います。

Spring Bootのバリデーション設計で分けたい4種類

まず、入力チェックを1つの言葉でまとめないことが大切です。実務では、同じ「バリデーション」でも責務が違います。

種類置き場所の目安
形式チェック必須、桁数、メール形式、日付形式Request DTO
相関チェック開始日が終了日以前、どちらか一方は必須DTOのクラスレベル制約、専用Validator
業務ルール同じメールアドレスは登録不可、在庫不足Service層、Domain
状態チェック承認済み注文は変更不可、退会済みユーザーは操作不可Service層、Domain

形式チェックは、APIの入力契約に近いです。そのため、Request DTOに置くと読みやすくなります。一方で、DBや外部APIを見ないと判断できないものは、DTOだけでは決められません。

Spring Boot公式のValidationでは、Bean Validation APIを利用できることが説明されています。標準的な仕組みを使いながら、どこまでをアノテーションで表すかを設計します。

Request DTOには形式チェックを置く

Request DTOには、リクエスト単体で判断できるチェックを置きます。例えば、必須、最大長、最小値、メール形式などです。

public record CreateUserRequest(
    @NotBlank
    @Email
    String email,

    @NotBlank
    @Size(min = 8, max = 72)
    String password,

    @NotBlank
    @Size(max = 50)
    String displayName
) {}

この形にすると、ControllerやService層に基本的な入力チェックを書かずに済みます。Request DTOを見れば、APIが受け付ける入力の前提もわかります。

ただし、DTOのアノテーションを増やしすぎると読みづらくなります。複雑な条件を無理に1つのDTOへ詰め込むより、用途別のRequest DTOに分ける方が自然です。

Controllerはバリデーションを起動する入口にする

Controllerでは、入力チェックの詳細を書くより、Request DTOを受け取り、バリデーションを起動する入口にします。Spring MVCでは、Controllerメソッドの引数に対する検証を扱えます。公式のSpring MVC Validationにも関連する説明があります。

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

  private final UserService userService;

  @PostMapping
  public ResponseEntity<UserResponse> create(
      @Valid @RequestBody CreateUserRequest request
  ) {
    UserResponse response = userService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(response);
  }
}

Controllerにif文が増えると、HTTP、入力検証、業務処理の責務が混ざります。最初は小さなチェックでも、APIが増えると重複しやすくなります。

そのため、Controllerは薄く保ちます。入力形式はDTOへ寄せます。業務判断はService層へ渡します。この分担がレビューでも見られやすいポイントです。

業務ルールはService層やDomainで検証する

次に、業務ルールはRequest DTOだけでは判断できません。例えば、メールアドレスの重複、在庫数、権限、ステータス遷移は、DBの状態やログインユーザーの情報が必要です。

@Service
public class UserService {

  @Transactional
  public UserResponse create(CreateUserRequest request) {
    if (userRepository.existsByEmail(request.email())) {
      throw new BusinessException("USER_EMAIL_ALREADY_EXISTS");
    }

    User user = User.create(
        request.email(),
        passwordEncoder.encode(request.password()),
        request.displayName()
    );

    return UserResponse.from(userRepository.save(user));
  }
}

この例では、メール形式はDTOで確認します。メールアドレスが既に使われているかはService層で確認します。理由は、Repositoryを使った状態確認が必要だからです。

なお、Domainモデルを厚くする設計では、状態遷移や不変条件をDomain側へ寄せることもあります。重要なのは、業務ルールをControllerやDTOに散らばらせないことです。

相関チェックは無理にフィールド単位にしない

開始日と終了日、配送先と請求先、パスワードと確認用パスワードなどは、1項目だけでは判断できません。こうした相関チェックをフィールド単位のアノテーションで無理に表すと、意図が読みにくくなります。

選択肢は大きく2つです。DTOのクラスレベル制約として表す方法があります。もう1つは、Service層に入る前の専用Validatorで明示的に確認する方法です。

Spring Framework公式のJava Bean Validationでは、Bean ValidationをSpringで扱う仕組みが説明されています。実務では、標準の制約で足りるものと、独自制約にするものを分けて考えると判断しやすくなります。

バリデーションエラーのレスポンス設計も合わせて考える

バリデーション設計では、チェックの場所だけでなく、エラーの返し方も重要です。画面がフォームなら、field単位で返せる形式が扱いやすくなります。

{
  "code": "VALIDATION_ERROR",
  "message": "入力内容を確認してください。",
  "errors": [
    {
      "field": "email",
      "code": "Email",
      "message": "メールアドレスの形式で入力してください。"
    }
  ]
}

エラーレスポンスがAPIごとに違うと、フロントエンド側の実装が複雑になります。ReactやTypeScriptと連携する案件では、バリデーションエラーの型も重要です。

また、表示用メッセージと内部エラーコードは分けると扱いやすいです。ログ調査、画面表示、多言語対応、問い合わせ対応で使う情報が違うためです。

実務レビューで見られやすいアンチパターン

レビューでは、動くかどうかだけでなく、変更しやすいかも見られます。特に、入力チェックが複数の層に重複している場合は注意が必要です。

指摘されやすい例起きる問題見直すポイント
Controllerにif文で入力チェックを書くAPIごとに重複しやすい形式チェックはRequest DTOへ寄せる
DTOに業務ルールを詰め込むRepositoryや認可情報が必要になり責務が崩れるService層やDomainへ移す
同じチェックをDTOとServiceで二重に書く仕様変更時に片方だけ漏れるチェックの種類で置き場所を決める
エラーメッセージを直書きする変更、翻訳、テストがしにくいエラーコードとメッセージ管理を分ける
バリデーション例外と業務例外を混ぜるHTTPレスポンスが不安定になる例外種別とレスポンス変換を整理する

特に、バリデーション例外と業務例外の扱いはAPI設計にも関わります。入力形式のエラーなのか、業務上許可できない操作なのかを分けると、HTTPステータスやエラーコードを決めやすくなります。

テストでは境界値と業務ルールを分けて確認する

バリデーション設計は、テスト設計ともつながります。DTOの形式チェックは、Controller層のテストやValidatorのテストで確認できます。一方で、重複チェックや状態遷移はService層のテストで確認します。

  • 必須項目が未入力のときにfield単位のエラーになるか
  • 文字数の境界値で期待どおりに通るか
  • メール形式など標準制約が有効になっているか
  • 重複データがあると業務例外になるか
  • 状態によって許可されない操作を止められるか

ここを分けておくと、テストの意図が読みやすくなります。失敗したときも、DTOの制約が悪いのか、業務判断が悪いのかを切り分けやすくなります。

よくある質問

バリデーションはすべてDTOに書くべきですか?

すべてDTOに書く必要はありません。必須、桁数、形式のようにリクエスト単体で判断できるものはDTOに向いています。一方で、DB状態、権限、ステータス遷移が必要な判断はService層やDomainに置く方が自然です。

Controllerで入力チェックを書いてはいけませんか?

絶対に書いてはいけないわけではありません。ただし、Controllerに入力チェックが増えると、HTTP処理と業務判断が混ざります。まずはRequest DTOとBean Validationで表せないかを検討します。

業務エラーは400で返すべきですか?

プロジェクトのAPI設計方針によります。入力形式のエラー、権限エラー、リソース状態によるエラーは意味が違います。HTTPステータス、エラーコード、表示メッセージのルールを先に決めることが重要です。

まとめ

Spring Bootのバリデーション設計では、入力チェックを1か所に押し込めないことが大切です。形式チェックはRequest DTOに置きます。相関チェックはクラスレベル制約や専用Validatorを検討します。業務ルールや状態チェックはService層やDomainで扱います。

また、エラーレスポンスの設計も合わせて考えます。field、code、messageを整理しておくと、フロントエンドとの連携や障害調査が進めやすくなります。

Java/Spring Bootの実務では、バリデーション設計、DTO設計、API設計、例外処理、テスト設計がつながります。bluenaの採用情報では、Spring Boot経験を活かせる案件相談も行っています。今の経験を次の環境でどう伸ばすか、一度整理してみてください。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア