책정리/GoF 디자인 패턴

6장 복제를 통한 객체 생성문제(Prototype패턴)

GONII 2019. 1. 15. 21:48
이미 생성된 객체를 복제해서 새로운 객체를 생성 있다.
  • 문제 사례 설명
그래픽 편집기를 객체지향 개념에 바탕을 두고 설계를 한다면, 문서에 추가되는 각각의 그래픽 요소는 객체로 표현될 있을 것이다. , 문서에 새로 끌어다 놓여지는 그래픽 요소 각각이 객체로 생성될 것이고, 문서는 이들을 포함하는 형태가 것이다.
여기서 우리가 고민해볼 문제는 각각의 그래픽 요소들이 문서에 추가될 어떤 방식으로 객체를 생성해주는 것이 가장 쉬우면서도 추후의 변경 등에 유연하게 대처할 있는 형태가 것인가라는 점이다.
  • 다양한 접근 방법 및 PROTOTYPE 패턴
주어진 문제를 요약하면, 팔레트에 정의된 그래픽 요소들을 객체로 생성하는 있어 가장 바람직한 방법은 무엇인가라는 것이다.
여기서 감안해야 것은 팔레트의 그래픽 요소는 계속해서 추가될 있다는 사실이다. 새로운 자료형이 얼마든지 추가될 있다는 것이다.
  • 기본적인 방법: 직접적인 객체 생성 방식
일차적으로 생각해볼 있는 방식은 그래픽 요소에 대한 객체를 직접 생성하는 것이다. , 이용자가 팔레트에서 그래픽 요소를 선택하면, 선택된 그래픽 요소가 무엇인지를 판단해서 그것에 해당하는 객체를 직접 생성하는 것이다.
이러한 방식으로 객체가 생성된다고 가정했을 , 클래스간 구조 객체 생성 로직을 가장 코드로 나타낸다면 [그림6-2] 같을 것이다.


[그림6-2] 부분으로 구성되어 있음을 있다. 하나는 이용자가 팔레트에서 선택한 도형의 종류를 판별해서 거기에 맞는 객체를 생성하는 것이고, 다른 하나는 이용자가 선택한 도형을 끌어다놓기 위해 마우스 버튼을 누른채 이동하는 동안 객체를 그려주기 위한 것이며, 마지막은 생성된 객체를 현재 문서 객체에 추가하는 것이다. 이중에서 우리가 논의해야 것은 첫번째 단계에 해당하는 객체 생성 부분인데, 부분은 switch문장을 활용해서 이용자가 팔레트에서 선택한 도형의 종류를 판별하고, 판별된 도형에 해당하는 객체를 직접 생성해주는 방식을 취하고 있다.
switch 문장 형태는 그래픽 요소의 개수가 많아지면 그만큼 소스코드의 길이도 길어지고 복잡해질 밖에 없다. 더구나 새로운 형태의 도형이 추가된다면, 이를 반영하기 위해 switch 문장의 수정이 불가피할 것이다.
[그림6-2] 직접 객체 생성 방식은 잦은 수정이 불가피한 단점이 있다. 더구나 새로운 형태의 도형이 추가된다면, 이를 반영하기 위해 switch 문장의 수정이 불가피할 것이다.
  • 좀더 나은 방법: 객체 생성 대행 함수 활용 방식
직접적으로 객체를 생성하는 방식의 문제점을 해결하기 위해 간접적으로 객체를 생성하는 방식을 고려해보자. 5장에서 다루었던 Factory Method 패턴이 대표적인 방식일 것이다.
Factory Method 패턴이 적용되려면 각각의 그래픽 요소에 대해 이들을 객체로 생성시켜 주기 위한 클래스가 정의되어야 한다. 예를 들어 Triangle 클래스의 삼각형 객체를 생성시켜주기 위해서는 TriangleCreate 클래스가 필요할 것이고, Rectangle 클래스의 사각형 객체를 생성시켜주기 위해서는 RectangleCreator 클래스가 필요할 것이다.
한편 그래픽 편집기 입장에서는 각각의 그래픽 요소를 생성하기 위해 이들을 생성해주는 객체를 별도로 가지고 있어야 것이다.
이같은 Factory Method 패턴 방식을 적용한 클래스 구조와 가상 코드를 살펴보면 [그림6-3] 같을 것이다.


