IT/프로그래밍 관련

디자인페턴 강좌중 5/6

KSI 2005. 7. 5. 23:09
http://www.uml.co.kr/
디자인 패턴과 UML을 위한 좋은 홈페이지입니다. 한 번 들려보세요
한글로 된 강좌도 읽을 수 있고, 객체 지향에 대한 생소한 분야들을 알게 될 것입니다.

으흑~ 이번엔 데코레이터 패턴입니다. 이 패턴에 대한 예를 찾기가 힘들더군요. 스타를 하다가 우연히 예를 발견해서 강좌를 씁니다. 제가 첨에 패턴 배울 때 이 데코레이터 패턴이 이해하기가 가장 힘들었었는데 그 때문인지 강좌 쓰기도 힘드는군요 -_-;;;;


[[ Decorator ]]

< 의도 >
동적으로 (실행시간중에) 객체에 부가적인 기능을 붙여주고 싶다. 이 패턴은 기능을 확장하기 위해 서브클래싱(상속)을 하는 것에 대한 유연한 대안을 제시해준다.

< 동기 >
..이번에도 스타크래프트 개발 현장..

유닛의 렌더링을 만들어야 하는 상황이라고 생각 해봅시다. 저의강좌에서는 상당히 늦게 언급하게 됐는데, 유닛의 렌더링같은 경우에는 다른 어떤 부분보다도 먼저 만들게 되겠죠.


빌로퍼가 말합니다. "이제는 유닛의 렌더링을 만들어야 한단다. 근데 문제가 그렇게 간단한건 아니고 여러 가지 마법 효과와 상태 효과를 잘 정리정돈해서 출력해야 하기 때문에 문제란다~. 이걸 잘 정리정돈하지 않으면 계속 개발을 하기가 힘들어지자너. 기능도 계속 추가해야되고, 기획때문에 무쟈게 뜯어고치기도 해야자나 그래서 좋은 구조를 만들어야되는데, 너밖에 기대할 사람이 없거든~.. 니가 수고좀 해라잉~"

드뎌 짐을 떠맡았습니다. 이젠 대우가 쫌 좋아진것 같군요..

당신은 고민을 합니다.

"이것을 어떻게 해결해야 할까.. DirectDraw를 쓴다면 각각의 유닛들이 그유닛의 그림을 나타내는 surface들을 가지고 있을테고, 그 surface를 백서 피스에 그려주는 Draw 함수를 가지고 있을 것이다. 흠.. 여기까진 문제가 없는데.. 마법이랑, 하단의 상태창등이 문제란 말이야.. 먼저 마법을 생각해보자. 마법을 어떻게 렌더링 하도록 해야 하나..? 마법을 객체로 만들어서 일괄적으로 렌더링 하도록 하면 될것 같은데.. Defensive Matrix처럼 유닛을 따라다녀야 하는 마법이 문제로군.. 흠..
게다가 하단의 상태표시창에 특별한 표시를 해야 하는 경우도 있으니.. 이런 마법이나 상태창을 표시하는 기능을 특정한 유닛들에만 붙일 수 있어야 하는데.. Unit클래스 내에 변수를 만들고, Draw에서 변수값에 따라 if로 처리하도록 할까? 그렇게 하면 if에 의한 하드코딩이 많아져서 유연하지도 않고 복잡하게 얽히게 될텐데..

상속을 이용할까? cTank가 락다운이 걸린다면 cLockDownedTank로 바뀌도록 한다면? 여기다 디펜시브 매트릭스까지 걸린다면? cLockDowned_DMatr-ixed_Tank로 바뀌도록 해야 하나? 으악~!! 이렇게 하면 모든 마법-유닛 조합마다 클래스를 만들어줘야 하는데... 게다가 마법-유닛 조합이 정해져 있어서 전혀 유연하지가 않단 말이야. 흐~ 어떻게 해야되낭... 어떻게.. 어떻게..헉헉.. 어떻게..헉.. 어떻..헥.. 어떻.. 어.. 어.. 어.. ....."

