의도

  • 다른 객체에 대한 접근을 제어하기 위한 대리자 또는 자리채움자 역할을 하는 객체를 둠

 

다른 이름

  • 대리자(Surrogate)

 

언제 쓰는가 ?

문서 편집기에서 이미지를 불러올 때 이미지에 대한 proxy를 둘 수 있음. 이 proxy의 역할은 문서 편집기는 빠르게 동작하나 이미지 자체가 렌더링 되는 순간에 대해 신경쓰지 않아도 되게 끔 하는 역할을 할 수 있음. proxy 객체는 이미지의 크기가 얼마나 될지도 알고 있어야 하며, 그렇게 함으로서 이미지가 다 띄워지지 않아도 그 공간만큼은 확보할 수 있음.

  • 원격지 프록시(remote proxy)는 서로 다른 주소 공간에 존재하는 객체를 가리키는 대표 객체로, 로컬 환경에 위치함.
  • 가상 프록시(virtual proxy)는 요청이 있을 때만 필요한 고비용 객체를 생성함. 위 설명의 이미지 proxy가 여기에 해당.
  • 보호용 프록시(protection proxy)는 원래 객체에 대한 실제 접근을 제어함. 객체별로 접근 제어 권한이 다를 때 유용할 수 있음.
  • 스마트 참조자(smart reference)는 원시 포인터의 대체 객체로 실제 객체에 접근이 일어날 때 추가적인 행동을 함. 1) 참조 횟수 관리 2) 맨 처음 참조되는 시점에 영속적 저장소의 객체를 메모리로 옮김 3) 실제 객체에 접근하기 전에 다른 객체가 그것을 변경하지 못하도록 lock을 검. proxy의 사용예시로서 스마트 포인터가 쓰인다 정도로 이해하면 될 듯. 스마트 포인터 혹은 참조자는 프록시 성격 중 보호용 프록시의 성격을 띄는 것으로 보임.

 

구조

GoF의 디자인 패턴 중 P.282
GoF의 디자인 패턴 중 P.282

  • Proxy
    • 실제로 참조할 대상에 대한 참조자를 관리.
    • Subject와 동일한 인터페이스를 제공하여 실제 대상을 대체할 수 있어야 함.
    • 실제 대상에 대한 접근을 제어하고 실제 대상의 생성과 삭제를 책임짐.
    • Proxy의 종류에 따라 다음을 수행
      • 원격지 프록시는 요청 메시지와 인자를 인코딩하여 이를 다른 주소 공간에 있는 실제 대상에게 전달
      • 가상의 프록시는 실제 대상에 대한 추가적 정보를 보유하여 실제 접근을 지연할 수 있도록 해야 함
      • 보호용 프록시는 요청한 대상이 실제 요청할 수 있는 권한이 있는지 확인함
  • Subject : RealSubject와 proxy에 공통적인 인터페이스를 정의
  • RealSubject : 프록시가 대표하는 실제 객체

 

협력 방법

  • 프록시 클래스는 자신이 받은 요청을 RealSubject 객체에 전달

 

결과

  1. 원격지 프록시는 객체가 다른 주소 공간에 존재한다는 사실을 숨길 수 있음
  2. 가상 프록시는 요구에 따라 객체를 생성하는 등 처리를 최적화할 수 있음
  3. 보호용 프록시 및 스마트 참조자는 객체가 접근할 때마다 추가 관리를 책임짐. 객체를 생성할 것인지 삭제할 것인지를 관리함

 

구현 / 고려사항

  1. C++에서는 멤버접근 연산자를 오버로드함
    1. operator-> 연산을 오버로드하면 가독성 좋은 proxy 생성 가능
    2. proxy가 단순 포인터 역할만 할 때는 이것만으로 충분함
  2. proxy가 항상 자신이 상대할 실제 대상을 알 필요는 없음
    1. proxy 클래스는 추상 인터페이스를 통해서만 대상과 일을 하므로, 각 RealSubject 별로 proxy 클래스를 만들 필요가 없음. 그러나 RealSubject의 인스턴스를 생성해야 하는 시점이 되면 어떤 클래스의 인스턴스를 생성해야 하는지 알아야 함.

 

예제 코드

  • proxy의 보호하는 기능에 대해서 예제로 짜봤습니다.
  • Computer에 접근하기 위해선 ComputerProxy로만 접근할 수 있게 끔 구성하였고, 컴퓨터는 Mom만 켜고 끌 수 있게 끔 템플릿 메서드 AddAccessible을 구성하였습니다.
#include "Proxy1.h"

int main()
{
	ComputerProxy computer_proxy;
	Son son;
	Mom mom;

	computer_proxy.AddAccessible(&mom);
	//computer_proxy.AddAccessible(&son);	//error!!
	computer_proxy.TogglePower(&mom);
	computer_proxy.TogglePower(&son);

	return 0;
}

  • 꺼져있던 컴퓨터가 Mom이 전원을 조작함으로서 켜집니다.
  • 접근권한이 없는 son은 컴퓨터의 전원을 만지지 못합니다. 이 때 주석친 코드는 static_assert에 의해 컴파일 타임에 에러가 납니다.
