'생각하는개발/디자인패턴'에 해당되는 글 1건

  1. 2010.11.17 추상 팩토리 패턴

추상 팩토리 패턴
서로 연관된 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한
인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.

추상 팩토리 패턴 예제
#include <iostream>
#include <fstream>
#include <string>
#include <vector>

using namespace::std;

class CAudio
{
public:
 virtual void Run() = 0;
};

class CWindowAudio : public CAudio
{
public:
 virtual void Run()
 {
  cout << "Window Audio Run" << endl;
 }
};

class CUnixAudio : public CAudio
{
public:
 virtual void Run()
 {
  cout << "Unix Audio Run" << endl;
 }
};

class CVideo
{
public:
 virtual void Run() = 0;
};

class CWindowVideo : public CVideo
{
public:
 virtual void Run()
 {
  cout << "Window Video Run" << endl;
 }
};

class CUnixVideo : public CVideo
{
public:
 virtual void Run()
 {
  cout << "Unix Video Run" << endl;
 }
};


class COSFactory
{
public:
 virtual CAudio* CreateAudio() = 0;
 virtual CVideo* CreateVideo() = 0;
};

class CWindowFactory : public COSFactory
{
public:
 virtual CAudio* CreateAudio(){ return new CWindowAudio; }
 virtual CVideo* CreateVideo(){ return new CWindowVideo; }
};

class CUnixFactory : public COSFactory
{
public:
 virtual CAudio* CreateAudio(){ return new CUnixAudio; }
 virtual CVideo* CreateVideo(){ return new CUnixVideo; }
};

void main()
{
 COSFactory* pOSFactory = new CWindowFactory;

 CVideo* pVideo = pOSFactory->CreateVideo();
 CAudio* pAudio = pOSFactory->CreateAudio();

 pVideo->Run();
 pAudio->Run();

 delete pVideo;
 delete pAudio;

 delete pOSFactory;
}

==========================================================================================================


첫 강좌를 쓰고 나니 왠지 굉장히 귀찮아 지는군요. 역시 게으른 자는 이래서 안된다니까.. >.<   그래서 '괜히 패턴을 8개나 소개한다고 했구나..' 하고 후회하고 있던 중에 이 강좌를 봐주시는 분이 있다라는 것을 알고 좀 부지런 해지기로 했습니다. 

암튼 읽어주시는 분들께는 감솨를 드립니다. ㅠ_ㅠ

 그리고 깜빡하고 말씀 안드린 것이 있는데, 디자인 패턴은 언어에 독립적입니다.  여기서 C++을 써서 구현을 했지만 다른 모든 언어에서 구현이 가능하다고 합니다. 심지어 기존의 C로도 구현이 가능하다고 합니다. 저는 C++밖에 모르기 때문에 '어떻게 다른 언어로 구현하느냐'라고 묻지는 마세요.

< Abstract Factory >
'추상적인 공장' 이라고 해석해야 하나? 암튼 그런 뜻입니다. 
객체 지향에서는 abstract(추상적인)이라는 말과 concrete(구체적인)라는 말이 자주 쓰이는데, 클래스 상속에서 상대적인 개념을 나타낼 때 주로 쓰입니다.  소녀라는 클래스가 사람이라는 클래스를 상속했을 때 소녀 concrete 클래스라고 부르고 사람을 abstract 클래스라고 부릅니다. 이것은 우리가 실 생활에서 개념을 다루는 방식과 비슷합니다.  우리가 사람이라는 개념이 소녀라는 개념보다 추상적인 개념이고, 소녀라는 개념이 사람 이라는 개념보다 구체적이다라고 말하는 것과 같습니다.  또한 실 생활에서 처럼 추상적인 클래스와 구체적인 클래스는 상대적인 개념이지요.  사람은 소녀에 비해 추상적인 개념이지만 동물이라는 개념에 비해서는 구체 적인 개념이지요.  이처럼 사람 클래스와 동물 클래스가 있다면 사람 클래스가 구체적인 클래스(concrete)가 되고 동물 클래스가 추상적인 클래스(abstract)가 되는 것입니다.

<의도>
 구체적인 클래스들을 지정하지 않고 연관되어 있는 객체들의 패밀리를 생성하는 인터페이스를 제공하고 싶다.
 책을 직역하니 대충 이런 내용이 되는군요.  이해가 안 가시리라 믿습니다. -_-;;

