본문 바로가기
카테고리 없음

프록시 패턴 (Proxy Pattern)

by SuldenLion 2026. 4. 23.
반응형
프록시 패턴 — 소프트웨어 아키텍처 & 설계
Structural Pattern

프록시 패턴 (Proxy Pattern)

실제 객체에 대한 접근을 제어하기 위해 그 앞에 대리인(Proxy)을 두는 구조적 설계 패턴. 접근 제어, 지연 초기화, 로깅, 캐싱, 원격 통신 등 다양한 목적으로 활용된다.

Client
[ Proxy ]
RealSubject
분류 Structural GoF 포함 주요 변형 4가지 난이도 ★★★☆

프록시 패턴이란?

다른 객체에 대한 접근을 제어하기 위해 그 객체의 대리자(surrogate) 또는 자리채움자(placeholder)를 제공하라.

— GoF, Design Patterns: Elements of Reusable Object-Oriented Software (1994)

프록시(Proxy)는 라틴어 procuracy에서 유래한 단어로 '대리인'을 뜻한다. 클라이언트는 프록시 객체와 대화하고, 프록시는 내부적으로 실제 객체(RealSubject)에 요청을 위임한다.

핵심은 클라이언트가 프록시인지 실제 객체인지 구분하지 못한다는 점이다. 둘 다 동일한 인터페이스(Subject)를 구현하기 때문이다. 프록시는 요청이 실제 객체에 도달하기 이나 에 다양한 부가 로직을 수행할 수 있다.

💡
구조 원칙

Subject 인터페이스 → Proxy와 RealSubject가 모두 구현. Client는 항상 Subject 인터페이스를 통해 접근. Proxy는 내부적으로 RealSubject의 참조를 보유하고 위임(delegation)한다.


4가지 프록시 유형

GoF는 하나의 프록시 패턴을 정의했지만, 활용 방식에 따라 크게 네 가지 변형으로 분류한다.

Virtual Proxy
가상 프록시

생성 비용이 큰 객체의 초기화를 실제로 필요한 시점까지 지연시킨다. 지연 초기화(Lazy Initialization)를 구현하는 대표적인 방법.

예: 대용량 이미지, 무거운 DB 연결 객체
Protection Proxy
보호 프록시

실제 객체에 대한 접근 권한을 제어한다. 역할(Role)이나 권한(Permission)에 따라 요청을 허용하거나 거부한다.

예: 관리자 전용 API, 파일 시스템 접근 제어
Remote Proxy
원격 프록시

다른 주소 공간(서버, 다른 프로세스)에 있는 객체를 로컬에서 사용할 수 있는 것처럼 표현한다. 네트워크 통신을 캡슐화한다.

예: gRPC 스텁, RMI, REST 클라이언트 래퍼
Caching Proxy
캐싱 프록시

비용이 큰 연산의 결과를 캐싱하여, 동일한 요청에 대해 실제 객체를 호출하지 않고 캐시된 결과를 반환한다.

예: API 응답 캐싱, 이미지 캐시 레이어

구조 및 UML 다이어그램

«client» Client uses Subject «interface» Subject + request(): void «proxy» Proxy - realSubject: RealSubject + request(): void - checkAccess(): bool «real subject» RealSubject + request(): void // 실제 비즈니스 로직 delegates
구성 요소역할핵심 특징
SubjectProxy와 RealSubject가 공유하는 인터페이스Client는 이 인터페이스만 알면 됨
RealSubject실제 비즈니스 로직을 담는 실체 객체생성 비용이 크거나 보호가 필요하거나 원격에 존재
ProxyRealSubject와 동일한 인터페이스를 구현하며 요청을 가로챔RealSubject의 참조를 보유, 접근 전·후 부가 로직 수행
ClientSubject 인터페이스를 통해 객체를 사용Proxy인지 RealSubject인지 구분하지 않음

보호 프록시 (Protection Proxy)

역할(Role) 기반 접근 제어를 추가하는 시나리오다. 관리자만 데이터를 삭제할 수 있도록 프록시가 요청을 가로채 검증한다.