이런~ 쓰러졌군요. 프로그래밍이란게 상당히 힘든거 같습니다. 이러한 상황을 잘 해결해 줄 수 있는 것이 데코레이터 패턴입니다. 데코레이터 패턴은 링크드 리스트에서 삽입을 하는 것처럼 추가할 기능을 원래의 객체 앞에다 붙이는 것입니다.(포인터를 이용해서)클라이언트 코드가 탱크 클래스 인스턴스를 참조하고 있다면 (포인터로 가리키고 있다면)

+------+ +-----+
|Client|-->|aTank|
+------+ +-----+

(클래스에 있어서 클라이언트는 그 클래스를 사용하는 부분의 코드를 말합니다. 다른 클래스이거나 main함수등이겠죠. 네트웍에서의 클라이언트가 아님) 여기에 탱크가 락다운에 걸렸다면 aTank 앞에다 aLockDown을 붙이는 것입니다.

+------+ +---------+ +-----+
|Client|-->|aLockDown|-->|aTank|
+------+ +---------+ +-----+

이 붙여진 객체(락다운)을 decorator라고 부르고, 이렇게 붙이는 것을 decorate한다고 합니다. decorator는 그것이 decorate하는 객체의 인터페이스와 통일시키기 때문에 그것의 존재는 투명하게 됩니다.(클라이언트에게는 보이지 않는다는 말임) 인터페이스를 통일시킨다는 것은 같은 인터페이스클래스로부터 상속받는다는 것입니다. 그리고 클라이언트에서는 탱크나 락다운의 포인터가 아닌 인터페이스 클래스의 포인터를 저장하게 되기 때문에 클라이언트로서는 락다운과 탱크의 구분이 필요 없게 되는 것이지요.

인터페이스 클래스가 IUnit이라고 하면 클라이언트의 관점에서는 이렇게 보이겠죠.

+------+ +-----+
|Client|-->|IUnit|
+------+ +-----+

클라이언트는 인터페이스 클래스인 IUnit의 포인터로 가지기 때문에 그 유닛이 무엇인지, 기능이 몇개가 덧붙여졌는지 상관할 필요가 없이 원래의 객체를 사용하듯이 사용하면 되는 것입니다.

위의 락다운 걸린 탱크에 디펜시브 매트릭스를 걸고, 그 탱크를 플레이어가 선택했다고 하면..

+------+ +------------+ +--------+ +---------+ +-----+
|Client|-->|aRenderState|-->|aDMatrix|-->|aLockDown|-->|aTank|
+------+ +------------+ +--------+ +---------+ +-----+

이렇게 되겠죠. aRenderState는 유닛의 상태를 하단의 상태바에 출력하는 기능의 클래스입니다.


< 소스 >

class IUnit
{
public:
//데코레이터에서 사용하는 포인터
IUnit * _pNext;
IUnit() : _pNext( NULL ) {}
virtual ~IUnit() {}

virtual void Attack() = 0;
virtual void Move() = 0;
virtual void Damage(int) = 0; //공격 당했을 때 에너지
//를 닳게함
virtual void Update() = 0;
virtual void Draw() = 0;
virtual void GetState() = 0;
};

class cTank : public IUnit
{
public:
void Attack();
void Move();
void Damage();

void Update();
void Draw();

void GetState();
};

//클래스가 스스로를 소멸시키지 못하므로 소멸을 대행해서 일괄적으로 처
//분 하는 클래스
class cGarbages
{
//쓰레기들의 저장공간
vector< void * > _pContainer; //STL의 vector클래스
public:
cGarbages & GetInstance(); //싱글턴

void Add( void * ); //쓰레기 추가
void Disposal(); //쓰레기 모두 처분(전부 delete)
};

