Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

レイヤードアーキテクチャを振り返る

こんにちは、Sansanプロダクト開発部の清水です。

ある程度のアプリケーションの大きさだと当たり前に使われる事が多い「レイヤードアーキテクチャ」の自分が考える設計のポイントや、実際に運用する際のポイントについて書いてみようかと思います。 基本的な話なので「今更かよ」って感じがしますが、実際に設計、運用する際には様々な考慮事項のあるものだと思うので、知ってる人にとっても復習にでもお役に立てればと思います。

そもそもレイヤードアーキテクチャって何?

概要

一言でいうと、アプリケーションを作る際にそれを構成する部品を、それぞれ責務が定義された論理的なグループにまとめて整理し、それぞれのグループ間のやり取りの仕方を決めておこうという事です。 このグループ間のやりとりにおいて、一方向かつ隣接するグループとしかやりとりを行えないようにする事が多く、層状になるのでレイヤードアーキテクチャと呼ばれます。 一般的に上記の 論理的な グループの事をレイヤー(Layer)と呼びます。*1

レイヤードアーキテクチャのメリット

アプリケーションは成長していくにしたがって、巨大で複雑なものになっていきます。 そうなってくるとコードが大量にある状態になり、開発を行う際に、どこに何をするものがあるのかを開発者が把握する事が困難になります。

なので、特定の責務によって整理し、何がどこにあるのかを把握しやすくすることで、保守性を高める事ができます。 例えばデータアクセスする処理があったとして、それがデータアクセスレイヤーに置かれているなら、開発者はそのレイヤー内を探せば簡単に見つける事ができます。

ただ、それだけなら単にコードのグループ化だけでも同じメリットが得られます。 グループ間のやり取りの仕方を決めて制限し層状にする事のメリットは、もしアプリケーションに対する変更があった場合に、その影響範囲を、層を直接参照している層のみに限定する事ができるようになります。 もしデータの永続化の方法が、最初はリレーショナルデータベースで行っていたのが様々な都合で KeyValueStore の方が適切になり変更を実施する事になった場合に、その変更の影響範囲はデータアクセスレイヤーとそれを参照するレイヤーのみに影響を抑える事ができます。

代表的なレイヤーの分割単位と、そのやりとりの仕方

レイヤードアーキテクチャではレイヤーを以下の3層に分ける事が多いかと思います。

名称 責務の概要 別名(その表記を見る書籍など)
プレゼンテーション ユーザとシステムのやりとりに関するものが置かれる。通常のWebアプリケーションならUIとなるHTMLや、WebAPIならリクエスト、レスポンスのモデル。あるいはControllerなど表示を調整するプレゼンテーションロジックなど。 ユーザインターフェース(DDD *2 )
ビジネスロジック そのシステムで業務を表現するモデルや処理が置かれる。業務的なバリデーションやルールの適応など。 ビジネス(MSAAG*3, ドメイン)(PoEAA*4, DDD)
データアクセス データアクセスに対する技術的な手段を提供するものが置かれる。主にデータベースや外部サービスを直接的に叩く処理など。 データ(MSAAG), データソース(PoEAA), インフラストラクチャ(DDD)

この3層を取る場合、代表的な処理の流れは以下になります。

  1. ユーザはプレゼンテーションレイヤーを介してシステムへの操作を行います。
  2. プレゼンテーションレイヤーはビジネスロジックレイヤーとのみやり取りを行います。
  3. ビジネスロジックレイヤーはデータアクセスのためにデータアクセスレイヤーを呼び出します。
  4. データアクセスレイヤーでのデータアクセスの結果をビジネスロジックレイヤーに返します。
  5. 4.の結果をプレゼンテーションレイヤーで加工しユーザへの表示を行います。

この時プレゼンテーションレイヤーは必ずビジネスロジックレイヤーを介してデータの取得や永続化を行い、直接データアクセスレイヤーとやり取りをする事は行いません。 このようにそれぞれのレイヤーに決められた責務以外の事は別のレイヤーに任せ、やり取りする相手を制限する事で処理の流れや依存関係を理解しやすくします。

layred-archtecture
Layred archtecture

レイヤードアーキテクチャ設計時のポイント

上記は代表的なレイヤーの切り方とやりとりの仕方です。それ以外のパターンを検討するケースについても述べていきたいと思います。