그러나 그림의 가상 코드에서 보듯이 직접 객체를 생성하는 방식에서 문제로 지적되었던 switch 문장은 여전히 남아있다. 따라서 Factory Method 패턴을 적용하였지만, 새로운 그래픽 요소가 추가될 경우 switch 문장을 수정해야 하는 문제나 그래픽 요소가 많을 경우 switch 문장이 길고 복잡해지는 문제는 전혀 해결되지 않았다. 더구나 방식은 그래픽 요소의 개수만큼 이들을 생성해주기 위한 클래스도 정의해야 하고, 각각의 그래픽 요소에 대해 그것을 생성해주기 위한 객체를 그래픽 편집기가 데이터 멤버로 가져야 하는 다른 문제를 안고 있다.
  • 패턴 활용 방법: Prototype 패턴
앞서 살펴본 방식들은 이용자가 팔레트의 그래픽 요소를 선택하면, 선택된 그래픽 요소가 무엇인지를 판별하여 거기에 알맞은 자료형의 객체를 생성하는 방식을 취하고 있다. 그러나 이런 방식의 문제는 바로 이용자가 선택한 그래픽 요소가 무엇인지를 판별하기 위해 switch 문장과 같은 조건 비교 문장이 포함되어야 한다는 점이다. 왜냐하면 switch 문장과 같은 조건 비교 문장이 포함될 경우 프로그램이 지저분하고 복잡해질 뿐만 아니라 새로운 그래픽 요소가 추가될 때마다 프로그램의 수정이 불가피하기 때문이다.
앞서 살펴본 방식들이 가지는 문제의 원인은 이용자가 선택한 그래픽 요소가 무엇인지를 판별하는 switch 같은 비교 문장이 포함되어 있다는 것이다. 따라서 이를 역으로 생각해보면 switch 같은 비교 문장을 포함하지 않는 형태의 접근 방식이 문제를 해결할 있는 바람직한 방법이 것이다.
그러나 여기서 문제가 되는 것은 switch 같은 비교 문장이 없을 경우 이용자가 선택한 그래픽 요소에 대해 객체를 생성하려고 객체의 자료형을 결정할 없다는 것이다. 그런데 정적 자료형(Static-type) 기반으로 하는 C++ 같은 프로그래밍 언어에서는 객체를 생성하기 위해 생성할 객체의 자료형을 결정해주는 것이 필수적이다. 따라서 일반적인 방법으로 객체를 생성한다면 생성할 객체의 자료형을 판별하기 위한 비교 문장을 제외시키기는 어렵다.
일반적으로 객체를 생성하는 방식은 객체를 생성할 시점에 생성할 객체의 자료형을 정하는 형태를 취하고 있으며, 기존에 생성된 객체와는 무관하게 객체의 생성이 이루어진다. 그러나 우리가 해결해야 문제는 바로 객체를 생성할 시점에 객체의 자료형을 별도로 정하지 않도록 해야 한다는 것이다. 물론 그렇다고 생성되는 객체의 자료형이 정해지지 않아도 된다는 것을 의미하지는 않는다. 다만 생성되는 객체의 자료형이 객체를 생성할 시점에 따로 정하지 않아도 이미 결정되어 있어야 한다는 것이다.
일반적으로 객체 생성 방법들은 기존에 생성된 객체와는 무관하게 새로운 객체를 생성하는 형태라고 하였다. 따라서 매번 객체를 생성하면서 자료형을 정해주어야 하는 것이다. 기존의 객체를 복제해서 새로운 객체를 생성한다면, 새롭게 생성된느 객체는 기존의 객체와 동일한 자료형을 가질 것이기 때문에 객체를 생성할 때마다 객체의 자료형을 정해줄 필요가 없을 것이다. 따라서 자연스럽게 생성할 객체의 자료형을 판별하기 위한 비교 문장이 불필요하게 것이다. 또한 새로 생성되는 객체는 기존 객체와 동일한 자료형을 가지기 때문에 모든 객체는 자료형이 정해져야 한다는 조건도 만족하게 된다.
이처럼 객체를 생성함에 있어 기존 객체를 복제해서 객체를 생성하는 방식이 Prototype패턴이다.


