バリデーションの実装はどこにすべきか?


前書き

どこに実装すべきかって自明じゃない?

私もそう思ってた.しかし,どうやらかなり色々な宗教があるようで,自分で実装したり,調べていくうちになかなか複雑な事象に直面した.
そのようなわけで,各言語,各フレームワークにおいて,バリデーションがどのような扱いになっているか?どのような宗教・派閥があるか?をまとめてみたので,紹介したい.

バリデーションとは何か?

そもそも論として,バリデーションとは何か?という疑問がある.自分はサーバーサイドエンジニアなので,GET /v1/users/12345などとみると,あぁ.12345というのがユーザーのIDで,おそらく「ユーザーIDは0以上の整数」のバリデーションが入っているんだろうな.と思う.実際のところ,この発想は間違っている場合がある.

これが話をややこしくさせる原因ではあるが,バリデーションには2つの定義が存在する.
これがすっきりとして書かれていたのが,TERASOLUNAのドキュメントの「4.1. 入力チェック」である.

ユーザーが入力した値が不正かどうかを検証することは必須である。 入力値の検証は大きく分けて、

  1. 長さや形式など、文脈によらず入力値だけを見て、それが妥当かどうかを判定できる検証
  2. システムの状態によって入力値が妥当かどうかが変わる検証

がある。

「入力チェック」という題名だが,URLにはしっかりとValidationの文字が入っている.
前節で「その発想は間違っている場合がある」と書いたのは,このあたりで,バリデーションという言葉が「ユーザーID 1234というユーザーが存在するか?」というもの(2.の例)を指している場合がある.
最初からで申し訳ないが,このあたりから前提が崩れている場合がある.
他人が「バリデーションを実装してほしい」と言った場合,以下の可能性がある.

A. 1.のバリデーションを実装を依頼される.
B. 2.のバリデーションを実装を依頼される.
C. 1.と2.の区別がついておらず,1.と2.が混ざって実装を依頼をされる.

という3パターンがある.C.の場合,このバリデーションの概要説明から入ったほうがよい.そして,大体の場合,複数のカラムのバリデーション実装を依頼されるので,それが,1.にあたるのか,2.にあたるのか.それを分別して,別タスクとして切り出して,計画するほうが良いと感じている.残念ながら,1.と2.を同じ層でするとコードが泥沼化する可能性がある.
今回,この文章の文脈で語るバリデーションは1.で「入力の値のみで判断できる内容」をバリデーションと呼ぶ.

サンプルのアーキテクチャ

今回,どの言語がどのレイヤーにバリデーションを実装しているか?というのを説明するために,以下のようなアーキテクチャの図を参考に説明する.

ざっとした役割の説明をする.
Controller層は実際にリクエストを受け付けるクラス.Springであれば,@RestControllerというアノテーションがついているクラスであったり,Flaskであれば,@app.route('/')というデコレータがついている関数を想像してもらえばよい.
Service層は業務ロジック層である.これはかなり議論を呼ぶので,詳細には語らないが,ドメイン層でいわゆる入力や出力に依存しない,ロジックを扱う層である.人によってはApplicationと呼んだり,クリーンアーキテクチャの文脈では,ApplicationBusinessRule層と呼ばれたり,単にUseCaseと呼ばれたりする.
Model層はデータの保存を扱う層として,ここでは扱う.これも議論を呼ぶが,RailsのActiveRecordの層や,RepositoryパターンにおけるRepositoryの層を指すものだと考えてほしい.

Controller層へ実装

代表的なフレームワーク

  • Spring
  • Laravel

概要

有名なライブラリでも採用されており,リクエストする入り口の方から不正な値を守り,ロジック層に不正な値を持ち込まないようにする方式.
例えば,Spring Boot エンティティー精査より引用すると,

public class ValidateExampleEntity {

    @NotNull
    public String value1;

    @Digits(integer = 3, fraction = 1)
    public String value2;
}

という形で,リクエストされたjsonのエンティティに対し,バリデーションをかける方式.もしくは,「Spring BootでつくったAPIのリクエストのバリデーションで出るExceptionのまとめ」にあるような

@Validated
@RestController
@RequestMapping("/v1/verify")
public class ValidationVerificationController {

