이번 글에서는 빌더(Builder) 패턴에 대해서 살펴보겠습니다
이 패턴 역시 이제까지 소개한 패턴들과 마찬가지로 객체를 생성하는 것에 관련된 패턴입니다.
이름부터 뭔가 생성하고 할당하고 할 것 같은 느낌이 들지요?
이 패턴은 객체를 구성할 때, 일부의 생성 처리로 하나의 큰 객체를 구성하도록
하는 것으로 생각하면 됩니다. 여러 상황이 있을 수 있겠죠?
이를테면, 여행사에서 클라이언트에게 스케쥴표를 제공하는 경우,
스케쥴의 세부 내용을 구성한 다음 스케쥴표를 넘겨주는 식이 되어야 겠죠.
여기서 스케쥴표는 하나의 큰 객체인 셈이고, 세부 스케쥴
(숙박할 호텔정보, 관광정보, 이벤트 정보 등등...)
정보는 스케쥴표를 구성해 주는 내용이 되지요.
모든 클라이언트가 동일한 스케쥴을 가질 리는 없으니, 여행사에서 스케쥴표를
제공할때는, 제공할 수 있는 스케쥴 내역의 일부를 클라이언트의 요청에 따라서
구성하고 넘겨주는 식이 될 겁니다. 이렇게 세부 스케쥴을 분할하여 관리하게 되면,
클라이언트에게 스케쥴을 작성해 줄때, 좀더 유연한 대처가 가능할 것입니다.
한가지 중요한 것은, 이 패턴은 이후에 접하게 될, 컴포지트(Composite),
데코레이터(Decorator) 패턴과 기본 개념이 상당히 유사하다는 점입니다.
이 두 패턴 역시, 세부 객체를 합쳐서 하나의 큰 객체를 구성하는 것에 관련된 내용을
다루고 있기 때문이지요.
개념을 헷갈리지 않기 위해서 언급해 두자면, 빌더(Builder) 패턴은 어디까지나
객체를 생성하는 단계에서 적용되는 개념이라는 것입니다. 세부 객체가 어떤 식이건 간에
클라이언트가 받아야 하는 객체는, 하나의 완성된 형태의 객체를 받아야 하는 것이지요.
위에서 언급한 다른 두 패턴은, 하나의 객체를 구성하는 세부 객체들이 존재한다는 점에서
빌더(Builder) 패턴과 유사한 측면이 있으나, 이들 두 패턴은 이미 존재하는 객체에 대해서
기능을 추가하거나 제거하거나 하는 식의 객체 변경에 대해서 다루는 패턴입니다.
"생성" 이냐 "변화" 냐에 촛점을 맞추어 구분하면 쉽습니다.

