cv.cm

코딩 · 디자인 패턴

프록시 패턴 예제로 쉽게 이해하기

7분 분량

핵심 답변

프록시(Proxy)는 실제 객체와 같은 인터페이스를 가진 대리 객체를 제공해 그 객체에 대한 접근을 제어하는 구조 패턴입니다. 클라이언트는 프록시를 실제 객체처럼 사용하고, 프록시는 각 호출을 언제 어떻게 전달할지 결정합니다. 대표적인 종류로 가상 프록시(지연 생성), 보호 프록시(접근 검사), 캐싱 프록시, 원격 프록시가 있습니다.

의도와 해결하는 문제

의도: 다른 객체에 대한 접근을 제어하기 위해 그 객체의 대리자 또는 자리표시자를 제공합니다. 프록시는 GoF(Gang of Four) 책에 실린 구조 패턴 중 하나입니다.

앱이 네트워크를 통해 날씨 서비스를 호출한다고 가정해 봅시다. 호출 한 번에 수백 밀리초가 걸리고 비용도 들며, 같은 도시가 분당 여러 번 요청되기도 합니다. 서비스 클래스는 수정할 수 없고, 호출하는 모든 곳에 캐시 코드를 넣고 싶지도 않습니다. 이때 프록시가 해법이 됩니다. 프록시는 같은 인터페이스를 구현하고 캐시를 보관하며, 캐시에 없을 때만 실제 서비스를 호출합니다. 호출하는 쪽은 전혀 바꿀 필요가 없습니다.

같은 발상은 다른 요구에도 적용됩니다. 비용이 큰 객체를 처음 사용할 때만 생성하거나, 호출 전에 권한을 검사하거나, 모든 호출을 기록하거나, 실제 객체가 다른 기계에 있다는 사실을 숨길 수 있습니다.

구조와 참여자

역할책임
서브젝트(Subject)실제 객체와 프록시가 공유하는 인터페이스로, 클라이언트가 둘 중 어느 쪽이든 사용할 수 있게 합니다
실제 서브젝트(Real subject)실제 작업을 수행하는 객체
프록시(Proxy)서브젝트 인터페이스를 구현하고, 실제 서브젝트에 대한 참조를 보관하거나 생성하며, 호출을 전달하기 전후에 제어 로직을 추가합니다
클라이언트(Client)서브젝트 인터페이스만 사용하며, 프록시가 끼어 있는지 알지 못합니다
프록시 종류제어하는 대상
가상 프록시비용이 큰 객체의 생성을 필요할 때까지 미룹니다
보호 프록시호출자가 해당 연산을 수행할 권한이 있는지 검사합니다
캐싱 프록시결과를 저장해 두었다가 반복 요청에 그대로 돌려줍니다
원격 프록시다른 프로세스나 기계에 있는 객체를 대신하고 통신을 처리합니다
로깅 프록시실제 객체를 바꾸지 않고 호출과 인자를 기록합니다

TypeScript 구현: 캐싱 프록시

WeatherService가 서브젝트이고, CachedWeatherService가 프록시입니다. 같은 메서드를 가지며, 캐시된 값이 없거나 너무 오래되었을 때만 실제 서비스로 전달합니다.

interface WeatherService {
  getTemperature(city: string): Promise<number>;
}
class RemoteWeatherService implements WeatherService {
  calls = 0;
  async getTemperature(city: string): Promise<number> {
    this.calls++; // imagine a slow HTTP request here
    return city.length * 3;
  }
}
class CachedWeatherService implements WeatherService {
  private cache = new Map<string, { value: number; time: number }>();
  constructor(private real: WeatherService, private maxAgeMs = 60_000) {}
  async getTemperature(city: string): Promise<number> {
    const hit = this.cache.get(city);
    if (hit && Date.now() - hit.time < this.maxAgeMs) return hit.value;
    const value = await this.real.getTemperature(city);
    this.cache.set(city, { value, time: Date.now() });
    return value;
  }
}
async function main() {
  const real = new RemoteWeatherService();
  const weather: WeatherService = new CachedWeatherService(real);
  await weather.getTemperature("Paris");
  await weather.getTemperature("Paris");
  console.log(await weather.getTemperature("Hanoi"), "real calls:", real.calls);
  // 15 real calls: 2
}
main();

클라이언트 변수의 타입이 WeatherService이므로, 의존성 주입 설정처럼 한 곳에서 실제 서비스와 프록시를 바꿔 끼울 수 있습니다.

JavaScript Proxy로 만드는 가상·보호 프록시

JavaScript에는 내장 Proxy 객체(ES2015)가 있습니다. 트랩(trap)이라 부르는 핸들러 함수로 프로퍼티 읽기, 쓰기, 함수 호출 같은 연산을 가로챕니다. 인터페이스마다 클래스를 작성하지 않고도 프록시를 만들 수 있는 편리한 방법입니다.

