프록시 패턴
프록시 패턴(Proxy Pattern)은 객체에 대한 접근을 제어하거나 대리인 역할을 하는 객체를 두는 디자인 패턴으로, 클라이언트는 실제 객체와의 직접적인 상호작용 없이 프록시 객체를 통해 객체를 사용할 수 있다. 이를 통해 성능 최적화, 보안, 접근 제어 등 다양한 목적을 달성할 수 있으며, 예를 들어, 객체가 무겁거나 생성 비용이 높은 경우 프록시 객체는 지연 로딩하거나 필요한 시점에만 객체를 생성하여 효율성을 높일 수 있다. 또한, 프록시 패턴을 사용하면 보안 검사, 로깅, 캐싱 등 부가적인 기능을 실제 객체와 분리하여 처리할 수 있어 시스템의 유연성과 유지보수성을 높이고, 코드의 재사용성과 응집도를 향상시킬 수 있다.
Interface-Based Proxy
인터페이스 기반의 프록시 객체는 실제 객체와 동일한 인터페이스를 구현한다. 이를 통해 실제 객체와 동일한 메서드를 제공하지만, 메서드 호출 시 추가적인 처리(예: 로깅, 보안 검사, 지연 로딩 등)를 수행하거나, 실제 객체에 대한 접근을 제어할 수 있게 된다. 이 방식은 실제 객체와 프록시 객체 간의 호환성을 보장하면서도, 클라이언트는 프록시 객체를 통해 실제 객체를 간접적으로 사용하게 되어 코드의 유연성과 확장성을 높일 수 있다.
// 실제 객체
class RealSubject {
constructor() {
console.log("RealSubject: Created");
}
request() {
console.log("RealSubject: Handling request.");
}
}
// 프록시 객체
class Proxy {
constructor() {
this.realSubject = null; // 실제 객체는 지연 로딩
}
request() {
if (!this.realSubject) {
console.log("Proxy: Creating RealSubject object...");
this.realSubject = new RealSubject(); // 실제 객체 생성
}
this.realSubject.request(); // 실제 객체의 메소드 호출
}
}
// 사용
const proxy = new Proxy();
proxy.request(); // Proxy: Creating RealSubject object... -> RealSubject: Created -> RealSubject: Handling request.
proxy.request();
Inheritance-Based Proxy
인터페이스 기반의 프록시 객체는 실제 객체와 동일한 인터페이스를 구현해야 하므로, 실제 객체의 모든 메서드를 동일하게 구현하거나 덮어써야 한다. 때문에 코드가 복잡해지고, 실제 객체에 새로운 기능을 추가하려면 인터페이스를 확장하거나 수정해야 하는 경우가 발생할 수 있다. 이로 인해 기존 시스템의 호환성을 유지하면서도 새로운 기능을 추가하는 것이 어려울 수 있다. 게다가 인터페이스를 제공하지 않는 라이브러리의 객체에 대해서는 인터페이스 기반의 프록시 객체를 만들 수 없다.
상속 기반의 프록시 객체는 이러한 문제를 해결할 수 있다. 상속을 통해 실제 객체의 기능을 확장하거나 수정할 수 있기 때문에, 인터페이스를 구현할 필요 없이 기존 객체의 메서드를 쉽게 오버라이드할 수 있다. 또한, 인터페이스를 제공하지 않는 객체에도 상속을 이용하여 프록시를 적용할 수 있어 더 유연하게 사용할 수 있다.
// 실제 객체
class RealSubject {
request() {
console.log("RealSubject: Handling request.");
}
}
// 상속을 통해 프록시 객체 생성
class Proxy extends RealSubject {
constructor() {
super();
}
request() {
console.log("Proxy: Performing additional tasks.");
super.request(); // 실제 객체의 메소드 호출
}
}
Reflection-Based Dynamic Proxy
상속 기반의 프록시 객체는 원본 클래스의 모든 메서드를 오버라이드해야 하기 때문에, 프록시 클래스가 원본 클래스의 메서드와 유사한 코드 논리를 반복적으로 구현해야 하는 문제가 발생한다. 만약 기능을 추가해야 하는 원본 클래스가 많다면, 각 클래스마다 별도의 프록시 클래스를 만들어야 하므로, 클래스 수가 급격히 증가하고, 코드의 유지 관리 비용도 크게 증가한다. 또한, 각 프록시 클래스의 코드가 유사하다 보니 불필요한 개발 리소스가 낭비되고, 시스템의 복잡도가 증가하게 된다. 이는 코드의 중복을 초래하고, 확장성과 유지보수성을 떨어뜨릴 수 있다.
리플렉션 기반의 동적 프록시는 런타임에 객체의 메서드를 동적으로 생성하고 호출하는 방식이다. 이 방식은 인터페이스를 구현한 프록시 객체를 생성할 때, 실제 클래스의 메서드 호출을 리플렉션을 통해 처리한다. 즉, 프록시 객체는 컴파일 타임에 구체적인 메서드 구현을 가지고 있지 않고, 런타임에 메서드를 동적으로 바인딩하여 호출한다. 이를 통해 코드 중복을 줄이고, 다양한 객체에 대해 동일한 프록시 클래스를 재사용할 수 있다. 리플렉션 기반의 동적 프록시는 특히, 메서드 호출 시마다 동적으로 처리해야 할 작업이 많을 경우 유용하며, 클래스나 메서드의 변경이 발생하더라도 프록시 클래스를 수정할 필요 없이 유연하게 대응할 수 있다.
// 실제 객체 인터페이스 정의
interface Subject {
request(): void;
}
// 실제 객체 클래스 구현
class RealSubject implements Subject {
request(): void {
console.log("RealSubject: Handling request.");
}
}
// 프록시 객체 (리플렉션 기반)
const proxy = new Proxy(new RealSubject(), {
get(target: Subject, prop: string | symbol, receiver: any) {
if (prop === "request") {
console.log("Proxy: Intercepting request.");
}
return Reflect.get(target, prop, receiver);
}
});
// 사용
proxy.request();
자바스크립트가 제공하는 Proxy 객체와 Reflect에 대한 더 자세한 내용은 [ Proxy & Reflect ] 포스트에 소개되어있으니 참고!
블로그의 정보
Ayden's journal
Beard Weard Ayden