TypeScript Subject 인터페이스 & RealSubject
interface DatabaseService {
  read(query: string): string[];
  write(data: string): void;
  delete(id: string): void;
}

class RealDatabaseService implements DatabaseService {
  read(query: string)  { console.log(`[DB] SELECT WHERE ${query}`); return ["row1"]; }
  write(data: string)  { console.log(`[DB] INSERT ${data}`); }
  delete(id: string)   { console.log(`[DB] DELETE id=${id}`); }
}
TypeScript DatabaseServiceProxy — 역할 기반 접근 제어
type Role = "admin" | "editor" | "viewer";

class DatabaseServiceProxy implements DatabaseService {
  private real = new RealDatabaseService();
  constructor(private role: Role) {}

  read(query: string) {
    // ✅ 모든 역할 허용
    return this.real.read(query);
  }
  write(data: string) {
    // ✅ viewer 제외 허용
    if (this.role === "viewer") throw new Error("❌ 쓰기 권한 없음");
    this.real.write(data);
  }
  delete(id: string) {
    // 🔒 admin만 허용
    if (this.role !== "admin") throw new Error(`❌ ${this.role}는 삭제 권한 없음`);
    this.real.delete(id);
  }
}

// 클라이언트 코드 — Subject 인터페이스만 사용
const viewer: DatabaseService = new DatabaseServiceProxy("viewer");
const admin:  DatabaseService = new DatabaseServiceProxy("admin");

viewer.read("id=1");   // ✅ 성공
viewer.delete("1");  // ❌ Error
admin.delete("1");   // ✅ 성공

가상 프록시 (Virtual Proxy)

고해상도 이미지 시나리오다. 이미지 로딩은 비용이 크므로, 실제로 화면에 표시될 때까지 초기화를 지연시킨다.

TypeScript LazyImageProxy — 지연 초기화
interface Image { display(): void; }

class HighResolutionImage implements Image {
  constructor(private filename: string) {
    console.log(`🖼 [디스크 로드] ${filename} — 수초 소요`); // 생성자에서 즉시 로드
  }
  display() { console.log(`✅ [화면 표시] ${this.filename}`); }
}

class LazyImageProxy implements Image {
  private realImage: HighResolutionImage | null = null;
  constructor(private filename: string) {
    // 생성자에서 이미지를 로드하지 않는다!
  }
  display() {
    // 최초 호출 시에만 실제 객체 생성
    if (!this.realImage) {
      this.realImage = new HighResolutionImage(this.filename);
    }
    this.realImage.display();
  }
}

// 클라이언트 코드
const img: Image = new LazyImageProxy("photo_8k.jpg"); // 로드 없음
img.display();   // 이 시점에 디스크 로드 + 화면 표시
img.display();   // 두 번째 호출 — 로드 없이 즉시 표시
⚛️
실무 적용 포인트

React의 React.lazy()Suspense가 가상 프록시 패턴의 실제 구현체다. 컴포넌트가 렌더링되어야 하는 시점까지 번들 로딩을 지연시킨다.


JavaScript 내장 Proxy 객체

ES6부터 JavaScript에는 언어 내장 Proxy 객체가 존재한다. 객체의 기본 동작을 가로채는 핸들러(Handler)트랩(Trap)을 정의할 수 있다. Vue 3의 반응성 시스템(Reactivity System)은 이 JavaScript Proxy를 기반으로 구축되어 있다.

JavaScript get / set / deleteProperty 트랩 활용
const createValidatedProxy = (target) => {
  return new Proxy(target, {
    // get 트랩: 속성 읽기 시 실행
    get(obj, prop) {
      console.log(`[Proxy] GET: ${String(prop)}`);
      return prop in obj ? obj[prop] : `'${String(prop)}'이 없습니다`;
    },
    // set 트랩: 속성 쓰기 시 유효성 검사
    set(obj, prop, value) {
      if (prop === "age" && (typeof value !== "number" || value < 0)) {
        throw new TypeError("age는 0 이상의 숫자여야 합니다.");
      }
      obj[prop] = value;
      return true;
    },
    // deleteProperty 트랩: 속성 삭제 차단
    deleteProperty(obj, prop) {
      throw new Error(`[Proxy] 속성 삭제 금지: ${String(prop)}`);
    }
  });
};

