디자인 패턴/GoF

[디자인 패턴] 구조 패턴 (7) 프록시 Proxy

로파이 2021. 1. 30. 18:52

프록시 (Proxy) 패턴

 

1. 의도

  • 다른 객체에 대한 접근을 대신 수행하는 대리 객체를 둔다.

2. 활용

 

티스토리의 글과 같이 한 문서에는 글, 이미지, 링크 개체 등 여러 그래픽 요소가 포함되어 있을 수 있다. 문서를 화면에 띄우기 위해 모든 그래픽 요소를 그려야 하는데 모든 문서에 대한 필요 요소를 메모리에 올릴 필요는 없다. 오버레이와 같이 운영체제 기능도 있지만 사용자 코드로 필요한 객체만 메모리에 올릴 시간을 결정하도록 한다. 따라서 현재 보고 있는 문서 내용에 관련된 객체만 필요할 때 생성하는 방식으로 이미지와 같이 데이터 용량이 큰 객체에 적용한다.

 

프록시 패턴은 자신이 객체를 생성하지 않고 사용자로부터 접근이나 요청이 있을 때 생성된 객체를 사용하도록 한다. 이로 현재 보고 있는 문서에 대해서만 관련 이미지 객체를 메모리에 불러와 띄우도록 한다.

프록시 패턴 예시

ImageProxy라는 클래스는 기존 추상 클래스 Graphic과 동일한 인터페이스를 가지고 있기 때문에 Image와 같은 기능을 제공한다. 내부적으로 이미지 파일에 대한 경로를 저장하고 Draw() 연산 호출 시 참조하고 있는 이미지가 생성이 필요할 때 인스턴스화 하기 때문에 가지고 있는 Image 참조자에 대해 lazy initialization을 하고 있다.

 

Proxy는 대리자 역할로 다음의 활용을 가진다.

  • Remote Proxy: 서로 다른 주소 공간에 존재하는 객체르 가리키는 대표 객체
  • Virtual Proxy: 요청이 있을 때만 필요한 고비용 객체를 생성, ImageProxy가 그 예
  • Protection Proxy: 객체에 대한 실제 접근을 제어
  • Smart Reference: 객체를 참조하는 횟수를 저장하고 해당 객체를 아무도 참조하지 않을 때 객체를 해제한다. C++ 스마트 포인터와 같다. 

3. 참여자

 

구조 패턴 - 프록시 패턴

 

  • Proxy: 실제 참조할 대상에 대한 참조자를 관리한다. Subject와 동일한 인터페이스를 가지고 실제 대상에 대한 접근, 생성, 삭제를 책임진다.
  • Subject: RealSubject와 Proxy에 공통적인 인터페이스를 정의. RealSubject가 요청되는 곳에 Proxy를 사용할 수 있게 한다.
  • RealSubject: 프록시가 대표하는 실제 객체이다.

4. 협력 방법

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

5. 결과

프록시의 종류에 따라 다음과 같은 결과를 가진다.

  • Remote Proxy: 객체가 다른 주소 공간에 존재한다는 사실을 숨긴다.
  • Virtual Proxy: 요구에 따라 객체를 생성하는 등 처리를 최적화한다.
  • Protection Proxy: 보호용 프록시 및 스마트 참조자는 객체가 접근할 때마다 추가 관리를 책임진다.

6. 구현

 

C++의 멤버 접근 연산자 ->를 오버로드하는 방식

- Image 클래스와 ImageProxy 클래스

class Image
{
public:
	Image(const char*);
	virtual ~Image();

	// 실제 image를 메모리로 불러옴
	Image* LoadAnImageFile(const char*);
	void Draw(Point);
};

// ImagePtr 클래스내에서 사용하기 위해 외부 함수 선언
extern Image* LoadAnImageFile(const char*);

class ImagePtr {
public:
	ImagePtr(const char* imageFile);
	virtual ~ImagePtr();

	// 멤버 접근 연산자 오버로딩
	virtual Image* operator->();
	// 간접 접근 연산자 오버로딩
	virtual Image& operator*();
private:
	Image* LoadImage();
private:
	Image* _image;
	const char* _imageFile;
};

ImagePtr::ImagePtr(const char* theImageFile)
{
	_imageFile = theImageFile;
	_image = 0;
}

// 내부에서 필요할 때 Image 인스턴스를 생성함
Image* ImagePtr::LoadImage()
{
	if (_image == 0)
	{
		_image = LoadAnImageFile(_imageFile);
	}
	return _image;
}

Image* ImagePtr::operator->()
{
	return LoadImage();
}

Image& ImagePtr::operator*()
{
	return *LoadImage();
}

멤버 접근 연산자를 통해 Image 인스턴스를 이용하게 되는데, 오버 로딩된 함수 내용을 보면

LoadImage() 연산을 내부에서 호출하고 있고 이는 참조자가 할당되있지 않다면 Image 객체를 인스턴스 화하는 과정을 포함하고 있다.

 

따라서 실제 사용자 코드에서는 다음과 같이 보여진다.