レイヤー間でのやりとりの仕方の設計

上記の代表的なケースでは、上位レイヤーは直下のレイヤーしか呼ばない流れになっていましたが、場合によっては例外的に中間レイヤーを飛ばす事を検討した方がいい場合もあります。

例えばある画面を表示するために必要なビジネスロジックの部品が複数あり、それぞれにデータアクセスを行って復元する必要がある場合は、その画面を表示するのに必要なデータアクセスの回数はビジネスロジックの部品の数分呼ばれる事になります。データアクセスはネットワークを跨ぐような事が多いので、複数回行うとパフォーマンスが劣化していく傾向があります。 この場合の緩和策として、データアクセスレイヤーにプレゼンテーションの都合である画面固有のデータアクセスを行うための部品を用意し、直接プレゼンテーションレイヤーから利用するという手があります。 この部品は、本来は複数のビジネスロジックの部品で構成されるべきものを、一度に取ってくるように実装できるのでパフォーマンスの劣化を防ぐ事ができます。

omit-layer
中間レイヤーを省く事を許容する

ただし、この部品はビジネスロジックを飛ばしているため、データアクセスレイヤーでの変更の影響を直で受けてしまいます。 また更新系の処理でこのような緩和策を行うと、本来は使いまわされるべきバリデーションなどのビジネスロジックが各画面固有の処理に散らばってしまい、変更が難しくなってしまいます。

このように緩和策を用意した方がいい場合でも、その緩和策を適用する部品の役割や利用を限定して行わないとレイヤー化するメリットが失われてしまう事もあるので、その判断と適応ルールは慎重に行うべきだと思います。

レイヤー間での依存関係の設計

通常上位レイヤーは下位レイヤーの詳細を知らないでもよいように設計をする事で、下位レイヤーに置いてある部品が再利用しやすくなります。 ある処理をビジネスロジックの部品を介してWebアプリケーションに実装するという話があったとしましょう。この時ビジネスロジックの部品は使われる予定のWebアプリケーション側の都合を一切知らないように実装します。 そうする事でビジネスロジックの部品が、Webアプリケーションの都合が一切入ってないのでバッチなど別アプリケーションでも再利用する事が可能になります。 これはコードの重複を防ぎDRYを実現するのに役立ちます。

一方で上記の話と反しますが、特定レイヤーの部品間での依存ルールを定める事もできます。 例えばデータアクセスの部品とビジネスロジックの部品を1:1で定義し、あるデータアクセスの部品はあるビジネスロジックの部品以外では使われない、とする事でデータのバリデーションをすべてビジネスロジックに寄せてデータアクセス側では省略するという緩和策を用意できるようになります。

decoupled-and-coupled
疎結合にする箇所と密結合にする箇所

このようにレイヤー内の部品の依存関係は実際に作るものに合わせて、どこを使いまわしを前提として疎に設計するか、どこを依存させて密結合にさせるかという判断が実際にレイヤー内の部品の役割を決める場合に求められます。

レイヤーの増減の設計

またレイヤードアーキテクチャは別に上記の代表的な3層に限っている訳ではないので、作るアプリケーションの規模や内容に応じてレイヤーを増やしたり、減らしたりしても良いものであるとされてます。 例えば簡単なCRUDしか行わないシンプルなアプリケーションがあったとして、殆どビジネスロジックが存在しないならビジネスロジックレイヤーを用意せず、プレゼンテーションレイヤー側に少しのビジネスロジックを用意すれば、それとデータアクセスレイヤーだけの方がシンプルでよいケースもあったりするかと思います。

逆に必要に応じて上記以外のレイヤーを用意する事もあります。 Webアプリケーション固有の処理で、特定のビジネスロジックを一点の手順で呼び出す処理を使いまわしたいケースなどに、ユースケースやアプリケーションと呼ばれるレイヤーをプレゼンテーションレイヤーとビジネスロジックレイヤーの間に追加する事もよく見られます。

add-or-remove-layer
レイヤー増減

これらのレイヤーの増減は、適切に現在のメリットと後々の保守性の検討を行わないと、複雑さを増大させる要因になるため慎重に判断すべきです。

レイヤー外の横断的関心事

