前置き
今回は、SOLIDのインターフェイス分離の原則と、セキュリティのかかわりについて
触れていきたいと思います。
疑問
すでにあるFatなインターフェイスを、インターフェイス分離原則に従い分離するとき、
以下の懸念事項がないでしょうか?
メリット
Fatなインターフェイスが解消され単一目的なインターフェイスとなり、保守性が向上する。
懸念事項
反面、下図のように外部のモジュールへと公開する入り口が増えることになるので、そこが新たな攻撃対象とならないのだろうか?
への脅威モデリングなどのよるセキュリティリスク分析の必要性が出てきませんか?
結論
たしかに、原則の適用によって増えたインターフェイス(入り口)へのセキュリティリスク分析は、分離前に必要になります。
しかし、これは単純なリスクの増大ではなく、むしろ 「リスク管理の質を向上させる」 という、極めてポジティブな側面を持っています。
なぜセキュリティ分析が必要になるのか
Fatなインターフェイスを分離すると、外部モジュールとの接点(論理的なエントリーポイント)の数は増えます。
Before、Afterの比較図で概念的に捉えてみましょう。
Before (ISP適用前)
IAllMightyInterface
という1つの巨大な入り口
After (ISP適用後)
インターフェイス分離の原則を適用し、以下の3つのinterface
に分離できたとする。
・IReaderInterface
・IWriterInterface
・IAdminInterface
という複数の小さな入り口。
入り口の数が増えれば、それぞれの入り口が意図せず悪用される可能性を個別に評価する必要が出てくるため、脅威モデリングなどでの分析対象は形式的に増えることになります。
なぜそれが「リスク管理の向上」に繋がるのか
ここからが重要です。
入り口の数(攻撃対象)はたしかに増えますが、
それぞれの入り口が持つ「権限」と「役割」が限定されるため、セキュリティはむしろ堅牢になります。
これは、最小権限の原則 (Principle of Least Privilege) を実現する上で極めて効果的です。
🔑 鍵のアナロジー
Fatなインターフェイス
1本ですべての扉(データ読み込み、書き込み、管理者機能)を開けられるマスターキーのようなものです。
便利ですが、一度この鍵が盗まれたら、被害は甚大です。
分離されたインターフェイス
「閲覧室の鍵」「書庫の鍵」「金庫の鍵」のように、役割ごとに分かれた個別の鍵です。
管理する鍵の数は増えますが、
もし「閲覧室の鍵」が盗まれても、金庫の中身は安全です。
具体的なメリット
上記のアナロジーでメリットを紹介していきます。
1. 攻撃対象領域(アタックサーフェス)の限定
あるクライアントモジュールが、データの読み取りだけを必要としている場合、IReaderInterface
だけを渡します。
たとえ、そのモジュールが乗っ取られても、
攻撃者は、書き込みや削除の機能(IWriterInterface
) を呼び出すことはできません。
Fatなインターフェイスでは、すべての機能が呼び出し可能であったため、ここが大きな違いです。
2. 脅威モデリングの焦点化と単純化
Fatなインターフェイスの脅威を分析するのは、「このすべて開けられるマスターキーで何ができますか?」 と問うようなもので、非常に複雑です。
しかし、分離されたインターフェイスでは、
「このIReaderInterfaceに対する脅威は何か?」 という、範囲が限定された問いに集中できます。
これにより、脅威分析はよりシンプルかつ正確 になります。
そのため、「情報漏洩」のリスクに集中でき、「データ改ざん」のリスクはスコープ外として明確に分離できます。
3. 意図の明確化
インターフェイスが役割ごとに分離されていると、コードを読むだけで、
「このモジュールは、データを読み取ることしか許可されていない」 という
設計者の意図が明確に伝わります。
これにより、意図しない使い方をされるリスクがレビューなどで発見しやすくなります。
分離しない・した時の比較表
インターフェイスの分離は、たしかに管理対象の数は増やすものの、それと引き換えに、
システム全体をより安全で、理解しやすく、堅牢にするための強力なセキュリティプラクティスとなります。
2段階の最小権限の原則
しかし、インターフェイスを分離するだけでは、クライアント(呼び出し側)から見える権限を最小化したに過ぎません。
そのインターフェイスを実装しているクラス自体は、まだ
複数の責務を持つ「単一責務に違反したクラス」のままであり、そのクラスの内部では最小権限の原則が満たされていない状態です。
この問題を解決する、ソリューションを紹介しましょう。
まずは、力業に近いものから紹介しましょう。
最小権限の原則が2つ以上のレベルで適用されることを考える。
🔑 レベル1:インターフェイスレベルの最小権限(ISPが担当)
これは、「クライアントに、必要のない機能を見せない・使わせない」 というレベルです。
目的
クライアントコードが、意図せず、あるいは悪意を持って、本来必要としない機能を呼び出すことをコンパイルレベルで防ぎます。
事例
データを読みこむだけのクライアントに IReader
インターフェイスだけを渡せば、そのクライアントは write() メソッドを呼び出すコードを書くことすらできません。
これは、家の 「ゲスト用の鍵」 のようなものです。この鍵では寝室や書斎には入れません。
🏠 レベル2: 実装レベルの最小権限(SRPが担当)
「クラス自体に、その責務に必要のない能力を持たせない」 というレベルです。
課題
ISPを適用しても、MyBigService
クラスが IReader
と IWriter
の両方を実装している場合、
read() メソッドが呼び出されたコンテキスト(メモリ空間)内から、何らかの脆弱性を突かれて write() のロジックが悪用される可能性は依然として残ります。
これは、ゲストをリビングに入れたら、そのリビングから寝室につながる内側のドアが施錠されていないのと同じ状態です。
解決策
ここで必要になるのが、単一責務の原則(SRP) です。
究極的には、インターフェイスだけでなく、クラスそのものも責務ごとに分離します。
ISPとSRPの協調:真の最小権限へ
以上のことから、
真の最小権限の原則は、抽象のISPと、具体SRPが協調して初めて達成されます。
Step 1:ISPを適用する
Fatなinterface
であるIMyBigService
を IReader
と IWriter
に分離します。
これにより、クライアントに対する契約が明確になります。
Step 2:SRPを適用する
MyBigService
クラスをそれぞれのインターフェイスを実装する小さなクラスに分割します。
ReaderService class implements IReader
WriterService class implements IWriter
これにより、
ReaderService は物理的に書き込みのロジックを一切持たなくなります。
WriterService も同様に、読み込みのロジックを持ちません。
多段階の最小権限の原則のまとめ
以上の2ステップを踏むことで、
「読み取り専用の鍵(IReader
)は、読み取り機能しかない専用の部屋(ReaderService
)の扉しか開けられない」
という、極めて堅牢な状態が実現します。
2つのリファクタリングパス
さて、目指すゴール(単一責務で、インターフェイスが分離された状態)は同じですが、
そこに至るまでの道筋には大きく分けて以下の2つのアプローチがあります。
アプローチA:インターフェイスから始める (トップダウン / 契約駆動)
ISP → SRP
これは、前のトピックで議論したプロセスです。
①. 契約の再定義
まず、クライアントの視点から「どのような役割(契約)が必要か?」を考え、FatなインターフェイスをIReader
, IWriter
などに分離します (ISPの適用)。
②. 実装の追従
次に、その新しい契約に合わせて、Fatな実装クラスをReaderService
, WriterService
などに分割します (SRPの適用)。
アプローチB:実装から始める (ボトムアップ / 実装駆動)
SRP → ISP
これが、もう一つの極めて有効なパスです。
①. 責務の分離
まず、クライアントのことは一旦忘れ、Fatなクラスの内部に着目します。
「このクラスの中には、どのような異なる責務が混在しているか?」を分析し、
それをReaderService
とWriterService
という、責務ごとに独立した小さなクラスに分割します (SRPの適用)。
②. 契約の抽出
次に、新しくできた単一責務のクラスから、その能力を表現するのに最適なインターフェイスを抽出します。
ReaderService
からは自然とIReader
インターフェイスが生まれ、WriterService
からはIWriter
インターフェイスが生まれます (ISPの適用)。
どちらのアプローチが優れているか?
どちらも有効な戦略ですが、状況によって最適なアプローチは異なります。
以下に比較表を載せておきます。
一般的には、アプローチB(実装から始める) の方が、より根本的でクリーンな結果に繋がりやすいとされています。
なぜなら、
健全で凝集度の高い実装があれば、そこから生まれるインターフェイス(契約)もまた、自然と健全で凝集度の高いものになる
からです。
新たな問題点
ただ、ミクロなクラス単位でのセキュリティというのは、セキュリティアーキテクチャの観点で、あまりにも保守のしにくさが目立ちます。
本来は同じパッケージやモジュールにまとまったものは、一様なセキュリティレベルを求められる単位でまとまっていてほしいものです。
なぜ基本は、2つのリファクタリングパスの方が良いのか
原則として「2つのリファクタリングパスの手法を取る」べき だとする理由は、以下の3つの重要な境界を一致させようとする、高度な設計思想に基づいています。
ドメインの境界 (DDD)
ビジネスロジックの関心事が同じものは、同じ場所にまとめるべきです。
セキュリティの境界
同レベルのセキュリティポリシーが適用されるべきものは、同じ場所にまとめるべきです。
保守の境界 (コンウェイの法則)
同じチームが責任を持つべきものは、同じ場所にまとめるべきです。
ベターな設計 -境界の一致-
理想的なアーキテクチャでは、これら3つの境界がほぼ一致します。
「基本は実装(SRP)から」というアプローチは、まさにこの理想状態を目指すものです。
まず、ドメインの関心事(責務)に基づいてクラスやモジュールを健全に分割(SRP)すれば、その塊は自然と 「一様なセキュリティレベルを適用すべき単位」 になります。
そして、その健全な塊からインターフェイスを抽出すれば、結果としてクリーンな契約(ISP)が生まれます。
補足 -最悪な設計-
対して、以下の図のような信頼境界の定義は、最悪な設計です。
ドメインの境界とセキュリティ信頼境界の位置が、矛盾したものになっています。
「2段階の原則」が持つ課題と位置づけ
一方で、「2段階の原則(ISP → SRP)」は、いくつかの深刻な課題を抱えています。
可読性の欠如
クラス内部は依然としてFatなままであるため、コードの意図が掴みにくく、ドメインの境界が曖昧になります。
ミクロなセキュリティ管理
クラスという最小単位ごとにセキュリティを考えるのは、あまりにも細かく考えすぎです。
クラス単位でセキュリティを考えるのは、都市計画で言うならば、ビル全体の警備計画を立てずに、部屋の中の机一つ一つに個別の警備員を配置するようなものです。
非効率で、抜け漏れが発生しやすく、保守運用は破綻します。
ビル(パッケージやモジュール)単位で入退館管理を行う方が、堅牢で合理的です。
したがって、「2段階の原則」は、
Fatなインターフェイスがどうしても影響範囲が大きくて、分離できないなどの
理想的リファクタリングが現実的に不可能な場合の、次善の策(or 応急処置)
として位置づけるのが最も賢明です。
よって、その戦略をまとめると以下のようになります。
戦略的なリファクタリング方針
プライマリ戦略(基本方針):SRP → ISP (実装駆動)
①. まず、ドメインの責務に基づいて、クラスやモジュールを凝集度の高い塊にリファクタリングする(SRP)。
②. その健全な塊(モジュール)に対して、一様なセキュリティポリシーを適用する。
③. 最後に、その塊から必要なインターフェイスを抽出する(ISP)。
セカンダリ戦略(例外的な場合の選択肢):ISP → SRP (契約駆動)
①. 前提
対象のモジュールが巨大すぎて、SRPを適用するリファクタリングの影響範囲が許容できない(レガシーシステムなど)。
②. 応急処置
まず、外部への影響を最小限に抑えるため、インターフェイスだけを分離(ISP)し、クライアントに対する 「外壁」 を固める。
③. 将来への布石
これを恒久的な解決策と見なさず、技術的負債としてADRなどのドキュメントに記録する。
そして、将来的にプライマリ戦略に移行するための計画を立てる。