State패턴을 하기 전에 함수 포인터의 대안으로서 가상함수를 소개해 드리겠습니다. State패턴이 사실 함수포인터랑 비슷하기 때문에...
< 가상함수를 이용한 함수포인터 >
가끔 너무 많은 if문 때문에 함수 포인터를 쓰는 경우가 있을 것입니다. 이 가상함수는 함수 포인터와 비슷하다고 할 수 있습니다. 차이점이라면 더 유연하고 직관적이며... 암튼 더 좋습니다.
void HandleCommand( int CommandID )
{
switch( CommandID )
{
case 1:
case 2:
case 3:
default:
}
}
이것을 함수 포인터로 바꾸면...
void Command1() { //구현.... }
void Command2() { }
void Command3() { }
typedef void (*CommandFunc)();
CommandFunc Commands[3] = { Command1, Command2, Command3 };
void HandleCommand( int CommandID )
{
Commands[ CommandID ]();
}
대충 이렇게 되겠죠.. (맞나...?)
이것을 가상함수 버전으로 바꾸면....
class iCommand
{
public:
virtual void Operate() = 0;
};
class cCommand1 : public iCommand
{
public:
void Operate() {}
};
class cCommand2 : public iCommand
{
public:
void Operate() {}
};
class cCommand3 : public iCommand
{
public:
void Operate() {}
};
cCommand1 com1;
cCommand2 com2;
cCommand3 com3;
iCommand * Commands[3] = { &com1, &com2, &com3 };
void HandleCommand( int CommandID )
{
Commands[ CommandID ]->Operate();
}
이렇게 됩니다. 사실 위와 같은 경우는 함수포인터보다 딱히 좋다고 할 수가 없습니다. 그럼 어느 경우에 좋은가?
- 다양한 인자를 받을 수 있다.
함수포인터는 문법 특성상 인자를 통일시켜야 하지만 가상함수를 이용한 방법은 다양한 인자를 넘겨줄 수 있습니다. 이에 대한 예제는 State 패턴에 나옵니다.
- 둘 이상의 함수도 지정할 수 있다. 함수포인터는 불가능하죠.
- 상속과 재정의를 이용한 확장이 용이하다.
이런 여러가지 장점이 있습니다. 클래스가 갖는 장점은 모두 가지고 있다고 보면 되겠습니다.
그리고 성능상의 장점이 있는데, switch문을 이용한 방법보다는 빠르고 함수포인터와는 비슷한 속도를 내게 될 것입니다.
[[ State 패턴 ]]
< 의도 >
객체의 내부 상태가 바뀌었을 때 그것의 행동까지 바뀌게 하고 싶다.
< 동기 >
...스타크래프트 개발 현장...
빌로퍼가 또다시 배를 내밀며 명령을 내립니다. "지난번의 Abstract Factory 패턴으로 만든 그것에 덧붙여 유닛 클래스를 구현해야 하느니라~. 유닛은 현재 상태에 따라 행동을 하고 어떤 자극에 대해 반응을 하느니라~. Attack 명령을 내렸다면 유닛이 Attack 상태로 바뀌고 그에 맞는 행동인 '보이는 적은 뭐든지 공격한다'를 실행하게 되느니라~. Hold Position같은 경우에는 유닛이 Hold Position상태로 바뀌게 되고 '그 자리에서 이동하지 않으면서 공격 가능한 적을 공격한다'를 실행하게 된다. 이렇게 만들어야 되는데, 문제는 같은 상태에 대해 같은 행동을 하는 유닛들이 많다는 것이니라~. 예를 들어 질럿이랑 드래군등은 Stop상태에서 공격 받았을 때 '반격한다'라는 반응을 나타내는 것으로서 동일하지만, 하이템플러 같은 경우에는 '도망간다'라는 반응을 나타내기 때문에 같지 않은 것이다. 이처럼 질럿이랑 드래군은 상태에 대한 행동이 같은데 똑같은걸 두 번 코딩할 필요는 없잖냐...~~. 이것을 니가 해결해봐라잉~. 배치기가 무섭지 않다면 농땡이를 부려도 될 것이야~~" 이렇게 명령이 떨어졌습니다.
당신은 배치기가 무서우므로 농땡이를 피울 수도 없을 것입니다. 그러나 딱히 좋은 방법이 생각나지도 않습니다. 그러자 당신은 외칩니다.
"도와줘~!! 졸라맨!!"
그러자 졸라맨이 막대기를 휘날리며 나타납니다.
%_TEXT_% 짜잔~~ ( ;;-_-;;; )
|\
/ \
그러나 그의 못미더운 모습을 보고 당신은 그를 무시하고(--+)
책을 뒤져보기 시작하더니 마침내 State패턴을 발견하고는 환희의 비명을 지릅니다. 꺄아~
< 소스 >
State패턴은 각각의 상태를 클래스로 만들고 인터페이스 클래스인 IState를 상속합니다. 위에서 설명한 '가상함수를 이용한 함수 포인터'와 비슷합니다.
//---------------------------------------------------------
class IState; //클래스 전방선언
class cZealot : public cUnit //cUnit은 지난 강좌에서
{ //나왔던 그거...
IState * _pState;
void ChangeState( IState * );
public:
cZealot();
~cZealot();
//이 명령들이 전달되면 유닛은 상태를 바꾸게 됩니다.
void Attack( cUnit * Target );
void Move( int x, int y );
void Stop();
void HoldPosition();
void Patrol( int, int, int, int );
void Update();
};
//----------------------------------------------------------
class IState {
public:
enum ID_STATE
{
STATE_ATTACK,
STATE_MOVE,
STATE_STOP,
STATE_HOLDPOSITION,
STATE_PATROL
};
private:
const ID_STATE _ID;
public:
IState( ID_STATE state ): _ID( state )
{}
ID_STATE GetID()
{
return _ID;
}
//상태에 대한 행동하는 함수.
virtual void Operate() = 0;
};
//Attack 상태. 공격 대상(target)과 행동의 주체(pUnit)을 인자로 받고 멤버 변수에 저장해놓습니다.
//그리고 Operate함수에서 멤버변수에 들어간 인자값을 참조하면서 행동을 수행합니다.
class cState_Attack : public IState {
cUnit * _pTarget;
cUnit * _pUnit;
public:
//Attack이 끝나면 Stop상태로 바꿔야 하기 때문에 cUnit의
//포인터를 받음
cState_Attack( cUnit * target, cUnit * pUnit )
:IState( STATE_ATTACK ), _pTarget( target ), _pUnit( pUnit )
{}
void Operate(); //각각의 상태에 대한 행동을
//구현함
};
class cState_Move : public IState {
int _x, _y;
cUnit * _pUnit;
public:
cState_Move( int x, int y, cUnit * pUnit );
void Operate();
};
class cState_Stop : public IState {
public:
void Operate();
};
class cState_HoldPosition : public IState {
public:
void Operate();
};
class cState_Patrol : public IState {
int _x1, _y1, _x2, _y1;
public:
cState_Patrol( int x, int y, int x2, int y2 );
void Operate();
};
//---------------------------------------------------------
cZealot::cZealot()
{
//상태 객체에 넘겨줄 인자가 모두 동일하다면 new, delete
//보다는 싱글턴을 쓰는 것이 좋을 것입니다.
_pState = new cState_Stop;
}
cZealot::~cZealot()
{
delete _pState;
}
void cZealot::ChangeState( IState * pState )
{
//상태 전이에 대한 특별한 처리가 필요하다면 여기에 코딩
//하면 됩니다.
delete _pState;
_pState = pState;
}
void cZealot::Attack( cUnit * Target )
{
ChangeState( new cState_Attack( Target, this ) );
}
void cZealot::Move(int x, int y )
{
ChangeState( new cState_Move( x, y, this ) );
}
void cZealot::Stop()
{
ChangeState( new cState_Stop );
}
void cZealot::HoldPosition()
{
ChangeState( new cState_HoldPosition );
}
void cZealot::Patrol( int x1, int y1, int x2, int y2 )
{
ChangeState( new cState_Patrol( x1, y1, x2, y2 ) );
}
void cZealot::Update()
{
//여기서 행동이 일어나게 됩니다.
_pState->Operate();
}
//---------------------------------------------------------
위에서 상태가 바뀔때마다 new, delete를 하게 되는데 이렇게하면 속도가 많이 떨어지게 됩니다. 여기에다 메모리 풀(Pool)을 만들어서 쓰면 성능을 개선시킬 수 있을 것입니다. 실험 결과 5배정도의 속도 향상이 있는 것 같더군요. 메모리 풀은 Effective C++에 나왔으므로 그 책을 참고하시기 바랍니다. 시간나면 제가 강좌를 쓰게 될지도 모르겠군요( 기대하진 마세요 )
//---------------------------------------------------------
< 결과 >
State패턴은 이런 장점들을 가지고 있습니다.
1. 특정 상태에 종속적인 행위를 다른 상태와 분리시킨다.
분류를 명확히 해놓는 것은 유지 보수를 위해 아주 중요한 것이죠. switch문을 통한 코딩과 State패턴을 이용한 것의 차이는 정리 안된 문서와 잘 분류되어 정리된 문서의 차이와 비슷하다고 할 수 있겠습니다.
2. 상태의 전이를 명시적으로 만든다.
객체가 그것의 현재 상태를 단순히 내부 데이터 값으로 나타냈다면 상태의 전이가 단순히 값의 대입이기 때문에 명시적이지 못할 것입니다. 때문에 상태 전이에 대한 처리를 하기가 애매할 것입니다. if문을 남발하는 지저분한 구조가 되겠지요. 반면에, 상태들에 대한 각각의 객체를 정의하는 것은 상태의 전이를 보다 명시적으로 만들게 됩니다. 또한 상태 객체를 이용하면 특정 상태로 전이되는 것에 대해 각 상태 클래스마다 함수를 만들어주면 되기 때문에 훨씬 유연하고 유지보수가 용이한 구조를 만들 수 있을 것입니다. 위의 소스에서는 상태의 전이에 대한 특별한 처리는 하지 않았기 때문에 필요한 분은 스스로 고안해서 쓰세요. 별로 어렵지 않을 껍니다.
3. State 객체들을 여러 클래스에서 공유할 수 있다.
Attack에 대한 상태를 생각해본다면, 질럿은 공격 당했을 때 반격을 할 것이지만 하이템플러는 도망을 갈 것입니다. 때문에 서로 다른 상태 클래스를 사용하겠지만 질럿이랑 드래군은 공격을 받았을 때 똑같이 반격을 할 것이므로 Attack에 대한 상태 클래스를 공유할 수 있습니다.
4. 더 유연하다.
switch문과 같은 방법은 정적이기 때문에 유닛의 상태에 대한 행위를 바꿀려면 하드코딩을 해야 합니다. 그러나 State패턴을 이용해서 다양한 상태-행위 클래스를 만들어놓고 스크립트에서 그것들을 조합해서 조립하면 유지보수, 확장등의 비용을 훨씬 줄일 수도 있고 스크립트만으로도 별 희한한 게d임을 다 만들 수 있게 될 것입니다.
//---------------------------------------------------------