[그림6-4]에서 Graphic 하위 클래스들은 모두 Clone()이라는 멤버 함수를 가지고 있는데 이를 통해 자신과 동일한 객체를 복제해주게 된다. 한편 특정 그래픽 요소를 선택했을 그에 해당하는 객체를 생성해주기 위한 GraphicEditor클래스는 이전 방식들과는 달리 이용자가 선택한 객체의 포인터를 pSelected라는 변수로 전달받아 그것과 동일한 객체를 복제 생성해준다. 이런 과정을 거치므로 Prototype 패턴이 적용된 경우에는 앞서 언급했던 방식들과는 달리 비교 문장이 불필요하게 되고, 이로 인해 멤버 함수 내의 구현이 간단해지게 되며, 새로운 그래픽 요소가 추가되더라도 멤버 함수 내부의 구현을 변경하거나 추가할 필요가 전혀 없어지게 된다.
물론 같은 동작이 가능하기 위해서는 그래픽 편집기의 팔레트가 단순히 그래픽 요소들을 GUI 형태로 나열해주는 역할만 하는 것이 아니라, 그래픽 요소에 대응하는 객체를 미리 생성해서 관리하는 형태여야 것이다. 그런 다음 이용자가 특정 그래픽 요소를 선택하게 되면 팔레트는 선택된 그래픽 요소에 해당하는 객체를 원본 객체로 삼고 문서에는 새로운 객체를 복제해서 추가하는 형태로 작업을 수행해야 것이다.
Prototype패턴이 적용되었다 하더라도 프로그램 수정없이 동적으로 팔레트에 새로운 그래픽 요소를 추가해서 동작할 있게 하기 위해서는 다른 고려가 있어야 한다. 이것은 11장에서 다룰 Composite 패턴을 같이 적용해야 해결할 있는 문제로 [그림6-5] 같은 형태의 클래스 구조 동작 형태를 가질 것이다.
[그림6-5]에서는 GraphicComposite 클래스가 추가되었는데, 클래스는 Graphic 클래스 하위 클래스의 객체들을 구성요소로 가질 있다. 이를 통해 클래스의 객체는 여러 개의 기본 도형이나 또다른 GraphicComposite 객체가 합쳐진 형태로 객체가 만들어질 있는 방법을 제공한다. 예를 들어 삼각형과 사각형을 합쳐 모양의 객체를 정의하고 이를 팔레트에 등록하고 싶다고 하자. 그러면 삼각형 객체와 사각형 객체를 구성 요소로 하는 GraphicComposite 객체를 생성하고 이를 팔레트에 등록하면 된다. 또한 모양의 GraphicComposite 객체가 팔레트에 등록되었을 이와 동일한 객체를 복제해서 생성해주기 위한 방법은 간단히 GraphicComposite 객체를 구성하는 각각의 components 객체에 대한 Clone() 멤버 함수를 불러주면 된다.
이처럼 Prototype 패턴은 기존 객체를 복제해서 객체를 생성하는 방법으로 그래픽 편집기 문제에 이를 적용할 경우 새로운 객체를 생성할 때마다 일일이 자료형을 선정해줄 필요가 없기 때문에 매우 유용하다. 더구나 Prototype 패턴은 새로운 그래픽 요소를 추가하더라도 프로그램의 수정없이 이를 반영할 있는 유연함을 가진다.
  • 샘플 코드
[소스6-1] Prototype 패턴 적용 샘플 코드
#include <iostream>
#include <list>
#include <map>
using namespace std;
 
class Position
{
public :
Position(){}
Position(int x, int y) { x_ = x; y_ = y; }
int x_, y_;
};
 
class Graphic
{
public :
virtual void Draw(Position& pos) = 0;
virtual Graphic* Clone() = 0;
};
 
class Triangle : public Graphic
{
public:
void Draw(Position& pos) {}
Graphic* Clone() { return new Triangle(*this); }
};
 
class Rectangle : public Graphic
{
public :
void Draw(Position& pos){}
Graphic* Clone() { return new Rectangle(*this); }
};
 
class GraphicComposite : public Graphic
{
public:
void Draw(Position& pos){}
Graphic* Clone()
{
GraphicComposite* pGraphicComposite = new GraphicComposite(*this);
list<Graphic*>::iterator iter1;
list<Graphic*>::iterator iter2;
iter2 = pGraphicComposite->components_.begin();
 
for (iter1 = components_.begin(); iter1 != components_.end(); iter1++)
{
Graphic* pNewGraphic = (*iter1)->Clone();
*iter2 = pNewGraphic;
iter2++;
}
return pGraphicComposite;
}
private:
list<Graphic*> components_;
};
 
class Document
{
public:
void Add(Graphic* pGraphic){}
};
 
class Mouse
{
public:
bool IsLeftButtonPushed()
{
static bool isPushed = false;
// gui 함수 활용 Left Button 상태 체크
isPushed = !isPushed;
return isPushed;
}
 
Position GetPosition()
{
Position pos;
// gui 함수 활용 현재 마우스 위치 파악
return pos;
}
};
Mouse _mouse; // gloval Variable
 
class GrahpicEditor
{
public:
void AddNewGraphics(Graphic* pSelected)
{
Graphic* pObj = pSelected->Clone();
while (_mouse.IsLeftButtonPushed())
{
Position pos = _mouse.GetPosition();
pObj->Draw(pos);
}
curDoc_.Add(pObj);
}
private:
Document curDoc_;
};
 
