本書は@pospomeがサーバサイドのアプリケーションアーキテクチャに関してまとめたものです。
pospome。サーバサイドエンジニア。DDD、実装パターンのようなアプリケーションアーキテクチャが専門。最近は認証認可に興味がある。Twitterは@pospome。ブログは「pospomeのプログラミング日記」。共にアプリケーションアーキテクチャ、Go言語、GCPを中心にアウトプットしている。キャラクターは羊ではなくポメラニアン。
最近のWebアプリケーション開発ではMVCのような一般的なレイヤ構造だけではなく、レイヤードアーキテクチャ、クリーンアーキテクチャのような複雑なレイヤ構造を採用する機会が増えてきました。レイヤ構造はコードの品質に大きな影響を与えます。そのためエンジニアはそれぞれのレイヤがどのようなもので、どのようなメリット、デメリットを持っているのかを把握し、使い分ける必要があります。本章ではレイヤ構造の中でも有名なものをパターン別に紹介します。
そもそもレイヤとは何でしょうか?レイヤ構造について説明する前にレイヤの定義について説明します。
筆者は"レイヤ=パッケージ(モジュール)"という認識を持っています。どのようなパッケージかというと、Webアプリケーションにおいて普遍的な責務を持つパッケージです。例えば、MVCパターンであれば、モデル、ビュー、コントローラーという3つのパッケージがレイヤとして存在します。ECサイトであれ、オークションサイトであれ、ドメインロジックを持つというモデルの責務が変わることはありません。そのため、常に使う側と使われる側の関係性が決まっている(レイヤ間は単方向依存である)のが特徴です。
一方普遍的ではないパッケージも存在します。それがWebアプリケーションの仕様に対するパッケージです。例えばECサイトであれば、買い物かごパッケージが存在するかもしれませんが、オークションサイトには買い物かごパッケージが存在するとは限りません。Webアプリケーションの仕様によってどのようなパッケージが存在し、どのような依存関係になるのかが異なります。
レイヤ構造を利用することで次のメリットを得ることができます。
レイヤ構造はレイヤ間の依存方向が単方向依存です。例えば、MVCではコントローラーはモデルに依存しますが、モデルはコントローラーに依存しません。そのためコントローラーを修正した場合はモデルに影響はありませんが、モデルを修正した場合はコントローラーに影響があります。このように修正箇所の影響範囲を予測することができます。単方向依存が保証されていない場合、パッケージ同士が修正の影響を受けあってしまい、影響範囲を予測することができません。
レイヤはWebアプリケーションにおいて普遍的な責務を持つパッケージです。そのためシステムを修正する際にどのレイヤのコードを修正すればいいのかが明確になりますし、レイヤ間の依存方向に沿って自然とコードを置く場所が決まります。実装者のスキルや好みによってコードを置く場所がブレることが少なくなります。
レイヤは普遍的な責務を持つパッケージなので、自分が担当するサービスや部署が変わったとしてもレイヤ構造が同じであれば、コードをキャッチアップするためのイニシャルコストを削減することができます。
レイヤは普遍的な責務を持つパッケージなので、それらの依存関係は常に適切です。レイヤを利用せず自由にパッケージを定義すると、それらの依存関係は実装者のスキルに依存したものとなります。誰もが適切にパッケージ同士の依存関係を定義できるわけではありません。レイヤ構造では最低限の秩序を守ることができます。
一方で当然ながら次のデメリットも存在します。
レイヤ構造に対する知識がない場合、レイヤそれぞれの責務を覚える必要があります。レイヤも1種類ではありません。プロジェクトごとに異なるレイヤ構造を採用している場合、それなりにキャッチアップコストがかかってしまいます。それらを覚えるまでは作業スピードも落ちてしまうでしょう。
レイヤ構造の階層が多ければ多いほど、全体のコード量は増えてしまう傾向にあります。実装対象のWebアプリケーションに対して不適切なレイヤ構造を導入してしまうと、レイヤ構造のメリットを享受できず、単にコード量を増やしてしまうことになります。
今回紹介したレイヤ構造のメリット、デメリットは一般的なものです。実際はレイヤ構造によってメリット、デメリットが異なります。
最初に紹介するのはコントローラーパターンです。コントローラーパターンはMVCパターンのコントローラーに全てのロジックを実装するシンプルなパターンです。レイヤがコントローラーのみなので、一般的にはレイヤ構造とは認識されないと思います。
パッケージ構成例はリスト1.1です。
コントローラーパターンはコントローラーレイヤしか存在しないので、パッケージもcontrollerのみです。基本的にcontrollerパッケージ直下にファイルやパッケージを置きます。サーバの出力がHTMLの場合は、templateパッケージのようなHTMLのテンプレートファイルを置くパッケージが存在します。
コントローラーパターンのレイヤはコントローラーレイヤのみです。そのためHTTPリクエストのバリデーション、DBトランザクション、ドメインロジックの実装など、コントローラーレイヤに全てのロジックを実装することになります。リスト1.2がコントローラーレイヤの実装例です。
コントローラーパターンはコントローラーレイヤに全てのロジックを実装するので、レイヤ構造としてはシンプルになります。学習コストはかからないでしょう。レイヤ同士の依存関係に悩むこともなく、すぐにコードを書くことができます。
全てのロジックをコントローラーレイヤに実装するという性質上、実装対象の仕様が複雑でコード量が多くなるとコードの見通しが悪くなってしまいます。リスト1.3のように関数ベースで処理を切り出すことで、ある程度見通しの良いコードにすることが可能ですが限界があります。
Go言語の場合はパッケージ単位で変数、関数へのアクセスを制御することになります。そのため、1つのレイヤに全てのロジックを実装してしまうと、実質全ての変数、関数へのアクセスが可能になり、特定のコードを修正することに対する他のコードへの影響が大きくなる可能性が高くなります。コード量が多くなる場合はコントローラーパターンは避けた方がよいでしょう。
レイヤ構造はシンプルですが、実装するコード量が多くなるとコードの見通しが悪くなるので、小規模な実装に向いています。筆者は次のユースケースでこのパターンを採用することが多いです。
これらはいずれもサービスを利用するエンドユーザー向けの実装ではなく、社内でのサービス運用に利用する実装です。社内でのサービス運用では信頼性のあるデータがすでに存在していて、それをWebUIからDBに登録するだけという要件が多いので、コントローラーパターンで十分実装可能なことが多いでしょう。おそらく読者の方もこのようなユースケースでコントローラーパターンを採用した経験があるでしょう。実装するコード量が多くなるとコードの見通しが悪くなるというはっきりとしたデメリットがあるので、他のレイヤ構造よりも採用すべきユースケースが限定されるレイヤ構造だと思います。