概要
画像の遅延ロード、API 呼び出しのキャッシュ、権限に基づくアクセス制御――これらは「実オブジェクトの手前に代理を置く」ことで実現できます。Proxy パターンは、Subject インターフェースを共有する代理オブジェクト(Proxy)が実オブジェクト(RealSubject)へのアクセスを仲介し、追加の制御を行う構造です。用途に応じて、仮想プロキシ(遅延初期化)、保護プロキシ(アクセス制御)、リモートプロキシ(ネットワーク越しの呼び出し)に分類されます。この記事では画像ローダーを題材に、仮想プロキシとアクセス制御プロキシの2つを実装し、Decorator パターンとの違い、Java 標準の java.lang.reflect.Proxy との関連を整理します。
使いどころ
画像ビューアで表示されるまでロードを遅延し、スクロールして見える範囲に入ったときだけ実体を生成する
ユーザーのロール(ADMIN / USER / GUEST)に応じてリソースへのアクセスを制御し、権限がなければ操作を拒否する
外部 API の呼び出し結果をキャッシュするプロキシを挟み、一定時間内の再呼び出しではキャッシュを返す
コード例
public class ProxyPatternSample {
// アクセス結果を record で表現(Java 17+)
record ProxyResult(boolean allowed, String message) {
static ProxyResult ok(String msg) {
return new ProxyResult(true, msg);
}
static ProxyResult denied(String reason) {
return new ProxyResult(false, "拒否: " + reason);
}
}
// Subject インターフェース
interface ImageLoader {
void display();
}
// RealSubject
static class RealImageLoader implements ImageLoader {
private final String path;
public RealImageLoader(String path) {
this.path = path;
System.out.println("[Real] ロード: " + path);
}
@Override
public void display() {
System.out.println("[Real] 表示: " + path);
}
}
// 仮想プロキシ: 遅延初期化
static class LazyImageProxy implements ImageLoader {
private final String path;
private RealImageLoader real;
public LazyImageProxy(String path) {
this.path = path;
System.out.println("[Proxy] 作成: " + path);
}
@Override
public void display() {
if (real == null) {
real = new RealImageLoader(path);
}
real.display();
}
}
// アクセス制御プロキシ
static class AccessControlProxy implements ImageLoader {
private final ImageLoader delegate;
private final String userRole;
public AccessControlProxy(ImageLoader delegate, String role) {
this.delegate = delegate;
this.userRole = role;
}
@Override
public void display() {
var result = checkAccess();
if (!result.allowed()) {
throw new SecurityException(result.message());
}
System.out.println("[AccessProxy] 許可: " + result.message());
delegate.display();
}
private ProxyResult checkAccess() {
if ("ADMIN".equals(userRole) || "USER".equals(userRole)) {
return ProxyResult.ok("ロール=" + userRole);
}
return ProxyResult.denied("権限なし: " + userRole);
}
}
public static void main(String[] args) {
// 仮想プロキシ
var img = new LazyImageProxy("/images/photo.jpg");
System.out.println("-- まだロードされていない --");
img.display(); // ここでロード
img.display(); // キャッシュ済み
System.out.println();
// アクセス制御プロキシ
var secured = new AccessControlProxy(
new LazyImageProxy("/images/secret.jpg"), "ADMIN");
secured.display();
try {
var guest = new AccessControlProxy(
new LazyImageProxy("/images/secret.jpg"), "GUEST");
guest.display();
} catch (SecurityException e) {
System.out.println("例外: " + e.getMessage());
}
}
}Version Coverage
record でアクセス制御の結果を型安全に表現できる。var で Proxy の呼び出しコードが簡潔になる。
// Java 17: var + record でアクセス結果を管理
var proxy = new LazyImageProxy("/images/photo.jpg");
proxy.display();
record ProxyResult(boolean allowed, String message) {
static ProxyResult ok(String msg) {
return new ProxyResult(true, msg);
}
}
var result = ProxyResult.ok("ロール=ADMIN");Library Comparison
注意点
Proxy と Decorator は構造が似ているが、Proxy はアクセス制御や遅延初期化が目的、Decorator は機能追加が目的。設計意図を明確にしないと混同しやすい
仮想プロキシの遅延初期化はスレッドセーフでない場合がある。マルチスレッド環境では synchronized や volatile を使った二重チェックが必要
アクセス制御プロキシで SecurityException を投げる場合、呼び出し側での例外処理を忘れないこと
java.lang.reflect.Proxy は動的プロキシを生成する標準 API だが、インターフェースにしか適用できない。クラスの動的プロキシには CGLIB などが必要
FAQ
Proxy はアクセス制御・遅延初期化など「実オブジェクトへのアクセスを仲介する」のが目的です。Decorator は「機能を追加する」のが目的で、構造は似ていますが設計意図が異なります。
単純な null チェックだけではスレッドセーフではありません。マルチスレッド環境では synchronized ブロックか volatile + double-checked locking を使ってください。
AOP 的な横断関心事(ログ・トランザクション・認証)を動的に適用したいときに使います。Spring AOP の内部でも同じ仕組みが使われています。