jblog
Proxy 패턴
ArchitectureGuru 디자인 패턴 #12

Proxy 패턴

객체에 대한 접근을 제어하는 대리인, Proxy 패턴을 알아봅니다.

2026-01-239 min readdesign-pattern, structural, architecture

#대리인을 세워라

회사에서 CEO한테 직접 보고하는 건 부담스럽다.

그래서 비서가 있다. 비서는 CEO 대신 일정을 관리하고, 불필요한 미팅을 걸러주고, 중요한 것만 전달한다.

비서가 CEO를 대리하는 것이다. CEO의 기능을 그대로 제공하면서, 그 앞에서 추가적인 일을 처리한다.

소프트웨어에서도 똑같다.

// 이미지를 로드하는 데 5초가 걸린다고 해보자
const image = new HeavyImage("huge-photo.jpg"); // 5초 대기... 😴
image.display(); // 이제서야 보여줌

페이지를 열자마자 보이지도 않는 이미지를 5초 동안 로드한다면?

대리인(Proxy) 을 세워서 진짜 필요한 순간까지 로딩을 미루면 된다.

visible: false

Proxy란?

다른 객체에 대한 대리자 또는 자리표시자 역할을 하는 객체를 제공하여, 원본 객체에 대한 접근을 제어하는 패턴

Proxy는 원본과 같은 인터페이스를 가진다. 클라이언트는 원본을 쓰는지 Proxy를 쓰는지 모른다.

visible: false

Proxy의 종류

1. Virtual Proxy — 지연 로딩

무거운 객체를 실제로 필요할 때까지 생성하지 않는다.

interface Image {
  display(): void;
  getInfo(): string;
}
 
// 진짜 이미지 — 생성 시 무거운 로딩
class RealImage implements Image {
  private data: Buffer;
 
  constructor(private filename: string) {
    this.data = this.loadFromDisk(); // 무거운 작업!
    console.log(`📸 ${filename} 로드 완료 (5초 걸림)`);
  }
 
  private loadFromDisk(): Buffer {
    // 실제로는 디스크에서 읽어오는 무거운 작업
    return Buffer.from("image data");
  }
 
  display() {
    console.log(`이미지 표시: ${this.filename}`);
  }
 
  getInfo() {
    return `${this.filename} (${this.data.length} bytes)`;
  }
}
 
// Virtual Proxy — 필요할 때까지 로딩 안 함
class LazyImageProxy implements Image {
  private realImage: RealImage | null = null;
 
  constructor(private filename: string) {
    // 아무것도 로드하지 않음!
    console.log(`🔖 ${filename} 프록시 생성 (즉시)`);
  }
 
  private getRealImage(): RealImage {
    if (!this.realImage) {
      // 실제로 필요한 순간에 로드
      this.realImage = new RealImage(this.filename);
    }
    return this.realImage;
  }
 
  display() {
    this.getRealImage().display();
  }
 
  getInfo() {
    // 간단한 정보는 프록시가 직접 응답
    if (!this.realImage) {
      return `${this.filename} (아직 로드되지 않음)`;
    }
    return this.getRealImage().getInfo();
  }
}
// 100개의 이미지를 준비하지만 실제 로드는 0개
const gallery = Array.from(
  { length: 100 },
  (_, i) => new LazyImageProxy(`photo-${i}.jpg`),
);
// "🔖 photo-0.jpg 프록시 생성 (즉시)" × 100 — 순식간!
 
// 사용자가 첫 번째 이미지를 클릭했을 때만 로드
gallery[0].display();
// "📸 photo-0.jpg 로드 완료 (5초 걸림)"
// "이미지 표시: photo-0.jpg"

2. Protection Proxy — 접근 제어

권한에 따라 접근을 차단하거나 허용한다.

interface Document {
  read(): string;
  write(content: string): void;
  delete(): void;
}
 
class RealDocument implements Document {
  constructor(
    private title: string,
    private content: string,
  ) {}
 
  read() {
    return this.content;
  }
 
  write(content: string) {
    this.content = content;
  }
 
  delete() {
    console.log(`"${this.title}" 문서 삭제됨`);
  }
}
 
// Protection Proxy
class DocumentProxy implements Document {
  constructor(
    private document: RealDocument,
    private userRole: "viewer" | "editor" | "admin",
  ) {}
 
  read(): string {
    // 누구나 읽기 가능
    return this.document.read();
  }
 
  write(content: string): void {
    if (this.userRole === "viewer") {
      throw new Error("읽기 권한만 있습니다");
    }
    this.document.write(content);
  }
 
