프록시 패턴은 실제 객체(RealService
)에 대한 접근을 제어하거나 기능을 확장하기 위해, 같은 인터페이스를 구현한 대리 객체(Proxy
) 를 중간에 두는 구조이다. 클라이언트는 실제 객체가 아니라 프록시를 통해 접근하지만, 두 객체 모두 동일한 인터페이스를 구현하기 때문에 사용 방식은 동일하다.
ServiceInterface
RealService
와 Proxy
가 모두 구현하는 공통 인터페이스RealService
Proxy
ServiceInterface
를 구현하고 내부에 RealService
에 대한 참조를 가진다.RealService
에 작업을 위임한다.Client
ServiceInterface
만 알고 있으며, Proxy
와 RealService
를 구분하지 않고 사용한다.클라이언트는 ServiceInterface
타입을 통해 서비스에 접근하려고 한다.
이때 프록시 객체가 ServiceInterface
를 구현하고 있으므로, 클라이언트는 프록시를 서비스처럼 사용한다.
프록시는 내부에서 요청을 가로채어, 접근 제어나 로깅, 캐싱 등의 부가 작업을 수행한다.
이후 실제 처리가 필요하면, 프록시가 내부에 가진 RealService
객체를 호출하여 요청을 위임한다.
클라이언트는 실제 서비스인지 프록시인지 알 필요 없이 동일한 방식으로 인터페이스를 통해 동작을 수행한다.
// 1. ServiceInterface
interface Service {
request(): void;
}
// 2. RealService
class RealService implements Service {
request() {
console.log("RealService: 요청 처리");
}
}
// 3. Proxy
class LoggingProxy implements Service {
private realService: RealService;
constructor(realService: RealService) {
this.realService = realService;
}
request() {
console.log("Proxy: 요청 전 로깅");
this.realService.request();
console.log("Proxy: 요청 후 로깅");
}
}
// 4. Client
const service: Service = new LoggingProxy(new RealService());
service.request();
// 원래의 fetch API를 감싼 프록시 함수
function createFetchProxy(originalFetch: typeof fetch): typeof fetch {
return new Proxy(originalFetch, {
apply(target, thisArg, args) {
const [url, options] = args;
console.log(`[API 요청] ${options?.method || "GET"} ${url}`);
const start = Date.now();
return Reflect.apply(target, thisArg, args).then((res) => {
const duration = Date.now() - start;
console.log(`[응답 시간] ${url} - ${duration}ms`);
return res;
});
},
});
}
// 프록시 적용
const fetchWithLog = createFetchProxy(fetch);
// 사용
fetchWithLog("/api/user", { method: "GET" });
Proxy
Proxy
는 객체의 속성 접근, 설정, 삭제, 함수 호출 등 모든 동작을 가로채고 제어할 수 있는 JavaScript
의 내장 객체이다.
핸들러 객체를 통해 get
, set
, apply
, has
, construct
등 다양한 트랩(trap)을 정의하여 대상 객체의 동작을 조작할 수 있다.
const personProxy = new Proxy(person, {
get: (obj, prop) => {
if (!obj[prop]) {
console.log(
`Hmm.. this property doesn't seem to exist on the target object`
);
} else {
console.log(`The value of ${prop} is ${obj[prop]}`);
}
},
set: (obj, prop, value) => {
if (prop === "age" && typeof value !== "number") {
console.log(`Sorry, you can only pass numeric values for age.`);
} else if (prop === "name" && value.length < 2) {
console.log(`You need to provide a valid name.`);
} else {
console.log(`Changed ${prop} from ${obj[prop]} to ${value}.`);
obj[prop] = value;
}
},
});
Reflect
Reflect
는 Proxy
핸들러 내부에서 기존 객체의 기본 동작을 호출할 때 사용하는 표준 API 모음이다.
Reflect.get
, Reflect.set
, Reflect.apply
등은 해당 동작을 원래 방식대로 수행하며, 코드의 일관성과 가독성을 높이는 데 유용하다.
const personProxy = new Proxy(person, {
get: (obj, prop) => {
console.log(`The value of ${prop} is ${Reflect.get(obj, prop)}`);
},
set: (obj, prop, value) => {
console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
Reflect.set(obj, prop, value);
},
});
Reflect
는 Proxy
가 기본 동작을 "직접 구현"하는 대신, 안정적으로 "위임"할 수 있게 해준다.
예를 들어 target[prop]
처럼 직접 접근하면 예상치 못한 오류나 사이드 이펙트가 생길 수 있는데, Reflect.get(target, prop, receiver)
는 이 과정을 일관되고 안전한 방식으로 수행해준다.