//Proxy1.h

#pragma once
#include <set>

class Person;
class Mom;
class Son;

class Device
{
public:
	virtual ~Device() {}
	
	virtual void TurnOn() abstract;
	virtual void TurnOff() abstract;
};

class Computer : public Device
{
	friend class ComputerProxy;

private:
	virtual void TurnOn();
	virtual void TurnOff();

private:
	Computer() {}
};

class ComputerProxy : public Device
{
public:
	~ComputerProxy();

public:
	void TogglePower(Person* person);

	template<typename T>
	void AddAccessible(T* person);

private:
	virtual void TurnOn();
	virtual void TurnOff();

private:
	Device* operator->();

private:
	Device* computer = nullptr;
	bool bTurnOn = false;

	std::set<Person*> AccessiblePeople;
};

class Person
{
};

class Mom : public Person
{
};

class Son : public Person
{
};

template<typename T>
void ComputerProxy::AddAccessible(T* person)
{
	static_assert(std::is_base_of_v<Person, T>, "T Must be Derived From Person Class!!");
	static_assert(!std::is_same_v<Son, T>, "Son can't access to computer!!");

	AccessiblePeople.insert(person);
}
  • 일단 컴퓨터에게 공통의 인터페이스를 제공하기 위해 Device 클래스를 만들어 ComputerProxy와 Computer가 각각 상속 받았습니다.
  • 이 때 외부에서 전원을 Toggle하는 메서드만 노출하기 위해 Proxy에서 TurnOn, TurnOff에 대해 private으로 세팅해주었습니다.
  • Computer는 직접 초기화하지 못하도록 생성자를 private으로 감춰둡니다. 대신 ComputerProxy가 friend 선언을 함으로서 ComputerProxy가 Computer를 생성할 수 있는 유일한 클래스입니다.
  • 또한 외부에서 실제 객체(computer)에 직접 접근하지는 못하도록 operator-> 또한 private으로 감추고 operator-> 연산이 호출된 시점에 computer가 nullptr일 경우 생성합니다. 이렇게 구성함으로서 computer에 직접 접근하여 조작하는 것이 아닌 ComputerProxy가 유일한 컴퓨터 생성기이자 컴퓨터 조작 인터페이스가 됩니다.
  • 또한 ComputerProxy에 set 컨테이너를 두어 set 컨테이너에 속한 Person 들에 한해서만 ComputerProxy에 접근할 수 있게 구성하였고, 이 때 Person 클래스의 서브클래스 중에 Son에 대해서는 AddAccessible 메서드를 통해서 컨테이너에 포함시키지 못하도록 static_assert를 사용했습니다.
//Proxy1.cpp

#include "Proxy1.h"
#include <iostream>
#include <type_traits>

void Computer::TurnOn()
{
	std::cout << "컴퓨터 전원이 켜집니다" << std::endl;
}

void Computer::TurnOff()
{
	std::cout << "컴퓨터 전원이 꺼집니다" << std::endl;
}

ComputerProxy::~ComputerProxy()
{
	if (computer)
	{
		delete computer;
	}
}

void ComputerProxy::TogglePower(Person* person)
{
	if (AccessiblePeople.find(person) == AccessiblePeople.end())
	{
		std::cout << "접근 권한이 없습니다!!" << std::endl;
		return;
	}

	if (bTurnOn)
	{
		TurnOff();
	}
	else
	{
		TurnOn();
	}
}

void ComputerProxy::TurnOn()
{
	operator->()->TurnOn();
	bTurnOn = true;
}

void ComputerProxy::TurnOff()
{
	operator->()->TurnOff();
	bTurnOn = false;
}

Device* ComputerProxy::operator->()
{
	if (computer == nullptr)
	{
		computer = new Computer;
	}

	return computer;
}
  • ComputerProxy는 operator->를 통해 computer 객체를 생성하는 책임을 지며, 소멸자를 통해 computer를 만들었다면 지우고 가는 책임까지 집니다.
  • ComputerProxy는 단순히 Computer의 메서드를 대리 호출해주는 역할을 하며, 이 때 operator->() 를 통해 호출하게 되면 Device의 인터페이스의 시그니처만 사용하도록 강제하기도 하고, 생성 시점까지 제어할 수 있습니다.
  • 또한 실제 외부에 노출되는 유일한 연산인 TogglePower에서는 set 컨테이너에 포함된 person에 대해서만 접근하여 동작할 수 있게끔 구성되어있습니다.

'디자인 패턴 > 구조' 카테고리의 다른 글

플라이급(Flyweight)  (0) 2024.01.02
퍼사드(Facade)  (1) 2023.12.22
장식자(Decorator)  (0) 2023.12.22
복합체(Composite)  (2) 2023.12.21
가교(Bridge)  (2) 2023.12.21

+ Recent posts