포스트

Proxy Pattern

실제 객체 접근을 대리자가 가로채 제어하는 패턴. 가상/보호/원격 프록시 종류와, 비슷해 보이는 데코레이터 패턴과의 목적 차이.

Proxy Pattern

난이도 중급 · 선행 Decorator

한 줄 요약

실제 객체와 같은 인터페이스를 가진 대리자를 앞에 세워, 호출을 가로채면서 지연·캐시·권한 체크 같은 부가 동작을 끼워 넣는 패턴. 클라이언트는 대리자인지 진짜인지 모른 채로 호출한다.

어떤 문제를 푸는가

이미지 갤러리 앱을 만든다. 고해상도 이미지가 100장 있고, 각 이미지는 디스크에서 5MB를 로드한다. 단순하게 짜면 이렇다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Image {
public:
    Image(const std::string& path) {
        loadFromDisk(path); // 5MB 로드, 100ms 소요
    }
    void display() { /* 화면에 표시 */ }
private:
    void loadFromDisk(const std::string&) { /* ... */ }
};

int main() {
    std::vector<std::string> paths = {"img_0.jpg", "img_1.jpg", "img_2.jpg"};
    std::vector<Image> gallery;
    for (auto& path : paths) {
        gallery.emplace_back(path); // 화면에 보이지도 않는 이미지들을 다 로드
    }
    // 메모리·시간 낭비
}

문제:

  • 100장 중 사용자가 실제로 보는 건 화면에 보이는 4~5장.
  • 보이지도 않는 이미지를 미리 다 로드해서 메모리·시간을 낭비한다.
  • 생성과 동시에 무거운 작업이 일어나면 호출자가 비용을 컨트롤할 수 없다.

패턴 적용 후

Image와 같은 인터페이스를 가진 ImageProxy를 두고, display()가 실제로 호출될 때만 진짜 이미지를 로드한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Image {
public:
    virtual void display() = 0;
    virtual ~Image() = default;
};

class RealImage : public Image {
    std::string path;
public:
    RealImage(const std::string& path) : path(path) {
        loadFromDisk();
    }
    void display() override {
        std::cout << path << " 표시" << std::endl;
    }
private:
    void loadFromDisk() {
        std::cout << path << " 로드 (5MB)" << std::endl;
    }
};

class ImageProxy : public Image {
    std::string path;
    std::unique_ptr<RealImage> real;
public:
    ImageProxy(const std::string& path) : path(path) {}
    void display() override {
        if (!real) {
            real = std::make_unique<RealImage>(path); // 처음 호출 시점에 로드
        }
        real->display();
    }
};

int main() {
    std::vector<std::unique_ptr<Image>> gallery;
    for (int i = 0; i < 100; ++i) {
        gallery.push_back(std::make_unique<ImageProxy>("img_" + std::to_string(i) + ".jpg"));
    }
    // 여기까지 디스크 로드 0번, 메모리 0MB

    gallery[0]->display(); // 이때 img_0.jpg가 처음 로드됨
    gallery[0]->display(); // 두 번째 호출은 캐시된 RealImage 재사용
}

달라진 점:

  • 100개 객체를 생성해도 실제 로드는 0번.
  • 진짜 보여줘야 할 때만 디스크 I/O 발생.
  • 클라이언트 코드는 Image*만 다루므로 RealImage인지 ImageProxy인지 신경 쓰지 않는다.

구조

1
2
3
4
5
Client ──uses──▶ Subject (interface)
                    ▲
                    │
        ┌───────────┴───────────┐
   RealSubject              Proxy ──has──▶ RealSubject
  • Subject: 실제 객체와 프록시가 공통으로 구현하는 인터페이스 (Image)
  • RealSubject: 진짜 일을 하는 객체 (RealImage)
  • Proxy: Subject를 구현하면서 RealSubject로의 접근을 제어 (ImageProxy)

프록시 종류

같은 구조로 다양한 목적을 푼다.

종류용도
가상 프록시무거운 객체 lazy 생성위 이미지 예제, ORM의 lazy loading
보호 프록시권한·접근 제어관리자만 호출 가능한 메서드 가드
원격 프록시원격 객체를 로컬처럼gRPC stub, Java RMI
캐싱 프록시결과 캐시HTTP 캐시, DB 쿼리 캐시
스마트 레퍼런스접근 시 참조 관리 같은 부가 동작GoF가 든 프록시 동기 중 하나

보호 프록시 한 줄 예:

1
2
3
4
5
6
7
8
9
class AdminPanelProxy : public AdminPanel {
    std::unique_ptr<AdminPanel> real;
    User user;
public:
    void deleteAll() override {
        if (!user.isAdmin()) throw std::runtime_error("권한 없음");
        real->deleteAll();
    }
};

실전 사례

  • Hibernate / JPA lazy loading: @OneToMany(fetch = LAZY)로 가져온 컬렉션은 사실 프록시. 처음 접근하는 순간 SELECT가 나간다.
  • Spring AOP: @Transactional, @Cacheable은 메서드 호출 앞뒤에 프록시가 끼어들어 트랜잭션·캐시를 관리한다.
  • gRPC / RMI 스텁: 클라이언트는 로컬 객체처럼 호출하지만 실제는 네트워크 너머의 원격 프록시.
  • std::shared_ptr: GoF의 smart reference 동기와 관련은 있지만 pointee와 같은 인터페이스를 구현하는 전형적인 Proxy 객체는 아니다.

Decorator Pattern과의 차이

구조가 거의 같다. 둘 다 같은 인터페이스를 가진 객체로 감싼다. 차이는 의도.

 ProxyDecorator
무엇을 하나접근을 제어기능을 추가
누가 결정프록시 자체가 규칙을 가짐 (lazy, 권한 등)클라이언트가 어떤 데코를 쌓을지 정함
중첩 의도보통 한 겹여러 겹 쌓는 게 정상

같은 코드라도 이름을 LoggingService(데코)로 두느냐 AuthorizedService(프록시)로 두느냐로 의도가 드러난다.

자세한 비교는 Decorator 패턴 글 참고.

안티패턴 / 주의

  • 프록시가 RealSubject의 메서드를 무조건 forwarding만 하면 가치 없음. 가로채서 뭔가를 해야 프록시다. 빈 껍데기면 그냥 제거.
  • 가상 프록시는 첫 호출 비용이 튄다. 응답 시간이 균일해야 하는 곳(실시간 시스템)에는 부적합.
  • 프록시 안에서 RealSubject 생성 실패 처리 누락. lazy 생성이 실패하면 호출자는 갑자기 예외를 받는다. 생성 실패 정책이 명확해야 한다.

스스로 점검

1. 100개 ImageProxy를 만들어도 디스크 로드가 0번인 이유는?

ImageProxy 생성자는 path만 저장한다. RealImagedisplay()가 처음 호출되는 시점에 lazy로 만들어진다. 보이지도 않는 이미지에 비용을 쓰지 않는다.

2. Spring의 @Transactional이 메서드 호출 앞뒤로 트랜잭션을 시작·커밋하는 메커니즘은 어떤 패턴인가?

Proxy. Spring AOP가 런타임에 동적 프록시를 만들어, 진짜 객체 호출 앞뒤로 트랜잭션 코드를 끼워 넣는다. 비슷한 어노테이션(@Cacheable, @Async)도 모두 프록시 기반.

3. 가상 프록시(lazy loading)가 부적합한 시스템은?

응답 시간이 균일해야 하는 실시간 시스템. 첫 호출 시점에 무거운 로드 비용이 튀어서 latency 편차가 생긴다. 미리 로드(eager) 또는 워밍업이 필요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.