    @PostMapping("/{path}")
    public String verify(@PathVariable(value = "path") @Valid @Size(max = 3) String path, // ①パスのパラメータ
                       @Valid @Size(max = 3) @NotNull String arg, // ②引数で指定するケース
                       @Valid ValidationVerificationGetRequest validationVerificationGetRequest, // ③引数でDTOを指定するケース
                       @RequestBody @Valid ValidationVerificationBodyRequest validationVerificationBodyRequest // ④引数で@RequestBodyをつけてDTOを指定するケース(JSON)
    ) {
        return "ok";
    }
}

ControllerクラスでPathに含まれる変数に対し,アノテーションでバリデーションを指定する場合もある.

メリット

  • フレームワークでバリデーション機能を提供されていることが多いので,開発速度が出やすい.
  • APIの仕様ごとに作れるので,分散開発しやすい.

デメリット

  • 実装箇所が分散する.
  • フレームワークに依存する.
  • (フレームワークの実装によるが)テストしにくい.
  • テストコードが多くなる.

Model層へ実装

ActiveRecord型

代表的なフレームワーク

  • Ruby on Rails
  • Django

一番有名なものとしてはRuby on RailsのActiveRecordでしょうか.Railsの「Active Recordバリデーション」では以下のような例が書かれています.

class Person < ApplicationRecord
  validates :name, :login, :email, presence: true
end

Personクラスのname,login,emailという属性は「空が不許可」というバリデーションが付与されています.このような形でフレームワークと密な形で実装されるバリデーションがあります.

メリット

  • バリデーションの実装が分散しない
  • テストしやすい

デメリット

  • ライブラリに依存する
  • (ActiveRecord由来かもしれないが)バリデーションが複雑化しすぎる

ValueObject型

一方で,フレームワークと疎な形で実装されるバリデーションもあります.ドメイン駆動開発で取り上げられるValue Object.値オブジェクトと呼ばれるデザインパターンがあります.
分かりやすい例であれば @nrslib さんのボトムアップDDDにC#の例があります.

public class UserName : IEquatable<UserName> {
  private readonly string name;

  public UserName(string name) {
    if (string.IsNullOrEmpty(name)) {
        throw new ArgumentNullException(nameof(name));
    }
    if (name.Length > 50) {
        throw new ArgumentOutOfRangeException("It must be 50 characters or less", nameof(name));
    }
    this.name = name;
  }

  public string Value { get { return name; } }

  public bool Equals(UserName other) {
    if (ReferenceEquals(null, other)) return false;
    if (ReferenceEquals(this, other)) return true;
    return string.Equals(name, other.name);
  }

  public override bool Equals(object obj) {
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((UserName) obj);
  }

  public override int GetHashCode() {
    return (name != null ? name.GetHashCode() : 0);
  }
}

このような形でユーザーの名前部分を一旦クラスでラップし,その内部的にバリデーションをする.といった方式があります.

メリット

  • ライブラリに依存しない
  • テストしやすい

デメリット

  • 実装量が多い
  • 個々人により実装方式にばらつきがでる.
  • ライブラリの提供されているものを車輪の再発明してるんじゃない?と言われ,合意を取りにくい.

Service層へ実装

これはどのライブラリが.というわけではないのですが,私が見たことある実装で,サービス層にバリデーションが実装されている例です.これをServiceと呼んでいいのか疑問があると思いますが,いわゆる業務ドメインの知識がライブラリ化されている.と思って,見てください.そうしたときに,API側のControllerのコードからではなく,運用スクリプトから機能を参照したい場合があります.そうしたときに,運用スクリプトにはバリデーションを実装したくない.といった要求も生まれます.そうしたときに,この中間層であるService層へバリデーションを組み込むのがベターになる選択肢が生まれます.これModel層のバリデーションで良くね?と思われるかもしれませんが,たまにこういうことが生まれます.
例えば,PHPのLaravelでWebサービスを提供しています.基本的に,LaravelではController層でバリデーションをするのが一般的なようです.しかし,このような運用スクリプトも楽に開発したい.そうしたときに,一旦LaravelのController層でのバリデーションを諦め,別の場所に実装することを考えます.そうすると候補となるのが,Service層かModel層になります.しかし,DB周りに使われるEloquentがモデルのバリデーションに対応していないため,traitを使って無理やり介入するか,Service層で実装する選択肢になります.個人的には,メタプログラミングの類で介入するとライブラリの挙動変更で辛い思いをすることもあるので,後者のService層でバリデーションを選んでしまう感じですね・・・本当はここにも突っ込みどころはあって,なんでDBに入っているModelの定義と業務ドメインのModelが同一なの?そこからして設計がおかしくない?みたいなこともあるのですが,往々にしてそういう実装になっていることもあって・・・