今までレイヤーについて記載してきましたが、全てのものをレイヤー内に整理する必要はありません。中にはレイヤーを跨ぐような機能も存在します。 例えばロギングや、キャッシュ、セキュリティに関する処理などのような機能です。これらのような、どのレイヤーでも呼ばれるものは横断的関心事(Cross Cutting Concern)と呼ばれます。

例えばキャッシュについて考えてみましょう。 データアクセスの結果をデータアクセスレイヤーでキャッシュしておけば上位のレイヤーからがキャッシュかどうかを気にしないで利用しパフォーマンスを向上させる事ができます。 一方でビジネスロジック側で複数のデータアクセスの結果をまとめてキャッシュしておく事でキャッシュヒット率の向上が見込めるなどあれば上位のレイヤーでの利用を検討すべきだったりします。

cross-cutting-concern
横断的関心事

このように利用ケースによってはどこのレイヤーにでも利用が発生しうるものは単一のライブラリ化しておくなど、レイヤーとして定義したものと責務を完全に分離しておく事で、再利用性と保守性を担保する事ができます。

レイヤードアーキテクチャ導入時のポイント

今までレイヤードアーキテクチャの代表的な例と、その例から逸脱したり、その例に載ってないレイヤー外の事項について記載してきました。 実際にレイヤードアーキテクチャの導入を行う場合に実施した方がいい事を記載してみます。

  • プロトタイピング
    • 実際にそのレイヤーの定義や部品でうまくアプリケーションを実装できるか、簡単な機能で実装して試してみる事をお勧めします。机上の設計だけで決定して実装が進んでしまった後で、上手くアプリケーションを構築できない事が発覚した場合にリカバリが難しくなる事があります。本当に現在のレイヤー定義やレイヤー内の部品の役割でうまくアプリケーションが構築できるかは検証する方が良いでしょう。
  • レイヤーの責務の明文化とその浸透策の検討
    • 一旦レイヤーの定義が決まったら、そのアプリケーションを開発、運用していく開発者に対して認識してもらう必要があります。もし殆どの開発者にレイヤーの定義が認識されてなかったら、そのレイヤーの責務が曖昧になってしまい最終的にはレイヤー化するメリットは失われてしまいます。実際弊社も初期にレイヤーには分けられてはいるものの、開発者への浸透がされず責務が曖昧になって後からのメンテナンスが困難になったケースがありました。それを防ぐためにはレイヤー、レイヤー内の部品の明文化とそれをどうやって他の開発者に共有するかは検討した方がよいでしょう。
  • 継続的に変更しやすいようにしておく
    • アプリケーションの成長に従い、今までのレイヤー定義だとうまく表現しきれないケースが発生する事もあります。YAGNIがアプリケーションを作る際の原則ではありますが、別に拡張しづらいようにやれという話ではありません。後からレイヤーの差し込みや部品の追加などができないような縛りはいれない方がよいと思います。

まとめ

Patterns of Enterprise Application Archtecture にもありますが、「レイヤードアーキテクチャを行う上で最も難しいのは、必要なレイヤーと各レイヤーが行うべき内容を決定する事である」とあります。 レイヤードアーキテクチャは一般的な手法ではありますが、杓子定規に3層にレイヤーを切ればアプリケーションをうまく作れるという訳ではありません。 実際はそのアプリケーションの特性だったり、運用するチームの技量や環境などに応じて、レイヤーの増減や、緩和策などの検討を行ったりと、上手くやるためには設計者のレイヤードアーキテクチャに対する理解が必要かと思います。

また近年本も発売された事で流行っている Clean Archtecture や、その類似である Onion Architecture などはこのレイヤードアーキテクチャの代表的な一例だと思います。 これらのアーキテクチャを検討する前に、その根底の考え方であるレイヤードアーキテクチャを学んでみてもいいのではないでしょうか。

参考文献

*1:対称的にサーバへの配置場所など 物理的な配置 でグループを分ける場合はティアー(Tier)と呼ばれており、レイヤーとよく混同する事がありますが、意味合いが違うので注意してください。

*2:エリック・エヴァンスのドメイン駆動設計

*3:Microsoft Application Architecture Guide, 2nd Edition

*4:エンタープライズアプリケーションアーキテクチャパターン

© Sansan, Inc.