プレゼンテーションレイヤ、いわゆるフロントエンドがクライアントサイドで実装・実行されるアーキテクチャ (注 1) において、管理画面/管理機能をあとから追加する際にどのような実装パターンがあるのかを整理してみます。
注 1: Presentation Domain Separation の実践の中でも、物理的にプレゼンテーションロジックとドメインロジックを分離しているアーキテクチャです。
用語の整理
プレゼンテーションレイヤ
三層アーキテクチャにおける、システムの利用者へユーザインターフェイスを提供する層です。本記事では"フロントエンド"とほぼ同義で使います。
OSI 参照モデルの第六層ではないです。
バックエンド Web API とは
プレゼンテーションを持たない Web API (HTTP プロトコルを用いてネットワーク越しに呼び出すアプリケーション) とします。
プレゼンテーションレイヤを担うフロントエンドには JavaScript の SPA や iOS / Android などのモバイルがいたり、IoT デバイスがいたりするやつです。
ちなみにクライアントサイドとフロントエンド、サーバサイドとバックエンドが混同されるシーンがたまに見られますが、本記事は以下の定義に従って記述します。
https://speakerdeck.com/koichik/isomorphic-survival-guide?slide=12 より引用
バックエンド Web API の例をサーバサイドの Web application framework と絡めて言えば、Rails でAPI mode を ON にしている API サーバとか、Django REST framework 使っているやつとか。近年増えているらしき Golang でのサーバサイド開発ももっぱらバックエンド Web API である印象をうけます。
細かい事を言うと、人間向けではなく対システム向けに構造化されたデータを出力する API の場合、構造化データの生成部分をプレゼンテーションレイヤと考えることもありますが、簡単のため本記事の埒外としておきます。
管理画面/管理機能
管理画面/管理機能とは、Web API が接続するデータストアの内容を参照/更新するアプリケーションを指します。事業の顧客ではなく内部ステークホルダーが利用するものです。
余談ですが、管理画面というとどうしてもブラウザで閲覧する Web アプリケーションやデスクトップアプリケーションぽいイメージがつきまとう気がしていますが自分だけでしょうか。iOS / Android プラットフォームで動く native apps でもよいしなんなら CLI ツールでも Slack app でもよいわけで、ざっくり管理機能と呼んだほうが適切かなと思っています。
追加する、とは?
新規サービスに携わったことがある方々は分かると思いますが、サービス初期は素早くユーザーに価値を提供し事業を成立させることが最優先であるため、どうしても管理画面をはじめ運営用ツールの開発は後回しになりがちです。
最初は管理機能なしで API サーバとモバイルアプリだけをリリースし、運用で戦いながら管理機能を作っていくというパターン。あると思います。
管理機能がない状況ではその用途に特化した GUI アプリケーションではなく CLI か何かの開発者向けツールで DB を参照/更新することがほとんどだと思いますので、管理機能を追加する前の全体の構成としてはこんな感じになります。
アーキテクチャパターン集
さて、本題の管理機能をシステムコンポーネントとして追加する実装パターンの話です。派生はいくつかありますが以下の 3 パターンが思いつきます。
これから個々に説明していきます。予め言っておくと単一にして絶対な正解などないので状況や文脈にあわせて選択するものだと考えています。
1. バックエンド API サーバにプレゼンテーションレイヤを追加する
バックエンド API サーバに管理機能向けのプレゼンテーションレイヤを追加するパターンです。以下の図では管理機能向けのフロントエンドをサーバ側に足しています。
Rails でいえば ERB, Slim, Haml などの view template を使って管理機能ユーザー向けの HTML をサーバサイドで生成するイメージです。
管理機能の view をモバイルで提供する場合には、サーバサイドで生成された HTML を WebView でレンダリングすることになります。 (view をクライアント側に実装するなら後述の 2 のパターンになるため)
Good: 実装コストが抑えられる
おおよその場合には最小コストで実装できるパターンだと思います。
- たいていの Web application framework には template engine やドメインロジックと view を繋ぐ便利 helper が用意されている
- 元々のバックエンド developer のスキルセットで対応しやすい
- CI/CD、インフラ等々の変更が少ない
また、Web application framework を持つような言語には大抵 admin 系のライブラリが用意されています (Rails Admin, administrate, active admin, django admin, Go admin etc.)。バックエンド開発者のスキルの延長線上の技術スタックを使いつつ、HTML/CSS マークアップも含めて開発工数を削減できるこれらのライブラリを素直に利用できるのはこのパターンのみでしょう。
ただ、admin 系のライブラリはブートストラップの局面では便利ですが、デフォルトの機能やデザインをカスタマイズしたくなったり、機能の一部を分離したくなったときに足かせになったり、拡張性・保守性の問題に高確率で向き合うことになります。そのため、この点についても Bad と捉える方もいるかもしれません。
Bad: Presentation Domain Separation のメリットを失う
最も大きい難点は物理的に Presentation Domain Separation が実現できているアーキテクチャを崩すことになる点です。
ドメインロジックに集中していた Web API に view 関連のコードが入り込むので、論理的な分割が必要になります。たいていフォルダなどのレベルではレイヤがきちんと分けられると思います。
しかし view 向けのロジックがモデル層に入り込んだり、view 向けには想定していなかったロジックがうっかり再利用される事態を完全に排除するのは難しいです。フレームワーク自体が密結合な設計・実装を推すようなつくりとなっている場合はなおさらです。論理的な分割を維持し続けるにはプログラマとチーム両方の練度が求められると感じます。
さらに Presentation Domain Separation で得られるメリットの裏返しで、テスタビリティの低下、プレゼンテーションのための依存関係またはプレゼンテーションのテストのための依存関係の追加の必要も発生します。
その他にも管理機能向けにリッチなフロントエンドの要求が生じてくると view template やフレームワーク標準のフロントエンドのサポートでは厳しくなってきます。
密結合を意識的に選択できるならあり
とはいえこれまで何度も議論が繰り返されているように「密結合=悪、疎結合=善」というわけではありません。管理機能に何を求めるかは事業・プロダクトの性質次第ですし、プレゼンテーションとの密結合、顧客向け機能周辺のコードとの密結合を意識的に選択できるならありな選択肢です。
人類は密結合を求めている
分業不要な規模でかつ性能問題が無い ならば常に密結合を選んできたのが人類です。 https://zenn.dev/koduki/articles/3f5215f2a79843#%E4%BA%BA%E9%A1%9E%E3%81%AF%E5%AF%86%E7%B5%90%E5%90%88%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6%E3%81%84%E3%82%8B
密結合にしても問題になりにくい状況だったので、これと引き換えにスタートアップでは最も重要な開発速度を出せる設計にした
https://speakerdeck.com/yasaichi/what-is-ruby-on-rails-and-how-to-deal-with-it?slide=41
2. バックエンド API サーバを利用するクライアントアプリケーションを作る
バックエンド API にプレゼンテーションレイヤを持たせず、管理機能専用のフロントエンドを作るパターンです。
先述の通り管理機能向けフロントエンドのプラットフォームを限定する必要はないのですが、現実的には Web ブラウザであることが大半だと思います。一般的な理由は以下です。
- モバイルアプリのようなリッチな体験が求められるシーンが少ない
- モバイルアプリ開発のほうが開発・運用コストがかかりがち (特にマルチプラットフォームの場合に顕著)
- 技術スタックがバックエンドと分離する可能性が高い
管理機能用のネイティブアプリを開発するぐらいなら事業のコアである顧客向け機能の開発にネイティブエンジニアの力を注ぎたい、というのは当然ですね。
なので以下のメリット/デメリットは、Web ブラウザで動作する Single Page Application (以下 SPA) を作ることを前提として話を進めます。
Good: Presentation Domain Separation の維持
パターン 1 とは対象的に、Presentation Domain Separation を維持できます。バックエンド API は複数のフロントエンドを持つことになりますが、プレゼンテーション層のコードが入り込むことはありません (アクロバティックな実装をすればもちろん可能ですが)。
プレゼンテーション関連の依存関係の追加も必要ありません。
SPA 開発は高コストという言説が一時期見られた気もしますが、実態としては年々かんたんになっていると個人的に思っています。特に管理画面の作成に限って言えばその用途に特化したフレームワークが存在しており、それらを利用するならば、(コンシューマ向けのリッチな体験を求められるアプリケーションに比べて)かなり低い学習コストで機能を開発できるようになっています。React AdminやVue Element Adminです。
フロントエンドの admin 系ライブラリはバックエンド発祥の admin 系ライブラリに比べてフロントエンドのプラクティスの詰め込みが充実しているのが気に入っています。Optimistic UI, Partial loading, Undo, Back forward cache 等々、あると便利だけど自前で書くとすこし面倒な実装パターンたちも、フレームワークのレールに沿って書くだけで動くものができます。リッチな体験は不要かもしれませんが実質無料でついてくるのであればあるに越したことはありません。
HTML/CSS マークアップを頑張る必要がほぼないというのはパターン 1 と同じです。強いて言うなら、SPA ベースのフレームワークのほうが Material UI のようなモダンな UI をデフォルトで提供しています。
管理画面のことをダッシュボードと呼ぶこともあり、サービスのメトリクスを図やグラフで良い感じに表示することもあるかもしれません。その際にフロントエンドの資産を使えるとだいぶやりやすいです。
Bad: システムコンポーネント / レポジトリの増加によるオーバーヘッド
SPA 開発のコストはさほど高くないとは言いつつ、単一アプリケーション、単一コードベースにすべて詰め込むのに比べれば見過ごせないオーバーヘッドが生じます。
フロントエンドアプリケーション用のデプロイパイプラインや CI/CD が必要ですし、フロントエンドとバックエンドを統合的に検証する E2E テストも整備が難しくなります (必要か?というのは別の議論)。
新たな管理機能を足す際にはバックエンド API とフロントエンド SPA の両方に手を加えなければならないので、monorepo でない場合は pull request を複数作らないといけない(ちょっとしたことに思えますが毎日繰り返すと馬鹿にできないコストです)。コンシューマ向けのモバイルアプリに比べれば可愛いものですが API の後方互換性の面倒も見ないといけません。
チームの技術ポートフォリオに合致するならあり
SPA に代表されるようなリッチフロントエンドの経験を持つチームメイトがいない場合や、JavaScript を中心としたフロントエンド領域がチームの技術ポートフォリオに存在しない (今後も投資しない) 場合は採用が難しいパターンかもしれません。
SPA 開発の総合的な学習コストは下がりつつあるとはいえビルドシステムやデプロイパイプライン周りの面倒を見る必要はありますし、単一アプリケーションをデプロイメントするのに比べたら開発の手数や考えなければならないことは増えます。
そのあたりを呑み込んでやっていけるのであれば個人的には推したいパターンです。
バックエンド API の前段に API ゲートウェイを置いて SSR させたりバックエンド API との仲介をさせるパターンも思いつきましたが、既存の API をバックエンドとして据えつつ新たにフロントエンドを外側に構築するという点でパターン 2 の亜種と考えています。
3. データストアにアクセスする別アプリケーションを作る
管理機能に求められるのは顧客向け機能のデータストアの参照/更新なので既存のバックエンド API を解する必要はない、という視点に立てばバックエンドを担う別 API サーバを置けばよいのでは?という発想もできます。
派生案としてさらにフロントエンドを分割するなど。
Good: ドメインロジックの分離
パターン 1 でもパターン 2 でも、バックエンド API の中で顧客機能向けと管理機能向けのコードが同居するのを防ぐのはなかなか難しかったりします。
管理機能追加以前から Repository パターンにより永続化層を切り出していて管理機能向けのドメインロジックも独立して書けるようになっているとか、両ドメインをまたぐメソッド呼び出しを禁止するとか、モジュール分割を適切に行っていれば話は違うかもしれません。が、管理画面を後追いで作るほどに顧客への価値提供を優先している事業の開発で、初手から手の込んだ (悪く言えば過剰な)設計をしている可能性はあまり高くないと思います。
バックエンドのアプリケーション自体を分離することでドメインロジックの分離を強制できます。
Bad: Distributed monolith
Distributed monolith であり、すでに知られている多くの問題が生じます。
一枚岩のシステムをネットワーク上にばらまいて、分散システムと同様のコストを払いながら、マイクロサービスの利点は享受できないという状態だ。異なる技術を組み合わせてサービスを構成できるという利点も失われ、また、組織や技術の疎結合化もできない。権威が許可なしでチームが技術的進歩を推進することができなくなるのだ。
https://www.infoq.com/jp/news/2016/03/services-distributed-monolith/
実装当初の要求次第では「とりあえず参照だけなら…」と手を出したくなるかもしれませんが、将来に渡って更新を一切行わない管理機能はほぼありえないと思います。
やがては更新系の機能が求められ、管理機能のためのデータモデル・テーブルが追加され、その際に Distributed monolith によるデータストアのスキーマ共有やスキーマ互換性などの問題が生じます。
利点としてドメインロジックの分離が可能と書きましたが、そもそも顧客向け機能と管理機能の土台となるドメイン (解決したい問題領域) は一緒なのではと思います。サブドメインや解決領域で差があれどコアドメインが一緒ならドメインモデルやそれを表現するコードも両者で再利用されるはず。
手のひらを返すようですが、結局似たような重要ビジネスロジックが両者に散らばるのであれば分離できて嬉しいものはなんだろう…という話になってしまいます。
派生案: マイクロサービス
派生として、データストアに直接アクセスせずに既存のバックエンド API を介してデータを参照/更新させることもできます。管理機能専用のデータについては必要に応じて専用のデータストアを持つかもしれません。
distributed monolith ではなくマイクロサービスアーキテクチャに寄せた形です。ビジネスロジックの再利用ができる、スキーマ共有問題などが起きないなどのメリットがあるだけこちらのほうがだいぶマシではありますが、管理機能を作るためだけにマイクロサービスの労苦を背負うのは高コストすぎないかと感じます。
その他 補足
バックエンド API を使い回すパターン 1, 2 のどちらにしても顧客向けと管理機能向けでアプリケーションのプロセスは分けておいたほうが良いと思います。両者で使われ方や負荷傾向が異なるので個別にコントロールしたくなるんじゃないかな。
アプリケーションのコードベースを単一にしつつモジュールの境界を作るテクニックは言語やフレームワークによって異なると思いますが、Rails であればShopify のモジュラモノリスの例や新規サービスの管理画面を短期間で見栄え良く実装するで紹介されているように mountable engine を使う方法があります。最低でも namespace は区切っておきたいところ。
また、顧客が管理機能向けのエンドポイントにアクセスできないようルーティングや権限まわりの制御も行う必要があります。
おわりに
先述の通りどれが正解と言いたいわけではないのでオチもないのですが個人的な体験の話をすると、直近のプロジェクトではパターン 2 を選択しています。最初は有用性を疑っていたフロントエンドの admin 系フレームワークについてもだいぶ評価を改めたので、その感想も近々書きたいと思います。
(2021-01-25 追記) 書きました React Adminの感想 - valid,invalid
管理画面を"後追い"で足すという開発ロードマップは新規事業やスタートアップでは多々あると思うのですが、その際のアーキテクチャパターンについて語られることが多くないと感じていたので整理してみました。
参考記事
- https://zenn.dev/koduki/articles/3f5215f2a79843
- https://bliki-ja.github.io/PresentationDomainSeparation/
- https://www.infoq.com/jp/news/2016/03/services-distributed-monolith/
- https://techlife.cookpad.com/entry/2015/04/06/155940
- https://techlife.cookpad.com/entry/introduce-mart-on-call
- https://speakerdeck.com/yasaichi/what-is-ruby-on-rails-and-how-to-deal-with-it
This article is for ohbarye Advent Calendar 2020.