メリット

  • 運用スクリプトが簡単に書ける.
  • 分散開発しやすい.

デメリット

  • ライブラリのバリデーション機能を利用できない.
  • バリデーションの実装が分散する.
  • テストコードが多くなる.

個人的なオススメ順

  1. ValueObject型
  2. Controller型
  3. ActiveRecord型
  4. Service型

個人的にはValueObject型が推しです.実装もシンプルですし,バリデーションのロジック自体は業務ドメインに近い場所にあると思うので,その周りに実装してあったほうが良い.と思います.サービスとして一番ヤバいのが,DBの不整合であったり,不正な値でDBを書き換えることで,それらがあるとサービス運営が不能になるような重篤なバグになりえます.これらのバリデーションをController層でやると抜け漏れがちですし,水際で守れるValueObjectは有用だと感じています.しかし,チームで組む際には,実装方式に齟齬があったり,車輪の再発明感があるので,厳しいかもしれません.また,Eloquentのような,ライブラリによっては,そもそもValueObjectを作ることが難しい場合もあります.
一方で,Controller型は,ライブラリの恩恵も受けられ,なじみやすいと思います.また,近年ではマイクロサービスアーキテクチャの流れもあり,そもそもAPIが小さい場合は,繰り返しがそんなに苦でなかったり,バリデーションの機能を一旦集約してしまい,それを使いまわすことで重複を回避する方法もないわけではないです.しかし.テストが作りにくい.もしくはテストがフレームワークに強く依存して書きにくかったり,ナレッジが少ないといった経験が自分にはあります.
ActiveRecord型はValueObject型っぽいですがライブラリへの依存が強いです.その代わり,開発速度が出るため,初期の実装には良かったりしますし,割とORMまわりも面倒を見てくれることも多いので,1粒で2度おいしい.みたいな状況になります.個人的には,バリデーションのルールがカオス化しすて,リプレイスするときに仕様を起こし直すのが大変じゃないかな・・・と思います.
Service型は,正直オススメしないです.ただ半分折衷案みたいなやり方があって,ValueObjectと併用するパターンがあります.Serviceクラスの関数の引数をクラスのインスタンスにしてしまい,そのクラスをValueObjectとして実装することで,引数のバリデーション機能をValueObject側に寄せることができます.そして,UserNameValidatorみたいなクラスでバリデーションの機能自体も別だしにすることで,バリデーション機能の実装の重複も逃れることができます.そこまでやる必要ある?と言われるとなかなかしんどいですが・・・

感想

今回調べてみて,バリデーションって割と簡単に扱われがちではあるけれども,意外に難しくないかな.と思いました.バリデーションの機能実装するのは,実際それほど難しいものではないです.しかし.チームで作る場合,「バリデーションの機能は何を指しているか?」「メンバーがどのフレームワークを使ってきたか?」によって,「バリデーションの認識が異なる.」といった状況が生まれるんだな.ということを痛感しました.そして,そもそも「バリデーションに対する意識の差があることを認知していない」という場合もなかなか混乱を招く原因になりそうです.これは自分の中では問題意識として大きくて,「お前らがModelと呼ぶアレをなんと呼ぶべきか。近辺の用語(EntityとかVOとかDTOとか)について整理しつつ考える」とかを見ると,発狂します.実際,リンク先にあるような話で「Entityって何ですか?」を同僚に聞いた時,あーこの人の言ってるEntityはORMっぽいやつだ.となった記憶があります.結構,認識が異なってたりします.そして,問題自身に興味を持ってもらえなくて,なぁなぁでやってしまう場合もあります.
人間が経験できるフレームワークの数なんてたかが知れてますし,ましてや最近はその設計思想の深い部分を知らずとも,Qiitaの記事をピックするだけでサービスが作れるご時世です.なんならRuby on Rails使っていてもController層でバリデーションする.なんていうパワープレイをしている人もいたりすると思います.意外に「フレームワークを適切に使いこなす」ってのもハードルが高いことです.SpringBootとか完全に理解できる人いるんですかね?
そんなわけで,私としては「バリデーションってどこでやるべき?」とか,「どういう実装方式がある?」といった時に迷ったり,チームの認識を合わせたり,自分たちの使っているフレームワークの思想を考えたりする際にこのドキュメントが判断の一助となればいいな.と思っています.