<동기>
 스타크래프트를 개발하는 상황을 상상해 봅시다.  빌 로퍼가 배를 내밀며 지시를 내립니다. "세 종족 모두 서로 다른 독특한 건물을 가지게 할것이고, 인터페이스는 모두 통일을 하게 만들어야 하느니라~~  알겠느뇨~~"  당신이 묻는다.  "왜 세 종족 모두 독특한 건물을 가져야 합니까?"  "재밌으니까!!!" 빌이 대답한다. 당신이 또 묻는다. "그럼 왜 인터페이스는 통일시켜야만 합니까?"  "인터페이스를 통일시키면 프로그래밍을 하기 편하니까!!" 라고 빌은 또 대답한다.  당신은 또 묻는다.  "그렇지만 인터페이스를 통일시키는 작업은 쉽지 않쟎습니까?"  그러자 빌이 인상을 찌뿌리며 말한다. "쟤네들(인공지능 프로그래머)이 몸이 쫌 약하자나~~  니가 쫌 고생해라~~ 잉?" 

(... 이것이 게임회사의 현실입니다.. -_-;; )

당신은 이제 각기 다른 건물 구성을 가진 세 종족의 인터페이스를 통일시키지 않으면 빌의 배치기에 목숨을 잃을 판입니다. 

 이것에 대한 좋은 해결책이 이 패턴입니다.  이 패턴은 객체들을 생성하는 것과 사용하는 것을 분리시켜서 그 객체의 내용(구현)에 신경을 쓰지 않고 객체를 사용할 수 있도록 해줍니다.  결국에 이것은 인터페이스와 구현을 분리시키게 되는 것입니다. 

 이제 빌의 배치기에 위협당하는 상황으로 돌아가봅시다.  당신이 어쩔 수 없는 상황에 부처님께 기도를 드리자 부처님이 당신의 꿈에 나타나 계시를 내려 주십니다.  "너의 정성이 갸륵하여 인터페이스 클래스라는 것을 알려 주겠노라!!"     할렐루야~~

class cUnit
{
public:
    virtual void Attack() = 0;      //반드시 재정의 해야함
    virtual void Move() = 0;
    virtual void Stop() = 0;
    virtual void HoldPosition() = 0;   

    virtual void Special1() {}      //재정의 안해도 됨
    virtual void Special2() {} 
};

class cZealot : public cUnit
{
public:
    void Attack(); 
    void Move();
    void Stop();
    void HoldPosition();
};
 

 이것이 인터페이스 클래스입니다.. 먼저 순수 가상함수를 눈여겨 보세요

    virtual cUnit * Create1stUnit() = 0;

이것이 순수가상함수입니다..  뒤에 = 0; 이 붙이있는 것을 보고 의아하게 생각 하시겠지만 이것은 0을 대입한다는 뜻이 아니라 구현이 없다는 뜻입니다.  이 순수 가상함수가 하나라도 있는 클래스는 인스턴스를 생성할 수 없고 순수 가상 함수를 포함한 클래스를 상속한 클래스는 순수 가상 함수들을 모두 재정의 해야만 인스턴스를 생성할 수 있습니다. 저 늠름한 질럿 클래스를 보시면 Attack함수부터 HoldPosition까지 4개 모두를 재정의한 것을 알 수 있습니다.  때문에 Zealot 클래스는 인스턴스를 생성할 수 있지만 cUnit은 인스턴스를 생성할 수 없습니다.  만약 cZealot에서 4개의 순수 가상 함수 중 하나라도 빼먹고 재정의 하지 않았다면 컴파일러가 에러를 일으키기 때문에 cZealot의 인스턴스도 생성할 수 없게 됩니다..  순수 가상 함수는 실제 함수가 아닌 인터페이스에 대한 약속이라고 보면 됩니다.  순수 가상 함수에 대해 설명을 하다 보면 끝이 없으니 C++ 비급을 보고 열심히 연마하시길 바랍니다.
 

 또 알아햐 할 것은 다형성입니다.  설명을 위해 하이템플러를 추가하겠습니다.

class cHiTemplar : public cUnit
{
public:
    void Attack();
    void Move();
    void Stop();
    void HoldPosition();
    void Special1();
    void Special2();        //하이템플러는 마법이 세개였나?
                            //귀찮아서 두개만...
};

cZealot zealot;
cHiTemplar templar;

cUnit * Unit1 = &zealot;
Unit1->Attack();            //zealot.Attack()와 같은 결과