class Palette
{
public:
Palette()
{
Graphic* pGraphic = new Triangle;
item_[1] = pGraphic;
 
pGraphic = new Rectangle;
item_[2] = pGraphic;
 
// 피룡한 만큼 등록
}
void RegisterNewGraphic(Graphic* pGraphic)
{
item_[item_.size() + 1] = pGraphic;
}
Graphic* GetSelectedObj()
{
return item_[GetItemOrder()];
}
int GetItemOrder()
{
int i = 1;
Position curPos = _mouse.GetPosition();
// 현재 마우스 위치가 몇 번째 항목을 지정하는지 판별
return i;
}
private:
map<int, Graphic*> item_;
};
 
int _tmain(int argc, _TCHAR* argv[])
{
Palette palette;
GrahpicEditor ged;
ged.AddNewGraphics(palette.GetSelectedObj());
 
return 0;
}
[소스6-1]에서 주의깊게 살펴보아야 것은 클래스의 객체를 복제해주는 Clone() 멤버 함수의 구현 방식이다. C++ 경우 Clone() 멤버 함수는 간단히 복제 생성자(Copy Constructor) 이용해서 구현할 있다. 다만 기본적으로 제공해주는 복사 생성자는 얕은 복사가 이루어지므로, 포인터 변수가 가리키는 내용물까지 복사하려면 복사 생성자를 별도로 정해 깊은 복사가 이루어질 있도록 구현해야 한다.
GraphicComposite 클래스의 Clone() 멤버 함수는 구현이 다소 복잡한데, 이는 모양과 같이 여러 개의 기본 그래픽 요소들이 합쳐진 객체를 복제하기 위한 것이다. GraphicComposite 클래스 객체는 자신을 복제하기 위해 자신을 구성하는 객체들의 Clone() 멤버 함수를 반복적으로 불러주는 형태를 취하고 있다.
하나 주의 깊게 보아야 부분은 Palette 클래스이다. 여기서 Palette 클래스는 새로운 그래픽 요소를 프로그램의 수정없이 동적으로 등록할 있도록 하기 위해 RegisterNewGraphic() 멤버 함수를 제공하고 있다. 물론 이때 프로그램의 수정없이 새로 등록되는 객체는 모두 기본 도형 여러 개가 조합된 GraphicComposite 클래스의 객체일 것이다. 왜냐하면 완전히 새로운 종류의 기본 도형 추가는 어쩔 없이 프로그램의 수정을 필요로 하기 때문이다.
이처럼 Prototype 패턴은 객체를 생성함에 있어 기존 객체를 복제해서 새로운 객체를 생성하는 것이며, 이때 주로 사용하는 것은 복사 생성자라는 것을 알아 보았다.
  • 구현 관련 사항
먼저 고려해볼 필요가 있는 것은 Prototype 패턴을 적용할 대상이 일정하게 지정되어 있는지, 아니면 동적으로 추가, 삭제될 가능성이 있는지 하는 것이다. 만약 Prototype 패턴을 적용할 대상이 미리 일정하게 정해져 있다면 복제해줄 원본 객체를 프로그램 실행 초기에 일괄적으로 생성해서 관리하는 것이 편리할 것이다. 그러나 반대로 Prototype 패턴을 적용할 대상이 동적으로 추가, 삭제될 가능성이 있는 경우에는 복제해줄 원본 객체를 생성하고 관리하는 역할을 담당 다른 객체가 존재하는 것이 유용할 것이다.
이처럼 복제할 원본 객체가 동적으로 추가, 삭제될 있는 환경에서 원본 객체들을 모아 관리하는 역할을 담당하는 객체를 Prototype Manager라고 한다. 일반적으로 이런 Prototype Manager 객체는 원본 객체를 등록, 관리하기 위해 registry 데이터 멤버를 가지며, registry 원본 객체에 대한 복제 요청이 있을 경우 이를 쉽게 찾아 복제해줄 있도록 하기 위해 값을 기준으로 원본 객체를 검색할 있는 map 형태의 자료구조를 가진다. 한편 Prototype Manager 새로운 원본 객체를 추가하거나 삭제할 있도록 하기 위해 Register(), UnRegister() 같은 인터페이스를 제공하고, 원하는 원본 객체가 이미 존재하는 것인지를 확인하기 위해 Check() 같은 인터페이스도 일반적으로 제공하게 된다.
Prototype 패턴 구현 고려해볼만한 다른 사항으로는 Clone() 멤버 함수의 구현이다.(깊은 복사, 얕은 복사)
  • PROTOTYPE 패턴 정리