  delete(): void {
    if (this.userRole !== "admin") {
      throw new Error("관리자만 삭제할 수 있습니다");
    }
    this.document.delete();
  }
}
const doc = new RealDocument("회의록", "오늘 회의 내용...");
 
const viewerDoc = new DocumentProxy(doc, "viewer");
viewerDoc.read(); // OK
viewerDoc.write("x"); // Error: 읽기 권한만 있습니다
 
const adminDoc = new DocumentProxy(doc, "admin");
adminDoc.write("수정"); // OK
adminDoc.delete(); // OK

3. Caching Proxy — 결과 캐싱

interface WeatherApi {
  getWeather(city: string): Promise<WeatherData>;
}
 
class RealWeatherApi implements WeatherApi {
  async getWeather(city: string) {
    console.log(`🌐 API 호출: ${city} 날씨 조회`);
    // 실제 API 호출 (느림)
    const response = await fetch(`https://api.weather.com/${city}`);
    return response.json();
  }
}
 
class CachingWeatherProxy implements WeatherApi {
  private cache = new Map<string, { data: WeatherData; expiry: number }>();
 
  constructor(
    private api: RealWeatherApi,
    private ttl: number = 300000, // 5분
  ) {}
 
  async getWeather(city: string) {
    const cached = this.cache.get(city);
 
    if (cached && cached.expiry > Date.now()) {
      console.log(`💾 캐시 히트: ${city}`);
      return cached.data;
    }
 
    console.log(`🌐 캐시 미스: ${city} — API 호출`);
    const data = await this.api.getWeather(city);
    this.cache.set(city, { data, expiry: Date.now() + this.ttl });
    return data;
  }
}

visible: false

JavaScript의 내장 Proxy

ES6부터 Proxy 객체가 내장되어 있다.

const user = { name: "junbeom", age: 25 };
 
const userProxy = new Proxy(user, {
  get(target, prop) {
    console.log(`📖 ${String(prop)} 읽기`);
    return target[prop as keyof typeof target];
  },
 
  set(target, prop, value) {
    console.log(`✏️ ${String(prop)} 수정: ${value}`);
 
    if (prop === "age" && (value as number) < 0) {
      throw new Error("나이는 0 이상이어야 합니다");
    }
 
    (target as any)[prop] = value;
    return true;
  },
});
 
userProxy.name; // 📖 name 읽기 → "junbeom"
userProxy.age = 26; // ✏️ age 수정: 26
userProxy.age = -1; // Error: 나이는 0 이상이어야 합니다

JS에 이미 있었다

Vue 3의 반응성 시스템이 바로 이 Proxy를 사용한다.

// Vue 3의 반응성 — 내부적으로 Proxy 사용
const state = reactive({ count: 0 });
state.count++; // Proxy가 변경을 감지 → UI 자동 업데이트

visible: false

Proxy vs Decorator

둘 다 "감싸는" 패턴이라 헷갈릴 수 있다.

ProxyDecorator
목적접근을 제어기능을 추가
관계프록시가 원본의 생명주기 관리데코레이터는 원본에 의존
투명성클라이언트는 프록시 존재를 모름여러 겹 감싸기 가능
비유비서 (걸러주는 역할)옷 레이어 (기능 추가)

visible: false

언제 사용하면 좋을까?

  1. 지연 초기화 (Virtual Proxy): 무거운 객체를 나중에 로드
  2. 접근 제어 (Protection Proxy): 권한에 따른 접근 차단
  3. 캐싱 (Caching Proxy): API 결과나 계산 결과 캐싱
  4. 로깅/모니터링 (Logging Proxy): 접근 기록
  5. 원격 프록시 (Remote Proxy): 원격 서버의 객체를 로컬처럼 사용

visible: false

장단점

장점단점
원본 객체 수정 없이 접근 제어응답이 늦어질 수 있음
클라이언트가 변경 없이 사용코드 복잡도 증가
다양한 제어 가능 (캐싱, 권한, 지연)

visible: false

정리하며

Proxy는 원본 대신 대리인을 세워서 접근을 제어하는 패턴이다.

핵심을 한 줄로 요약하면,

"직접 가지 말고 대리인을 보내라"

JavaScript/TypeScript에서는 ES6 Proxy 객체 덕분에 매우 자연스럽게 구현할 수 있다. Vue 3의 반응성 시스템이 대표적인 실전 사례다.

이것으로 Structural 패턴 7개를 모두 다뤘다. 다음 글부터는 Behavioral 패턴으로 넘어가서, 객체들 간의 소통과 책임 분배를 다루는 패턴들을 알아보겠다. 첫 번째는 Chain of Responsibility 패턴이다.

visible: false

#Reference

Refactoring Guru - Proxy

refactoring.guru

댓글

댓글을 불러오는 중...