▲ IOS 게임 버거퀸 월드(Burger Queen World) 의 스크린샷
위 스크린샷은, 버거퀸 월드라는 Ios용 모바일 게임의 스크린샷입니다.
이 게임에서는 재료를 조합하여 햄버거를 만들고, 이 햄버거를 손님에게 판매합니다.
위의 게임과 동일한 상황이라고 생각해 봅시다.
햄버거를 만들어 손님에게 판매를 해야 합니다. 일단 재료에 대한 클래스들을
정의해야 할 것이고, 이들을 통합하여, 하나의 버거를 만들어 제공하는 처리가 필요합니다.
그럼 코드를 통해 표현해 보겠습니다.
이 코드에는 STL 리스트 컨테이너를 사용하므로,
#include <list>
를 꼭 선언하시고 활용해 보시길 바랍니다.
class CMaterial
{
protected:
char m_Name[64];
public:
void OutName() { cout<<m_Name<<endl; }
public:
CMaterial() {}
virtual ~CMaterial() {}
};
class CBread :
public CMaterial
{
public:
CBread() { strcpy_s(m_Name,"브레드"); }
~CBread() {}
};
//빵(브레드)
class CPatty :
public CMaterial
{
public:
CPatty() { strcpy_s(m_Name,"패티"); }
~CPatty() {}
};
//패티
class CTomato :
public CMaterial
{
public:
CTomato() { strcpy_s(m_Name,"토마토"); }
~CTomato() {}
};
//토마토
class CPotato :
public CMaterial
{
public:
CPotato() { strcpy_s(m_Name,"포테이토"); }
~CPotato() {}
};
//포테이토
재료(Material) 클래스들의 코드입니다. 부모 형인 CMaterial 이 있고,
나머지 재료 클래스들은 이를 상속받은 구조로 만들어져 있습니다.
인자로 char 형 배열의 이름을 가지고 있으며,
각 생성자는 자신의 재료에 맞도록 이름을 지정하도록 구성되어 있습니다.
class CBurger
{
private: //버거 재료 리스트
list<CMaterial*> m_Material;
public:
void AddMaterial(CMaterial* pMat)
{
m_Material.push_back(pMat);
//받은 재료를 재료 목록에 추가
}
void OutMaterial()
{
cout<<"<<버거 재료 목록>>"<<endl;
list<CMaterial*>::iterator iter = m_Material.begin();
for(;iter != m_Material.end(); ++iter)
(*iter)->OutName();
}
public:
CBurger() {}
~CBurger()
{
list<CMaterial*>::iterator iter = m_Material.begin();
for(;iter != m_Material.end(); ++iter)
{
delete (*iter);
(*iter) = NULL;
}
m_Material.clear();
//컨테이너 해제 및 비우기
//추가된 재료는 이곳에서 비우게 됩니다.
}
};
버거 클래스입니다. 재료들을 보관할 리스트를 가지고 있으며,
외부로부터, 재료 객체를 받아서 리스트 컨테이너에 추가할 수 있는 처리와
재료 객체의 OutName 함수 호출로 재료 객체의 이름 출력 기능,
그리고 소멸자에서 받은 재료 객체를 모두 해제하고 컨테이너도
비우는 처리를 포함하고 있습니다.
어떻게 보면 재료 객체를 담을 수 있는 껍데기를 제공하는 것이라 생각해도 되겠지요.
실제로 빌더 패턴을 사용할때에는 외부로부터 객체에 속하는 서브 객체들을 넘겨받아
내부의 정보를 기록하거나 하는 식의 방식으로 사용됩니다.
class CBurgerBuilder
{
public:
virtual void Add_Recipe_Material(CBurger* pBurger) = 0;
//빌더에 맞는 재료를 추가할 수 있도록 준비
public:
CBurgerBuilder() {}
virtual ~CBurgerBuilder() {}
};
class CTomatoBurgerBuilder :
public CBurgerBuilder
{
public:
virtual void Add_Recipe_Material(CBurger* pBurger)
{
pBurger->AddMaterial(new CBread);
pBurger->AddMaterial(new CPatty);
pBurger->AddMaterial(new CTomato);
}
public:
CTomatoBurgerBuilder() {}
~CTomatoBurgerBuilder() {}
};
//토마토 버거 빌더
//이 빌더로는 토마토 버거에 해당되는 재료를 추가합니다.
class CPotatoBurgerBuilder :
public CBurgerBuilder
{
public:
virtual void Add_Recipe_Material(CBurger* pBurger)
{
pBurger->AddMaterial(new CBread);
pBurger->AddMaterial(new CPatty);
pBurger->AddMaterial(new CPotato);
}
public:
CPotatoBurgerBuilder() {}
~CPotatoBurgerBuilder() {}
};
//포테이토 버거 빌더
//이 빌더로는 포테이토 버거에 해당되는 재료를 추가합니다.
빌더 클래스의 모음입니다. 상위 클래스인 CBurgerBuilder 가 존재하고,
이 객체를 상속받는 여러 빌더 클래스가 존재합니다.
Add_Recipe_Material 함수는 공통된 인터페이스를 마련하기 위해서,
가상함수로 구현되어 있습니다. 물론 통일된 인터페이스이므로,
어떤 빌더 객체냐에 따라서, 함수 호출시 버거 객체에 추가하는 재료 객체가
달라지게 됩니다.
어찌되었건 동일한 함수로, 여러 빌더 객체에 대응할 수 있는 처리를
마련하였으므로, 이 처리를 통해 팩토리 클래스를 구성하면 됩니다.
class CFactory
{
public:
static CBurger* MakeBurger(CBurgerBuilder* BurgerBuilder)
{
CBurger* pBurger = new CBurger;
BurgerBuilder->Add_Recipe_Material(pBurger);
//빌더 클래스는 다형성에 의해 어떤 버거를 만들 빌더인지
//이미 정해져서 인자로 넘어옵니다.
//그리고 만들 버거에 따라서 필요한 내용을 넣어줍니다.
//받은 빌더에 따라서 다른 버거를 반환하게 됩니다.
return pBurger;
}
//버거 생성을 담당하는 팩토리 클래스입니다.
//준비한 빌더 객체를 통해서 객체를 추가합니다.
};
버거 객체를 할당하고, 넘겨받은 빌더를 통해서 버거에 재료를 추가해 넣어
이를 반환하는 기능을 가진 팩토리 클래스입니다.
단순히 빌더를 사용하여 객체를 생성할때 서브 객체를 추가하여 완성된
상태의 객체를 반환하는 역할만을 표현하기 위해서 많이 단순화된
상태입니다.
어찌되었건 MakeBurger 함수는 빌더 객체를 인자로 받습니다.
이때 이 빌더 객체는 다형성이 적용되어 있으므로, 여러 빌더가 들어올
수 있습니다. 이 빌더들은 통일된 인터페이스 구조를 따르며 모두
Add_Recipe_Material 함수를 포함한 상태입니다.
따라서, 어떤 빌더가 들어오건 간에 팩토리에서는 Add 함수만 호출하는 것으로,
버거의 재료를 알맞게 셋팅하고, 반환할 수 있게 됩니다.
이 부분이 중요합니다. 분기를 사용한 것도 아닌데, 세부 객체를 필요한
재료로 구성하여 객체를 생성하고, 이를 반환할 수 있습니다.


이렇게 준비된 빌더 클래스와 팩토리 클래스를 통해서, 실제 버거 객체를 할당하고,
출력 결과를 볼 수 있습니다.
2개의 버거 객체를 할당했으며, 하나는 토마토 버거,
하나는 포테이토 버거 객체입니다.
각각의 객체는 다른 재료를 가진 상태이며,
출력결과를 통해 세부 재료들을 볼 수 있습니다.
중요한 점은, 동일한 팩토리 클래스의 동일한 함수를 통해 생성한 객체이지만,
인자로 넘겨준 빌더 객체가 무엇이냐에 따라서, 내부 객체를 다르게 설정할 수
있었다는 점입니다.
이렇게 빌더(Builder) 패턴을 사용하면, 빌더를 통해서 서브 객체를 할당하는
방법을 단순화 할 수 있으며, 무엇보다도 서브 객체 할당 과정을 은닉화 할 수 있습니다.
덧붙여, 빌더(Builder) 역시 구현하는 방식에 대해서는 여러 방식이 있습니다.
제가 소개한 방식이 정답은 아니므로, 여러 방식을 사용해 보고,
자신한테 맞는 방식이 무엇인지 알아보는 것이 좋을 것 같습니다.
다음 글부터는 이제 객체 생성 패턴을 넘어서 구조적인 부분을 다루는
패턴에 대해서 보도록 하겠습니다.
태그 : DesignPattern, Builder





덧글