DP-12: Proxy パターン
実オブジェクトへのアクセスを制御する「代理人(プロキシ)」を挟むパターンです。 代理人を経由することで、遅延ロード・アクセス制御・ログ記録などを透過的に追加できます。
Proxy パターンとは
「プロキシ(Proxy)」とは「代理」を意味します。 Proxy パターンでは、実オブジェクトと同じインターフェースを実装した代理オブジェクト(プロキシ)を用意し、 クライアント(呼び出し側)とのやり取りをプロキシが仲介します。 クライアントはプロキシと実オブジェクトを区別せずに扱えるため、透過的にアクセス制御や追加処理を実現できます。
Proxy の3種類
| 種類 | 目的 | 代表例 |
|---|---|---|
| 仮想プロキシ | コストの高いオブジェクト生成を必要になるまで遅らせる(遅延ロード) | 大きな画像ファイルのロード、DB接続 |
| 保護プロキシ | ロールや権限を確認してからアクセスを許可する | 管理者のみ閲覧可能なデータ、API アクセス制御 |
| リモートプロキシ | ネットワーク越しのオブジェクトをローカルのように扱う | RMI(Remote Method Invocation)、REST クライアント |
Java 標準ライブラリの実例: java.lang.reflect.Proxy(動的プロキシ)
Java には実行時にインターフェースの実装を動的に生成するjava.lang.reflect.Proxy があります。 Spring Framework や Hibernate などの DI フレームワークは、この動的プロキシを使ってトランザクション管理や AOP(アスペクト指向プログラミング)を実現しています。
// java.lang.reflect.Proxy を使った動的プロキシの概念コード
// インターフェースに対してのみ使用可能(クラスには使えない)
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[]{ MyInterface.class },
(proxyObj, method, args) -> {
System.out.println("メソッド呼び出し前: " + method.getName());
Object result = method.invoke(realObject, args);
System.out.println("メソッド呼び出し後: " + method.getName());
return result;
}
);サンプルコード
Java 8 版では2種類のプロキシを実装しています。LazyImageProxy は初回 display() 呼び出し時だけ RealImageLoader を生成します(仮想プロキシ)。AccessControlProxy はロールを確認してからアクセスを許可します(保護プロキシ)。
よくあるミス・注意点
⚠️ Proxy パターンと Decorator パターンを混同する
両者は構造が似ていますが、目的が異なります。Proxy パターンは「アクセス制御や遅延ロードなど、オブジェクトへのアクセス自体を管理する」ことが目的です。Decorator パターンは「既存オブジェクトに機能を追加する」ことが目的です。 判断の目安: 「このクラスは何かを制御しているか(Proxy)」「機能を追加しているか(Decorator)」
⚠️ 仮想プロキシのスレッドセーフ性に注意する
複数スレッドが同時に仮想プロキシを呼び出すと、realLoader == null のチェックと生成が競合して 実オブジェクトが複数回生成されてしまう可能性があります(二重チェックロッキング問題)。 マルチスレッド環境では synchronized やAtomicReference を使ってスレッドセーフにしましょう。
⚠️ プロキシの存在を意識せず実オブジェクトにキャストしてしまう
プロキシは実オブジェクトと同じインターフェースを実装していますが、instanceof や直接キャストで実クラスを前提にしたコードを書くと、 プロキシを経由したときに ClassCastException が発生します。 常にインターフェースを通じてアクセスするよう統一しましょう。
テストする観点
- 仮想プロキシが遅延ロードしていること: 生成直後は RealImageLoader のコンストラクタが呼ばれておらず、初回 display() 時だけ呼ばれること
- 2回目以降の display() ではコンストラクタが呼ばれないこと(ロード済みの実オブジェクトが再利用されること)
- USER / ADMIN ロールは display() が正常に実行されること
- 権限のないロール(GUEST など)では SecurityException がスローされること
- プロキシが実オブジェクトと同じインターフェースを実装しており、クライアントから区別なく使えること
- Java 17 版: ProxyResult.allowed() と ProxyResult.denied() が正しいフィールド値を持つこと
- Java 21 版: describeProxy() が各 ProxyType サブタイプに対して正しい説明文を返すこと