ImagePtr image = ImagePtr("anImageFileName");
image->Draw(Point(50, 100));
// 멤버 접근 연산자 오버로딩
// (image->operator->())->Draw(Point(50, 100));

위 구현 예시에서는 접근 연산자를 통해 image 인스턴스를 이용하였지만 반드시 접근 연산자를 오버로딩해야하는 것은 아니다. 또한 실제 연산은 Image 클래스의 Draw() 연산을 이용하고 있기 때문에 완벽한 대리자 역할을 하고 있는 것은 아니다.

  

예제 코드

가상 프록시 패턴으로 Graphic, Image, ImageProxy를 구현해보자.

 

Graphic.h

Graphic 추상 클래스는 Image 클래스와 ImageProxy 클래스의 공통 인터페이스를 제공한다.

class Graphic
{
public:
	virtual ~Graphic();

	// 그리기 및 마우스를 이용한 이미지 크기 재조정
	virtual void Draw(const Point& aT) = 0;
	virtual void HandleMouse(Event& event) = 0;

	// 이미지 크기
	virtual const Point& GetExtent() = 0;

	// 파일 로드 및 저장
	virtual void Load(istream& from) = 0;
	virtual void Save(ostream& to) = 0;
protected:
	Graphic();
};

 

Image.h

Image 클래스는 Graphic 클래스를 상속받아 실제 객체로서 프록시가 Image 인스턴스를 통해 호출하는 함수를 구현한다. 

class Image : public Graphic
{
public:
	Image(const char* file);
	virtual ~Image();
	// Graphic 인터페이스와 동일
	virtual void Draw(const Point& at);
	virtual void HandleMouse(Event& event);

	virtual const Point& GetExtent();
	virtual void Load(istream& from);
	virtual void Save(ostream& to);
private:
	//...
};

 

ImageProxy.h

Image를 대리하는 ImageProxy 클래스

Draw(), HandleMouse(), GetExtent() 인터페이스는 반드시 Image 인스턴스를 반환하는 GetImage()->Func()의 형태로 구현한다.

GetImage()는 내부에서 _image가 할당되지 않았다면 Image 인스턴스를 생성하여 할당해준다. (lazy initialization)

class ImageProxy : public Graphic
{
public:
	ImageProxy(const char* imageFile);
	virtual ~ImageProxy();

	// Graphic 인터페이스와 동일
	virtual void Draw(const Point& at);
	virtual void HandleMouse(Event& event);

	virtual const Point& GetExtent();

	virtual void Load(istream& from);
	virtual void Save(ostream& to);
protected:
	// 실 객체를 접근 제어
	Image* GetImage();
private:
	// 프록시가 관리하는 변수
	Image* _image;
	Point _extent;
	char* _fileName;
};

// ImageProxy 초기화
ImageProxy::ImageProxy(const char* fileName)
{
	_fileName = strdup(fileName);
	_extent = Point::Zero();
	_image = 0;
}

// Image 인스턴스 접근
Image* ImageProxy::GetImage()
{
	if (_image == 0)
	{
		_image = new Image(_fileName);
	}
	return _image;
}

// GetExtent()를 통한 이미지 크기 반환
const Point& ImageProxy::GetExtent()
{
	if (_extent == Point::Zero())
		_extent = GetImage()->GetExtent();
	return _extent;
}

// 화면에 그리기
void ImageProxy::Draw(const Point& at)
{
	// 모든 접근은 GetImage를 통해 한다.
	GetImage()->Draw(at);
}

// 이미지 크기 재조정
void ImageProxy::HandleMouse(Event& event)
{
	GetImage()->HandleMouse(event);
}

// 이미지 크기 정보와 이름 저장
void ImageProxy::Save(ostream& to)
{
	to << _extent << _fileName;
}

// 이미지 크기 정보와 이름 불러오기
void ImageProxy::Load(istream& from)
{
	from >> _extent >> _fileName;
}

 

마지막으로 텍스트 문서를 의미하는 TextDocument 클래스를 정의하고 ImageProxy를 텍스트 문서에 삽입한다.

class TextDocument{
public:
	TextDocument();
    
    void Insert(Graphic*);
    // ...
};

Text Document* text = new TextDocument;
// ...
text->Insert(new ImageProxy("anImageFileName"));

 

vs 적응자 패턴

  • 적응자는 정의된 인터페이스와 다른 인터페이스를 호환하기 위해 사용한다.
  • 프록시는 대리 역할로 상대 인터페이스와 동일한 인터페이스를 가진다.

vs 장식자 패턴

  • 다른 클래스 인스턴스를 소유하고 함수를 내부에서 호출하는 점에서 비슷하지만 장식자 패턴은 기능을 추가하기 위해 덧대는 목적을 가진다. 또한 가장 바탕이되는 인스턴스가 먼저 호출되어야 하기 때문에 계층적 의미가 담겨 있다.
  • 프록시는 소유 인스턴스와 거의 동등한 입장을 가지며 사용자 인터페이스 호출시 대리자 역할로 객체 접근을 제어하고 실제 연산은 소유 인스턴스에게 넘긴다. 따라서 생성을 제외한 다른 기능은 거의 추가 되지 않는다.