프록시 패턴
같은 인터페이스의 대리 객체를 통해 실제 객체의 생성이나 접근을 제어하는 구조 패턴.
· 마지막 업데이트: 2026-10-25 · 6 min read
개요
갤러리의 이미지가 실제로 표시되기 전에는 무거운 로딩을 미루고 싶다고 하자. 호출부마다 로딩 여부를 검사하는 대신, 이미지처럼 사용할 수 있는 대리 객체를 둘 수 있다.
프록시(Proxy)는 실제 객체 대신 같은 인터페이스로 요청을 받으며 접근을 제어한다. 이 예제는 처음 그릴 때 실제 객체를 만드는 지연 생성 프록시다.
UML
RealImage와 ImageProxy는 IImage를 구현한다. ImageProxy는 Lazy<RealImage>를 보관하고 Draw()가 호출되면 실제 객체에 위임한다.
역사
GoF는 1994년 Design Patterns에서 Proxy를 구조 패턴으로 정리했다. 다른 객체에 대한 대리자나 자리표시자를 두어 접근을 제어하는 구조다.1
왜 사용하는가?
호출부는 실제 이미지와 프록시를 같은 방식으로 사용한다. 로딩을 미룰지 같은 접근 제어 선택은 프록시 한곳에서 다룬다.
권한을 검사하는 프록시라면 요청마다 현재 권한을 확인해야 한다. 여기서는 생성 지연만 다루며, 인증이나 원격 통신을 구현했다고 간주하지 않는다.
예제
프록시를 만든 시점과 처음 Draw()를 부른 뒤의 실제 객체 수를 비교한다. 파일을 여는 대신 생성 횟수만 센다.
using System;
using System.Collections.Generic;
using System.Linq;
IImage image = new ImageProxy();
Console.WriteLine(RealImage.Created);
Console.WriteLine(image.Draw());
Console.WriteLine(RealImage.Created);
// 실제 객체와 대리 객체를 호출부가 같은 방식으로 쓰게 한다.
interface IImage { string Draw(); }
sealed class RealImage : IImage
{
public static int Created { get; private set; }
public RealImage() => Created++;
public string Draw() => "draw image";
}
// 생성 시점만 제어하고 실제 작업은 생성한 객체에 위임한다.
sealed class ImageProxy : IImage
{
private readonly Lazy<RealImage> image = new(() => new RealImage());
public string Draw() => image.Value.Draw();
}
0
draw image
1프록시 생성만으로 RealImage가 만들어지지는 않는다. Draw()가 Lazy.Value에 접근한 뒤 하나가 생성된다.
범위 넓힌 예제
같은 프록시로 두 번 그려도 실제 객체는 한 번만 생성된다.
using System;
using System.Collections.Generic;
using System.Linq;
IImage image = new ImageProxy();
Console.WriteLine(image.Draw());
Console.WriteLine(image.Draw());
Console.WriteLine("created: " + RealImage.Created);
// 실제 객체와 대리 객체를 호출부가 같은 방식으로 쓰게 한다.
interface IImage { string Draw(); }
sealed class RealImage : IImage
{
public static int Created { get; private set; }
public RealImage() => Created++;
public string Draw() => "draw image";
}
// 생성 시점만 제어하고 실제 작업은 생성한 객체에 위임한다.
sealed class ImageProxy : IImage
{
private readonly Lazy<RealImage> image = new(() => new RealImage());
public string Draw() => image.Value.Draw();
}
draw image
draw image
created: 1Lazy가 재사용하는 것은 실제 객체다. 실제 객체의 Draw()는 매 호출마다 실행하므로 결과 데이터 캐시와는 구별된다. 실제 자원이 파일이나 GPU 객체라면 해제 책임도 정해야 한다.
TypeScript 예제
호출부는 IImage로 그리기를 요청하고, 프록시는 실제 객체를 언제 만들지 결정한다. 생성 지연 조건을 화면마다 반복하지 않으려고 접근 제어와 실제 그리기를 나눈다.
// 실제 객체와 대리 객체를 호출부가 같은 방식으로 쓰게 한다.
interface IImage { draw(): string; }
class RealImage implements IImage {
static created = 0;
constructor() { RealImage.created++; }
draw(): string { return "draw image"; }
}
// 생성 시점만 제어하고 실제 작업은 생성한 객체에 위임한다.
class ImageProxy implements IImage {
#image: RealImage | undefined;
draw(): string {
this.#image ??= new RealImage();
return this.#image.draw();
}
}
const image: IImage = new ImageProxy();
console.log(RealImage.created);
console.log(image.draw());
console.log(image.draw());
console.log("created: " + RealImage.created);0
draw image
draw image
created: 1TypeScript에서는 #image에 실제 객체를 보관해 C# Lazy<RealImage>의 지연 생성 역할을 표현했다. draw()의 결과를 캐시하는 것이 아니라 객체를 재사용하며, 실제 이미지 파일은 읽지 않는다.
남용하면 생기는 문제
프록시가 파일·네트워크 접근이나 재시도를 숨기면 겉보기에는 같은 메서드여도 지연 시간과 실패 방식이 달라진다. 호출부가 동기 계산이라고 믿게 둔 채 원격 호출을 끼워 넣으면 투명한 대리라는 장점이 오히려 장애 원인을 가린다. 실제 I/O를 넣을 때는 취소·실패·실행 횟수 계약을 함께 드러내야 한다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.