Unit1 = &templar;
Unit1->Attack();            //templar.Attack()와 같은 결과

 이것이 다형성입니다.  Unit1에 들어간 클래스형에 따라 그 클래스형에 맞는 함수가 호출되는 것을 알 수 있을 것입니다.  이것 역시 자세한 것은 C++비급을 참고하세요.

 이 인터페이스 클래스는 인터페이스와 구현을 확실히 분리시켜 주기 때문에 유닛을 사용에 대한 프로그래밍 할 때 유닛이 무엇인지 신경 쓸 필요도 없게 해줍니다.  또한 이것으로 인해 모든 유닛들을 큐나 리스트, 트리 등의 자료구조로 다룰 수 있게 되는 이점도 있습니다.

 또 다른 이점은 사용할 때에는 인터페이스 클래스만 include 해주면 되기 때문에 컴파일 연관성을 줄여주고 컴파일을 빠르게 해준다는 것입니다. 
 
 이제 Abstract Factory의 핵심을 설명하는 것이 남았습니다.
Abstract 클래스의 핵심은 생성하는 것과 사용하는 것의 분리와 객체 패밀리의 지정입니다.

class cPrimaryFactory               //1차 생산건물에 대한
{                                   //인터페이스 클래스
public:
    virtual cUnit * CreateUnit1() = 0;
    virtual cUnit * CreateUnit2() = 0;
    virtual cUnit * CreateUnit3() = 0;
    virtual cUnit * CreateUnit4() = 0;

    //베이스 클래스의 소멸자는 virtual이어야 합니다.
    //Effective C++참조.
    virtual ~cPrimaryFactory();    
};

class cBarrack : public cPrimaryFactory
{
public:
    cUnit * CreateUnit1()   { return new cMarine; }
    cUnit * CreateUnit2();  { return new cFirebat; }
    cUnit * CreateUnit3();  { return new cGhost; }
    cUnit * CreateUnit3();  { return new cMedic; }

};

 여기서 CreateUnit함수들의 구현을 단순히 [return new 유닛] 이렇게 해놨는데, 실제로는 이렇게 단순하지 않을 것입니다.  메모리 유출을 피하기 위해 먼저 Object Manager같은 클래스에 포인터를 저장하고 난 다음에 리턴하던지 하겠죠.

class cGateway : public cPrimaryFactory
{
public:
    cUnit * CreateUnit1();          //질럿 생산
    cUnit * CreateUnit2();          //드래군
    cUnit * CreateUnit3();          //하이템플러
    cUnit * CreateUnit4();          //다크템플러
};

class cHatchery : public cPrimaryFactory
{
public:
    cUnit * CreateUnit1();          //저글링
    cUnit * CreateUnit2();          //히드라
    cUnit * CreateUnit3();          //???
    cUnit * CreateUnit4();          //???
};

 저그의 해처리는 특이한 구조이기 때문에 다른 패턴을 동원해야 합니다만 설명을 위해 단순화하기로 하겠습니다.  UML도표를 그려서 설명하면 쉬운데.. 힘들군요.  뭐~ 이 강좌는 어디까지나 소개 차원에서 하는 것이므로 실제로 이 패턴들을 써서 프로그래밍을 하고 싶으신 분은 디자인 패턴 책을 반드시 사시길 바랍니다.  이미 패턴을 모두 알고 있다 해도 디자인 패턴 책은 패턴 사전으로서의 가치가 크기 때문에 프로그래밍 할 때
반드시 참고하시길..

 위의 PrimaryFactory를 상속한 클래스들이 Abstract Factory 들입니다. 

cPrimaryFactory * Factory1st = UI.GetUserFactory1();
                                //유저가 현재 선택한 종족의
                               //첫번째 생산 공장에 대한 포
                               //인터를 얻어옵니다.