const user = createValidatedProxy({ name: "Alice", age: 30 });
user.age = 25;    // ✅ 성공
user.age = -5;    // ❌ TypeError
delete user.name; // ❌ Error: 삭제 금지

현실의 프록시 패턴

🌐
Nginx / API Gateway

클라이언트 요청을 대신 받아 인증·로드밸런싱·SSL을 처리 후 백엔드로 전달하는 리버스 프록시.

⚛️
React.lazy() / Suspense

컴포넌트가 실제로 뷰포트에 진입해야 할 때 번들을 동적 로드하는 가상 프록시 구현체.

🟢
Vue 3 Reactivity

ES6 Proxy로 객체의 get/set을 가로채 의존성을 추적하고 DOM을 자동 업데이트한다.

Spring AOP

@Transactional, @Cacheable 등의 어노테이션은 동적 프록시를 생성해 메서드 호출을 가로챈다.

🔌
gRPC Stub

원격 서버의 서비스를 로컬 객체처럼 호출할 수 있게 해주는 원격 프록시(Remote Proxy).

📦
Hibernate Lazy Loading

연관 엔티티를 실제 접근 전까지 로드하지 않는 가상 프록시로 N+1 문제를 완화한다.


장단점

장점 (Advantages)
  • 클라이언트 코드 수정 없이 객체에 부가 기능을 추가
  • 접근 제어, 캐싱, 로깅을 한 곳에서 처리 (SRP 준수)
  • RealSubject가 준비되지 않았을 때도 Proxy가 응답 가능
  • OCP 준수 — 기존 코드 변경 없이 Proxy만 교체
  • 원격 객체를 로컬처럼 사용하는 추상화 제공
단점 (Disadvantages)
  • 클래스 수가 늘어나 코드베이스 복잡도 증가
  • Proxy 레이어 추가로 인한 응답 지연 발생 가능
  • 동적 프록시 디버깅이 까다로울 수 있음
  • Subject 인터페이스 변경 시 Proxy도 함께 수정 필요

유사 패턴과의 비교

패턴목적인터페이스핵심 차이
Proxy접근 제어 / 부가 기능 삽입동일한 Subject 구현RealSubject와 1:1 관계, 동일 인터페이스
Decorator기능 동적 추가동일한 인터페이스여러 Decorator 조합 가능, 기능 확장이 목적
Facade서브시스템 단순화새로운 단순 인터페이스여러 클래스를 묶음, 인터페이스가 다름
Adapter호환되지 않는 인터페이스 변환인터페이스 변환기존 코드를 새 인터페이스로 감쌈
⚠️
Proxy vs Decorator — 자주 묻는 혼동

둘 다 동일한 인터페이스를 구현하며 위임을 사용한다는 점에서 구조가 유사하다. Proxy는 접근 제어가 핵심이며 RealSubject의 생명주기를 관리한다. Decorator는 기능 확장이 목적이며 클라이언트가 여러 Decorator를 자유롭게 조합한다.


정리

프록시 패턴은 "실제 객체에 직접 접근하지 마라, 항상 대리인을 통해 접근하라"는 간접성(indirection)의 원칙을 패턴으로 구현한 것이다. 이 단순한 원칙 위에서 접근 제어, 지연 초기화, 원격 통신, 캐싱, 로깅이라는 완전히 다른 문제들을 하나의 구조로 해결한다.

현대 소프트웨어에서 프록시는 어디에나 있다. Nginx가 요청을 가로채고, Spring이 트랜잭션을 열고, Vue가 객체 변경을 감지하며, React가 컴포넌트 로딩을 지연시킨다. 이 모든 것이 프록시 패턴이 현실에서 동작하는 모습이다.

프록시 설계에서 가장 중요한 것은 클라이언트가 프록시의 존재를 알아채지 못해야 한다는 투명성(transparency)이다. Subject 인터페이스를 충실히 구현하고, 부가 로직은 최소화하며, RealSubject에 책임을 올바르게 위임하는 것이 좋은 프록시의 조건이다.

반응형

댓글