type Account = { owner: string; balance: number };
function readOnly<T extends object>(target: T): T {
  return new Proxy(target, {
    set(_obj, prop) {
      throw new Error("Cannot change " + String(prop) + ": read-only");
    },
  });
}
const account = readOnly<Account>({ owner: "Ana", balance: 100 });
console.log(account.balance); // 100
try {
  account.balance = 0;
} catch (e) {
  console.log((e as Error).message); // Cannot change balance: read-only
}

언제 쓰고 언제 쓰지 말아야 할까

  • 실제 클래스나 호출하는 쪽을 바꾸지 않고 지연 로딩, 접근 제어, 캐싱, 로깅, 원격 접근을 추가하고 싶을 때 사용합니다.
  • 라이브러리 클래스나 네트워크 클라이언트처럼 실제 객체를 직접 통제할 수 없을 때 사용합니다.
  • 단순한 함수 래퍼로 충분하다면 사용하지 않습니다. 함수 하나라면 memoize(fn) 같은 고차 함수가 클래스보다 간단합니다.
  • 느리거나 실패할 수 있는 원격 호출을 로컬처럼 보이는 프록시 뒤에 숨기지 마세요. 호출하는 쪽도 지연 시간과 오류를 처리해야 하므로, Promise를 반환하는 식으로 인터페이스에 드러내야 합니다.
  • 동일성에 주의하세요. 프록시는 실제 서브젝트와 다른 객체이므로 proxy === real은 false입니다.

실제 사용 사례

  • Vue 3는 JavaScript Proxy를 기반으로 반응성 시스템을 구축합니다. 프로퍼티를 읽으면 의존성이 기록되고, 쓰면 업데이트가 일어납니다.
  • Java는 런타임에 동적 프록시 클래스를 만드는 java.lang.reflect.Proxy를 제공하며, 프레임워크는 이를 이용해 인터페이스 주위에 트랜잭션, 보안 검사, 원격 호출을 추가합니다.
  • Hibernate 같은 객체 관계 매퍼(ORM)는 연관된 엔티티에 대해 지연 프록시를 반환하고, 처음 접근할 때 데이터베이스에서 실제 데이터를 읽어 옵니다.
  • 원격 프로시저 호출 라이브러리는 로컬 객체처럼 보이지만 호출마다 네트워크로 전송하는 클라이언트 스텁을 생성합니다.

관련 패턴

패턴프록시 패턴과의 차이
데코레이터구조는 같지만 목적이 다릅니다. 데코레이터는 새 동작을 추가하며 흔히 여러 겹으로 쌓습니다. 프록시는 접근을 제어하며 보통 실제 객체의 생명주기를 관리합니다.
어댑터어댑터는 객체에 다른 인터페이스를 제공하지만, 프록시는 인터페이스를 그대로 유지합니다.
퍼사드퍼사드는 서브시스템 전체에 더 단순한 새 인터페이스를 제공하지만, 프록시는 객체 하나를 대신합니다.
플라이웨이트플라이웨이트는 객체를 공유해 메모리를 절약하고, 가상 프록시는 객체 생성을 미뤄 시간과 메모리를 절약합니다.

자주 묻는 질문

프록시 패턴과 데코레이터 패턴의 차이는 무엇인가요?

둘 다 같은 인터페이스로 객체를 감쌉니다. 데코레이터는 책임을 추가하며 보통 클라이언트가 조합합니다. 프록시는 실제 객체에 대한 접근을 제어하며 그 객체를 직접 생성하거나 관리하는 경우가 많습니다.

JavaScript의 Proxy 객체는 프록시 패턴과 같은 것인가요?

패턴을 쉽게 구현하도록 해 주는 언어 기능입니다. JavaScript Proxy는 트랩을 통해 대상 객체에 대한 연산을 가로채는데, 이는 GoF에서 말하는 프록시가 하는 일과 정확히 같습니다.

가상 프록시란 무엇인가요?

가상 프록시는 큰 이미지처럼 생성 비용이 큰 객체를 대신합니다. 그 객체가 필요한 메서드가 처음 호출될 때 비로소 실제 객체를 생성합니다.

리버스 프록시 서버도 프록시 패턴의 예인가요?

네트워크 수준에서 같은 발상을 따릅니다. nginx 같은 서버가 같은 HTTP 인터페이스를 가진 다른 서버 앞에 놓여 캐싱, 접근 제어, 로드 밸런싱을 더해 줍니다. GoF 패턴은 하나의 프로그램 안의 객체에 같은 발상을 적용한 것입니다.

코딩 튜토리얼 전체