//첫번째 유닛 생산 명령이 떨어졌을 경우
Factory1st->CreateUnit1(); 
//세번째 유닛 생산 명령이 떨어졌을 경우
Factory1st->CreateUnit3();

 이렇게 사용하면 되는 것입니다.  인터페이스가 통일 되었기 때문에 공장이 어느 종족의 것인지, 유닛이 구체적으로 어떤 종류의 것인지 신경을 쓸 필요가 없게 되었습니다. 만약 이 패턴을 쓰지 않고 만든다면 if문을 이용해서 세 종족의 경우를 모두 코딩 해주어야 할 것입니다.  코딩의 비용이 3배나 늘어나게 되는 셈이지요.  게다가 구조가 바뀌었을 때 3배나 되는 코드를 수정해야 하므로 유지보수 비용도 3배가 되는 셈입니다.  그러나 실제로는 3배가 훨씬 넘는다고 할 수 있습니다.  이 패턴을 쓰면 구현과 인터페이스를 분리시키기 때문에 함수 하나의 구현이 바뀐다고해서 인터페이스를 바꿀 필요도 없을 뿐더러 대부분의 경우 바뀌는 부분이 그 함수 하나 뿐일 경우가 많습니다.  그러나 if를 이용한 구조적 프로그래밍의 경우에는 함수 구현이 조금만 바뀌어도 연쇄적으로 수정해야 할 부분이 생기는 경우가 흔합니다. 

 이 패턴에서 객체의 생성과 사용을 분리시키는 이유는 인터페이스와 구현의 분리를 위해서라고 말씀 드렸습니다. 객체의 생성은 구현에 종속적일 수밖에 없는데 그 이유는 생성자에서 객체의 값을 할당하는 등의 설정을 하기 때문입니다.  객체의 생성이 단순히 new Object를 하는 것만을 의미하지는 않고, Object에 각종 설정을 하는 것을 모두 포함합니다.  Bitmap클래스라면 new Bitmap을 해서 클래스를 만들고 비트맵을 로딩하고, 필요한 만큼 잘라내는 등등 사용하기 전에 세팅하는 것을 모두 포함하는 것입니다.  우리가 컴퓨터를 사용하듯이 말입니다.  컴퓨터 부품들을 새로 사서 조립을 하고 세팅을 하는 과정은 부품들에 따라 약간씩 다릅니다.  많이 다른 경우도 있지요.  그렇지만 일단 세팅을 다 끝내고 사용하는 것은 다 같습니다. 

<적용>
이럴 때 Abstract Factory 패턴을 사용하라는군요

- 시스템이 그것의 컴포넌트들이 어떻게 생성되고 조립되고 표현되는지(사용되는지)와 독립적이어야 할 때.
- 시스템이 다양한 컴포넌트의 패밀리 중 하나로 설정되어야 할 때
- 관련된 컴포넌트들의 패밀리가 함께 쓰여야 하도록 디자인 되었고, 이런 구속을 강제해야만 할 때
- 클래스 라이브러리를 제공 하려는데 그것의 구현을 드러내지 않고 인터페이스만 드러내길 원할 때

<맺음말>
 어쩌다 보니 인터페이스 클래스와 abstract Factory패턴, Factory Method패턴까지 섞여버렸습니다.  원래 디자인 패턴에서는 디자인 시의 의사소통을 위해 패턴들의 개념을 명확히 구분하는데, 이 강좌의 목적은 어디까지나 소개이므로 '디자인 패턴은 좋은 것이다!'라는 것에 초점을 맞추어 설명을 했습니다. 계속 강조하지만 디자인 패턴을 실제로 써먹으실 분들은 책을 사서 제대로 공부하시길 바랍니다. 

p.s 원래는 factory method 패턴을 따로 하려고 했는데 위에 포함이 되었으므로 따로 하지 않겠습니다.  위에서 객체의 생성을 함수에 몰아넣고 하는 것이 factory method 패턴입니다. 그리고 factory method들을 묶어놓는 것이 abstract factory 입니다.
 
p.s 예전에 C&C를 좋아하며 Rule을 에디트하는 에디터를 받아서 열심히 했던 적이 있었습니다.  저 에디터를 써보신 분은 알겠지만 C&C에서는 유닛들이 부품들로 이루어져 있고, 스크립트를 통해 부품 조합을 새로 함으로써 MOD를 만들 수 있도록 해놓았습니다. 탱크같으면 포는 레이저포, 장갑은 특수 티타늄 장갑, 이동장치는 탱크바퀴 등등 이런 식으로 객체가 나뉘어져 있습니다. 이런 것을 구현할 때에도 이 abstract factory 패턴은 아주 유용합니다. 인터페이스를 통일해 버리기 때문에 프로그래밍 부분에서는 그 부품이 어떤 것인지는
신경을 쓸 필요가 없습니다.  단지 인터페이스를 갖고 놀면서 프로그래밍을 하면 되는 것이지요.  반면에
이 패턴을 쓰지 않으면 if문을 남발한 하드 코딩을 하게 될 것입니다.  더  좋은 방법을 생각해 낸다 하더라도
이 패턴을 이용하는 것만 큼 많은 이점을 가지기는 힘들 겠지요.  잘 쓰시기 바랍니다.

Posted by 모과이IT
,