class cUnitDecorator : public IUnit
{
//(클라이언트 코드에서의 유닛에 대한 포인터)의 포인터
//데코레이터 리스트에서의 Head라고 할 수 있다.
IUnit ** _pPtr;
public:
cUnitDecorator( IUnit *& pNext )
: _pPtr( &pNext )
{
_pNext = pNext;
*_pPtr = this;
}

//디폴트 구현을 제공함.
virtual void Attack() { _pNext->Attack(); }
virtual void Move() { _pNext->Move(); }
virtual void Damage(int dmg) { _pNext->Damage(dmg); }
virtual void Update() { _pNext->Update(); }
virtual void Draw() { _pNext->Draw(); }

//상태 정보 리턴. sState는 유닛의 상태정보를 담는 구조체
virtual sState GetState() { return _pNext->GetState(); }

//스스로 데코레이터들의 리스트에서 삭제한다.
void SelfDestroy()
{
if( *_pPtr == this )
*_pPtr = _pNext;
else
{
//리스트에서 이것의 앞에 있는 데코레이터를 찾는다.
IUnit * pUnit = *_pPtr;
while( pUnit->_pNext != this )
{
pUnit = pUnit->_pNext;
}
pUnit->_pNext = _pNext;
}

cGarbages::GetInstance().Add( this ); //소멸
}
};

class cLockDown : public cUnitDecorator
{
int _Duration; //지속시간
LPDIRECTDRAWSURFACE _pSurface; //락다운의 그림을 저장하는 서피스
public:
cLockDown( cUnit * pNext )
: cUnitDecorator( pNext )
{}
void Attack() {} //락다운에 당하면 아무 일도 할 수 없죠.
void Move() {} //그래서 명령을 내려도 아무일도 하지 않음
void Draw()
{
_pNext->Draw(); //원래의 탱크를 화면에 출력한 다음
_pSurface->BltFast(); //그 위에 락다운의 그림을 출력한다.
}
void Update()
{
_Duration--;
//지속시간이 지나면 자동 소멸
if( _Duration <= 0 )
SelfDestroy();

_pNext->Update();
}
};

//Defensive Matrix
class cDMatrix : public cUnitDecorator
{
int _Energy; //디펜시브 매트릭스의 에너지
int _Duration; //지속시간
public:
void Damage(int dmg)
{
//데미지가 디펜시브 매트릭스에 가해진다.
_Energy -= dmg;
//에너지가 0이 되면 파괴
if( _Energy <= 0 )
{
SelfDestroy(); //소멸
}
//디펜시브 매트릭스가 있는 동안은 유닛에가 데미지가
//가지 않기 때문에 유닛의 Damage함수는 호출하지 않는
//다.
}
void Draw()
{
_pNext->Draw(); //원래의 탱크를 화면에 출력한 다음
_pSurface->BltFast(); //그 위에 디펜시브매트릭스 그림을 출력
}
void Update()
{
_Duration--;
//지속시간이 지나면 자동 소멸
if( _Duration <= 0 )
SelfDestroy();

_pNext->Update();
}
};

class cRenderState : public cUnitDecorator //상태창에 유닛 상태 출력
{
public:
cRenderState( IUnit * pUnit );
void Draw()
{
//상태를 얻어온 다음에..
sState TankState = _pNext->GetState();
//...탱크의 상태를 하단의 상태창에 출력...
}
};

//클라이언트 코드( 대충 이런 식으로 사용하게 되겠죠 )
class cGameManager
{
IUnit * pTank;
public:
void OnClickLockDown()
{
....
//이제 탱크가 락다운에 걸렸습니다.
_pTank = new cLockDown( _pTank );
}
void OnClickDMatrix()
{
....
//디펜시브 매트릭스 명령이 내려지자 탱크가 마법에 걸립니다.
_pTank = new cDMatrix( _pTank );
}
void OnClickUnit( IUnit * pUnit )
{
//이제 이 탱크를 플레이어가 선택 했습니다.
pTank = new cRenderState( pTank );
}
void Update()
{
...
//이렇게 해서 탱크를 화면에 출력할 것입니다.
_pTank->Draw();
_pTank->Update();
//매 틱마다 쓰레기들을 처분합니다.
//이것땜에 속도가 저하되는게 싫으시다면 뭐~ 10틱이나 20틱, 또는 1초에
//한번씩 처분하도록 해도 되겠죠~
cGarbages::GetInstance().Disposal();
}
};

//Psynoic Storm같은 경우는 특정 유닛에게 거는 마법이 아니기 때문에 데
//코레이터를 쓰지 말고 독립된 객체로 처리해야 하겠군요.

위에서 *&(포인터 레퍼런스)를 써서 클래스를 가리키는 포인터를 조작하도록 했는데, 이렇게 쓰려면 주의해야 할 것이 있습니다. 클래스를 가리키는 포인터가 단 하나!!이어야 한다는 것.(Composite패턴을 쓰면 저절로 그렇게되죠)