Prototype패턴이 유용한 경우
  • 객체를 생성하는 방식이나 객체의 구성 형태, 표현 방식 등과는 무관하게 객체를 생성하고 싶을 Protototype패턴이 유용하다. 왜냐하면 Prototype 패턴은 직접 객체를 생성하는 방식이 아니라, 원본 객체를 Clone() 멤버 함수를 통해 복제하는 방식이므로 객체를 생성하는 방식이나 구성 형태, 표현 방식 등이 모두 Clone() 멤버 함수 내부에서 결정되고 Client 입장에서는 전혀 신경 필요가 없기 때문이다.
  • 생성할 객체가 실행 시간(Run-Time) 결정될 경우 유용하다. 생성할 객체가 실행 시간에 결정된다는 것은 실행 시간 때까지 어떤 자료형의 객체를 생성해야 할지를 없다는 것을 의미한다. 따라서 일반적인 형태로 객체의 자료형을 명시하고 객체를 생성하는 방식은 적용이 불가능하다. 반면 Prototype 패턴은 생성될 객체의 자료형을 정해줄 필요없이 기존의 객체를 복제만 하는 것이므로, 실행 시간에 생성할 객체가 결정되어도 충분히 객체 생성을 수행해낼 있다.
  • Abstract Factory Factory Method 패턴의 경우에는 생성할 객체의 클래스 종류만큼 객체를 생성해주는 클래스를 따로 정의해야 했었다. 그러나 Prototype패턴을 적용할 경우에는 이러한 별도의 객체 생성 클래스들이 불필요하다. 따라서 Prototype 패턴은 생성될 객체의 클래스 구조와 병행해서 객체 생성을 위한 클래스들을 정의하고 싶지 않을 유용하다.
  • 어떤 클래스의 객체들이 안되는 상태들 하나에 머무르는 형태일 경우 Prototype 패턴을 사용하면 미리 상태별로 객체를 만들어 주고, 필요할 경우 원하는 상태의 객체를 복사해서 사용할 있어 유용하다.
  • 완전히 새로운 행위를 수행하는 객체를 생성하고자 새로운 클래스를 정의하지 않고, 기존 클래스로부터 객체를 생성해서 해당 객체의 상태 값을 변경시켜줌으로써 새로운 행위의 수행이 가능하게 하는 것이 효율 적일 있다.
  • 어떤 객체의 생성이 부분 부분을 조합해서 생성되는 형태인 경우에도 Prototype 패턴을 적용하는 것이 유용할 있다.
  • 실행 시간 환경(Run-Time Environment) 의해 동적으로 로딩되는 클래스가 있는 경우 클래스의 객체를 생성하기 위해서는 Prototype 패턴을 적용하는 것이 유용하다. 왜냐하면 경우 클래스의 생성자를 통해 객체를 생성하고 싶어도 클래스가 동적으로 로딩되기 때문에 이를 정적으로 참조할 없기 때문이다.
Prototype 패턴의 장점
  • Prototype패턴은 Abstract Factory Builder 패턴 등과는 달리 객체를 생성해주기 위해 별도의 클래스를 정의할 필요가 없다. , Abstract Factory Builder 패턴의 경우에는 생성될 객체의 자료형에 따라 객체를 생성해주기 위한 클래스들을 정의해야 하지만, Prototype 패턴에서는 이런 것들이 필요 없다.
  • Prototype 패턴은 실행 시간(Run-Time)에도 생성할 객체의 종류를 추가 또는 삭제할 있다는 장점을 가진다. 물론 이를 위해 별도의 Prototype Manager 같은 객체가 필요할 있으나, 동적으로 생성할 객체의 종류를 변경할 있다는 것은 일반적인 객체 생성 방식들이 가지지 못하는 장점이다. 또한 이와 비슷한 이유로 Prototype패턴의 경우에는 Client 생성할 객체의 종류를 객체 생성 시에 정해줄 필요가 없다는 장점도 있다.
Prototype 패턴의 단점
  • 생성될 객체들의 자료형인 클래스들이 모두 Clone() 멤버 함수를 구현해야 한다는 것이다. 여기서 Clone() 멤버 함수의 구현은 때로 매우 까다로울 있다. 예를 들어 클래스 내부 구조가 순환 참조를 하고 있는 경우에는 Clone() 멤버 함수의 구현이 쉽지 않을 것이다. 또한 이미 존재하고 있는 클래스에 대해 Clone() 멤버 함수를 일일이 추가한다는 것은 무척 귀찮을 있을 것이다.
반응형