4層かクリーンアーキテクチャかは、使うDBが決める
はい、どうもこんにちは佐藤です!
先日、第二の故郷である X を眺めていたところ、クリーンアーキテクチャに対する賛否が話題となっていました。
なんか、この 2 年くらいで。
ストアドアーキテクチャ、4 層レイヤード、オニオンアーキテクチャ(クリーンアーキテクチャ)と駆け抜けてしまったので、思ったことを述べていこうかなと。
このブログでは以前、AI時代だからこそ、ドメイン駆動開発とクリーンアーキテクチャが重要 という記事を書きました。今もその考えは変わっていません。ただ、3 世代を通しでやってみると、条件がはっきり見えてきたのです。
4 層からオニオンへの変更は、モックテストをやるかどうかでしかなかった。
そしてそのモックテストに価値があるかどうかは、使っている DB が決めます。私の結論はこうです。SQL Server なら 4 層で十分。Postgres ならオニオンにする価値がある。
先に一つだけお願いを。ここに出てくる判断は、チームのみんなで決めて、正解に持っていった話です。誹謗中傷は許しません。当時の制約の中で全員が納得して出した答えとして読んでくださいね。
では、3 世代を順番に振り返っていきますね!
第 1 世代:ストアドは速い。ただし読めない
4 層の案件よりも前にやっていた、保守案件の話から始めます。
2010 年代まで流行っていた、ストアドプロシージャ中心の作りでした。日本には、こういうシステムありますよね?
お客様環境であったため、初期の作成からいじることができません。問題はあるのだけれど仕組みを変えられず、メンテがやりにくい PJ でした。
まず良いところから。ストアドを経由しているので、SQL のパワーがそのまま使えます。書き方が明らかに違うところ以外は、パフォーマンスが非常に良い。ここは今でも素直に強みだと思っています。
では、何が辛かったのか。
ロジックがストアドの中に入ってしまって、可読性が非常に悪い。 SQL の IIF がネストされると、本当に読めたものではありません。SQL なのでデバッグも難しいです。WITH 句で切り出していけば良いのだろうけど、10 年もののソースコードなので、そんなこともなく。
そして何よりも、本番リリースの難しさでした。
開発・検証・本番と、ストアドを揃えないといけません。そのチェックだけで工数を持っていかれていました。
「マイグレーションを入れろよ」「ツールを使えよ」「DB を繋げてバケツリレーのようにしろよ」と思うかもしれません。わかります。でも、クライアントの環境ですし、本番で動いてしまっているのです。大きく仕組みを変えることができませんでした。
極めつけがこれ。修正依頼が来たときに、まずこう考えるところから始まるのです。
フロントか? サーバーか? ストアドなのか?
どこに何があるのか、誰にも即答できない状態でした。
第 2 世代:4 層にしたら、どこに何があるか分かるようになった
去年、この共通認識のあるチームで新規案件をやることになりました。
一番困っているストアドをどうにかしよう。そこが出発点です。
ドメイン駆動開発を軸にして、こう分けていきました。
- コントローラー層:入出力の受け口
- アプリケーション層:業務ロジックの本体
- ドメインサービス層:業務知識の置き場
- インフラストラクチャ層:永続化の担当
いわゆる 4 層レイヤードですね。ストアドから見れば、10 年分くらいの進化です。
効果はすぐに出ました。業務ロジックがアプリケーション層に集中するので、どこに何があるのかが非常にわかりやすいのです。前の世代の「フロントか? サーバーか? ストアドなのか?」が消えました。保守性はかなり上がったと思います。
もう一つ大きかったのが、マイグレーションファイルを管理するようになったこと。本番リリースの手間がだいぶ減りました。
悪手ではあるのですが、データマイグレーションもこのマイグレーションファイルに入れています。おかげで、マスタデータの差異による不具合はなくなりました。ちなみに顧客データをマイグレーションファイルに入れていた時には焦りましたが! ここら辺の感覚は育てていかないとなと思っています。
では、辛かったところは何でしょうか。
1:N 問題に苦しめられました。
業務アプリなので修正が入ることが想定できます。速度よりも可読性を重視する思想での 4 層なので、まぁ仕方ないかなと。遅いとクレームがあるところを、虱潰しでやっていきました。
厄介なのは、開発環境ではデータ数も少なくて検知が難しいこと。本番に近いデータ量にならないと表に出てきません。
SQL のパワーが使えないので、パフォーマンスは総合的には低めです。どうしても 1:N が回避できず、ロジックが少ない部分は View も活用する作りにしました。ドメイン駆動の境界づけられたコンテキストにまたがるので、もちろん難しい判断です。
テストコードがない故の不具合も、それなりに出ました。
それでも、デリバリーの速さは改善しています。開発者ファーストの作りなので、開発ストレスがだいぶ低いのですよ。ここは数字よりも、チームの空気で分かる部分でした。
なぜあのとき、オニオンにしなかったのか
さて、ここが本題の入口です。
4 層を選んだ時点で、オニオンアーキテクチャやクリーンアーキテクチャの考え方は当然ありました。どこまでやるのかは、けっこう悩んでいたのです。
やめる決定打になったのは、2 つでした。
1 つ目。テストコードをメンテする構造がなかった。
他社から保守を受け継いだソースコードだったのですが、そこにテストコードを維持する仕組みがありませんでした。チームの状況を見て、テストコードはまだ早いと判断したのです。
テストコードがないなら、どうなるか。インターフェースを用意して、リポジトリを差し替えられるようにする必要がありません。
クリーンアーキテクチャで依存性を逆転させる最大の理由は、実装を差し替えられることです。差し替える先がないなら、それは何のためのインターフェースなのでしょうか。
2 つ目。インターフェースで苦しんだ経験があった。
これまた保守だけ受け継いだ PJ の話です。全てのクラスにインターフェースを噛ませるレガシーがありました。実装が 1 つしかないのに、抽象クラスだけが延々と並んでいる状態。定義を追うだけで疲れます。
そこで苦しんできたチームだったのですよ。だから「やめよう」で全員一致しました。
構造の硬さは、それを支える体制とセットでないと意味がありません。 当時のチームには、まだその体制がなかったということです。
第 3 世代:オニオンにして増えたのは、テストが書ける状態だけ
そして最後に、オニオンアーキテクチャをやりました。
変えた理由は、たった 2 つです。
AI でのコーディングができるようになって、インターフェースを書く労力が下がったこと。 さっき「インターフェースで苦しんだ」と書きましたが、あの苦しみの正体は、書く手間と追う手間でした。書く手間のほうは AI がほぼ消してくれます。
もう 1 つが、不具合の初期検知として、モックテスト程度でもテストコードが欲しかったこと。
では、4 層のデメリットは消えたのでしょうか。
そうでもありません。
- 1:N の問題はそのまま引き継ぎます
- SQL のパワーを使えない問題も、そのまま残ります
- 増えたのは、テストコードでしょうもない不具合がガードできる状態
ここで、こう言いたくなる方もいますよね。「インターフェースの効能は、テストだけじゃないでしょう」と。
はい、その話をします。
「差し替えられる」は、たいてい使わない
インターフェースを挟むメリットとして、必ず出てくるのがこれです。
外部システムや DB との差し替えがやりやすくなる。
理屈としては、まったく正しいです。リポジトリの実装を入れ替えれば、上の層に一切手を触れずに永続化先を変えられます。教科書どおりですね。
ただ、10 年以上この業界にいて思うのですよ。
モジュールや DB だけを差し替える案件なんぞ、そうそうない。
考えてみてください。DB を替えるという話が現場に降りてくるのは、どういうときでしょうか。ライセンス費がきつくなった。サポートが切れた。そもそもサーバーごと EOL を迎えた。
だいたい、そういうタイミングなのです。
そして、その状態のシステムは DB だけが古いわけではありません。言語のバージョンも、フレームワークも、認証の作りも、揃って古いのですよ。DB だけをきれいに差し替えて、はい終わり、とはならないですよね?
そこまでやるなら、フルリプレイスになります。
しかもフルリプレイスなら、リポジトリの実装クラスだけを書き換えて済むわけがありません。テーブル設計から見直しますし、業務要件だって当時と変わっています。インターフェースは、そこを救ってくれないのです。
外部システムとの連携なら、多少は目があります。決済代行を乗り換える、SMS 送信の業者を変える、ストレージを S3 に寄せる。このあたりは実際に起きます。
でも、そのときも素直には差し替わりません。相手の API が返す項目が違いますし、エラーの体系も違います。抽象がそのまま使い回せることは、あまりないのですよ。
結局、「将来差し替えるかもしれないから」は、層を増やす理由になりません。 実装が 1 つしかないインターフェースは、来ない未来のために毎日払っている家賃みたいなものです。
そう考えていくと、残るものは 1 つだけになります。
4 層とオニオンの実質的な差は、モックテストだけ。 依存性の逆転も、リポジトリインターフェースも、DI も、全部モックテストのために入れているのです。
ということは、話はシンプルになりますよね。
モックテストに価値があるなら、オニオンにする価値がある。 逆に、モックテストが当てにならないなら、わざわざ層を増やす意味はありません。
では、その価値は何で決まるのでしょうか。
モックテストの価値は、DB が決める
モックテストの価値は、モックの挙動が実 DB の挙動とどれだけ一致するかで決まります。
ここがズレると、テストは緑なのに本番で落ちます。それはガードではなく、ただの安心料ですよね。
そして一致するかどうかは、アーキテクチャではなく DB 製品の癖が決めるのですよ。
SQL Server だと、けっこうズレる
いま関わっている案件は SQL Server なのですが、モックが通っても実 DB で落ちる場面が何度もありました。
代表的なのが、パラメーターの 2100 制限です。IN 句に ID を並べて一括取得するような処理を書くと、ある日突然ここにぶつかります。モックは配列を受け取って返すだけなので、何個渡そうが平然と緑になるのですよ。
null チェックもうまくいきません。 SQL Server は一意制約に NULL が 1 行しか入りません。標準的な挙動と違うので、モックの世界では通るデータが実 DB で弾かれます。真偽値も bit で、アプリ側の bool とのマッピングにひと癖あります。
じゃあ実 DB でテストすればいいじゃないか、と思いますよね? ここも重いのです。Docker で SQL Server を立てるのはめんどうですし、まともにやるなら AWS 環境の構築が必要になります。ハードルが高い。
つまり SQL Server では、モックがズレるうえに、ズレを実 DB テストで埋める道も塞がっているわけです。
なのに、地方の中小企業では SQL Server が多いのですよ! ここは声を大にして言いたい。
この条件下でオニオンにしても、得られるのは「しょうもない不具合が少し減る」程度でした。それなら 4 層で十分というのが、正直な実感です。
Postgres だと、話が変わる
一方で Postgres はどうでしょうか。
モックと実 DB の作りが一致しやすいのです。
まず、SQL Server の 2100 のような早い段階の壁がありません。バインドパラメーターの上限は桁が 1 つ違うので、業務アプリの現実的な範囲でぶつかることはまずないでしょう。モックで通った一括取得は、実 DB でもそのまま通ります。
NULL や真偽値の扱いも素直です。一意制約の NULL は標準どおり複数行を許しますし、boolean 型がそのまま存在します。アプリ側のドメインモデルと、DB のスキーマが同じ形のままでいられるのですよ。
そして何より、Docker で一瞬で立ちます。
これが効きます。モックテストで大半を高速に回して、モックでは見られないところ、つまり 1:N の発行クエリ数や、実際のインデックスの効き方だけを実 DB のテストで見る。二段構えが現実的なコストで組めるわけです。
こうなると、リポジトリをインターフェースで切り分けた意味が出てきます。同じユースケースに対して、テストではモックを、統合テストでは実 DB 実装を差し込む。依存性の逆転が、ちゃんと仕事をするのです。
Postgres なら、オニオンにする価値がある。少なくとも、私が踏んできた範囲ではそう感じています。
まとめ
というわけで、4 層かクリーンアーキテクチャかは、使う DB が決めるよ!って話でした!
あ。正確には、モックテストが信用できる DB なら、オニオンに振る価値があるって感じですねっ。
アーキテクチャの議論は「どちらが優れているか」の勝負になりがちです。でも 3 世代を駆け抜けてみて腹落ちしたのは、そこではありませんでした。層を増やして得られるものは、テストがどれだけ本番の挙動を再現できるかで決まるのです。来ない差し替えに備えても、構造は強くなりません。裏付ける検証がなければ、ただの抽象クラスの山になります。
とはいえ、Postgres が万能だと言いたいわけではありません。分散が要るなら NewSQL という選択肢もありますし、扱うデータの性質しだいで答えは変わります。そこは案件ごとに考えるところですよね。
私がお願いしたいのは一点だけです。DB を選ぶときに、テストのことも考えてみてほしい。
性能、コスト、運用。DB 選定の評価軸はいくつもありますが、そこに「この DB は、テストで本番の挙動をどこまで再現できるのか」を並べてほしいのですよ。ここを見ておくと、そのあと採れるアーキテクチャの幅が変わります。逆にここを外すと、どれだけ層をきれいに切っても、テストが本番を教えてくれない状態になるのです。
次に層を増やそうとするときは、「この層は、何を差し替えるために作るんだっけ?」と一度だけ聞いてみてください。答えが出るなら、迷わず作っていきましょう!