커헉! 힘들다. 위의 예제는 디자인 패턴 책하고 쪼금 다릅니다. 실시간중에 데코레이터들이 붙여졌다 떼어졌다 하기 때문에...

< 응용(적용?) >
이럴때 데코레이터를 사용합니다.

-개개의 객체에 동적이고 투명하게(다른 객체에 영향을 미치지 않고) 기능을 추가하기 위해서

-특정 기능을 독립적인 클래스로 만들기 위해서

-서브클래싱을 통해 확장을 하는 것이 좋지 않을 때, 서브클래싱을 통한 확장이 가능하지만 이럴 경우 가능한 모든 기능들의 조합을 모두 클래스로 만들어야 하기 때문에 효율적이지 못할 때

-클래스 정의가 감추어져 있거나 서브클래싱이 불가능할때
(소스없이 헤더랑 라이브러리 파일들만 공개하는 경우 상속하기가 곤란하죠. 이럴때 ..)

< 결과 >
-정적인 상속보다 더 유연하다. 데코레이터는 실시간(run-time)중에 기능을 붙이고, 뗄 수 있다. 반면 상속은 각각의 기능이 추가된 서브클래스를 새로 만들어야 한다. (LockDowned_DMatrixed_Tank?) 이것은 클래스의 숫자를 증가시키고 시스템을 복잡하게 만든다. 게다가 데코레이터는 기능들을 조합해서 쓸 수 있게 해준다.

데코레이터는 같은 기능을 중첩(2개 이상)해서 붙일 수 있게 해준다. 락다운에 두 번 걸린 탱크가 있다면 aLockDown 데코레이터를 탱크에다 두 개 붙여주면 되는 것이다.

-쓰지 않는 기능을 위해 비용을 지불하지 않는다. 데코레이터를 붙이지 않은 유닛 클래스는 단지 그냥 유닛 클래스일 뿐이다. 새로운 기능을 위해 if검사 따위도 하지 않기 때문에 성능 저하가 없다.

-확장이 아주 쉽다. 상속을 통한 확장은 슈퍼클래스의 클래스 정의에 대해 알고 있어야 한다. 때문에 슈퍼클래스가 복잡할 경우에는 확장을 하기도 복잡해지는 경향이 있다. 그러나 데코레이터는 원래의 클래스와 독립적으로 부가된 기능에만 신경쓰면 되기 때문에 원래의 클래스가 복잡하다 하더라도 간단하게 확장할 수 있다.


-많은 수의 작은 객체들.. 객체들을 생성할 때 new, delete를 사용한다면 성능 저하가 나타날 것이다. 이럴땐 Memory Pool을 만들어서 쓰면 성능저하를 막을 수 있다.(Effective C++참조)

이러한 작은 객체들은 서로간의 연결 관계에 따라 달라지지만 거의 비슷하게 보이게 될 것이다. 때문에 알아보기가 힘들고, 디버그도 힘들어지게 될 것이다.

-성능 저하.

위의 소스에서 cRenderState클래스를 보자. 이 클래스는 단지 Draw함수에 기능을 추가하기 위해 데코레이터를 썼는데, 나머지 함수 5개는 아무 일없이 함수를 한 번씩 더 거치게 된다. 때문에 약간의 성능 저하가 있게 된다. 성능 저하라고 해봤자 아주 약간일 뿐이지만, 속도가 중요한 부분에서는 좋지 않을 것이다. 이런 경우에는 Strategy 패턴을 쓰는 것이 좋다.

그 외에 컴포넌트 클래스가 무겁다면(변수나 함수가 많다면) 데코레이터에는 쓰지도 않는 변수들이 들어가게 되므로 그 또한 메모리 낭비나 성능 저하의 원인이 될 수 있다. 이럴 때에도 Strategy 패턴을 쓰는 것이 좋다.

(Strategy패턴 강좌는 곧 하겠습니다.. )


p.s 끝까지 봐주셔서 캄사합니다. (꾸벅)
      다음은 Command패턴이 될듯 하군요.