트렌드브리프
프록시 패턴, 원본 객체 접근을 제어하는 대리 객체

프록시 패턴, 원본 객체 접근을 제어하는 대리 객체

· 게시 · 약 4분 분량

프록시 패턴은 원본과 같은 인터페이스를 가진 대리 객체를 사이에 두어 접근을 제어하는 GoF 구조 패턴이다. 지연 로딩·권한 검사·원격 호출·캐싱에 쓰이며, 데코레이터·어댑터와의 차이까지 코드로 짚었다.

목차
  1. 프록시 패턴의 구조와 구성 요소
  2. 프록시의 네 가지 종류
  3. 가상 프록시를 코드로 구현하기
  4. 데코레이터·어댑터와 왜 헷갈리나

프록시 패턴은 원본 객체에 직접 접근하는 대신 같은 인터페이스를 가진 대리 객체를 사이에 끼워 접근을 제어하는 구조적 디자인 패턴이다. 대리 객체(프록시)가 호출을 먼저 받아 필요할 때만 원본을 만들거나, 권한을 검사하거나, 결과를 캐시한 뒤 원본에게 넘긴다. 에리히 감마를 비롯한 네 저자(흔히 GoF로 부른다)가 1994년 책 『Design Patterns』에서 정리한 스물세 개 패턴 가운데 하나다.

여기서 다루는 건 객체지향 코드 안에서 쓰는 GoF 프록시 패턴이다. 웹 서버 앞단에 두는 리버스 프록시나 사내망의 HTTP 프록시 설정은 이름만 겹칠 뿐, 이 글의 대상이 아니다.

프록시 패턴의 구조와 구성 요소

핵심은 '같은 인터페이스, 통제된 접근'이다. 클라이언트 코드 입장에서는 원본을 쓰는지 대리인을 쓰는지 구분할 수 없어야 한다. 그래서 프록시는 원본과 똑같은 인터페이스를 구현하고, 안으로는 원본 참조를 숨겨 둔다.

구성 요소는 세 가지다. 원본과 프록시가 함께 구현하는 인터페이스(Subject), 실제 일을 하는 원본(RealSubject), 그 앞을 지키는 대리인(Proxy). 클라이언트는 인터페이스 타입만 알면 되고, 손에 쥔 게 원본인지 프록시인지는 신경 쓰지 않는다.

실무 비유로는 신용카드가 은행 계좌의 프록시다. 계좌 잔액에 직접 손대는 대신 카드가 결제 요청을 받아 한도와 유효성을 확인한 뒤 실제 계좌로 넘긴다. 가게 입장에서 카드든 현금이든 '결제'라는 인터페이스는 똑같다.

프록시의 네 가지 종류

쓰임새에 따라 크게 네 갈래로 나눈다. 구조는 같고, 프록시가 원본에 넘기기 전에 무슨 일을 하느냐가 다르다.

의뢰인의 요청이 검사·도장을 든 접수처(프록시)를 먼저 거쳐 금고 속 원본 객체로 전달되는 선화 일러스트
요청은 대리 객체를 먼저 거친다 — 검사·기록·지연 로딩을 끼우고 원본 접근을 제어한다.

가상 프록시를 코드로 구현하기

아래는 가장 흔한 가상 프록시다. ImageProxy는 RealImage와 같은 Image 인터페이스를 구현하지만, 실제 파일 읽기는 display()가 처음 불릴 때까지 미룬다.

interface Image {
 void display();
}

// 원본: 만들 때 파일을 읽어 초기화 비용이 큼
class RealImage implements Image {
 private final String path;
 RealImage(String path) {
  this.path = path;
  loadFromDisk();  // 무거운 초기화
 }
 private void loadFromDisk() { /* ... */ }
 public void display() { System.out.println(path); }
}

// 프록시: 같은 인터페이스, 처음 display() 할 때만 원본 생성
class ImageProxy implements Image {
 private final String path;
 private RealImage real;
 ImageProxy(String path) { this.path = path; }
 public void display() {
  if (real == null) real = new RealImage(path);
  real.display();
 }
}

클라이언트는 ImageProxy 객체만 들고 있다가 필요한 순간에 display()를 부른다. 그 전까지 무거운 loadFromDisk()는 실행되지 않는다. 원본을 직접 썼다면 객체를 만드는 즉시 파일을 읽었을 것이다.

이런 프록시를 매번 손으로 짤 필요는 없다. 오라클 자바 공식 문서에 따르면 java.lang.reflect.Proxy의 newProxyInstance 메서드에 인터페이스 목록을 넘기면 런타임에 동적 프록시 객체가 만들어지고, 그 객체를 향한 모든 메서드 호출은 InvocationHandler의 invoke 메서드 한 곳으로 모인다. 스프링 프레임워크의 AOP가 트랜잭션이나 로깅 같은 부가 기능을 붙일 때 바로 이 JDK 동적 프록시(또는 CGLIB)를 쓴다.

데코레이터·어댑터와 왜 헷갈리나

셋 다 다른 객체를 감싸 위임(delegation)하는 모양이라 클래스 다이어그램이 닮았지만, 의도가 다르다. 프록시는 인터페이스를 그대로 두고 원본으로 가는 접근을 통제한다. 데코레이터는 인터페이스를 그대로 두되 원본에 기능을 덧붙인다. 어댑터는 아예 인터페이스를 바꿔, 서로 안 맞는 두 코드가 함께 돌아가게 이어 준다.

구분하는 실전 질문은 하나다. 감싼 이유가 뭔가? 접근을 늦추거나 막거나 대신하려고 감쌌으면 프록시, 기능을 추가하려고 감쌌으면 데코레이터, 모양이 안 맞아 끼워 맞추려고 감쌌으면 어댑터다. 프록시는 원본을 아직 안 만들었을 수도 있는 반면 데코레이터는 감쌀 원본이 반드시 먼저 있어야 한다는 점도 실무에서 갈리는 지점이다.

더 많은 글 보기 RSS 구독