소프트웨어/C# & ASP.NET

잉여들을 위한 클래스설계 이야기 2/4

falconer 2009. 12. 11. 09:05

본론으로 가기전 여기에서 MFC 클래스 구조도를 함 봅니다~*

적절한가?

적절한가?


Rhea君을 포함한 우리 잉여들에게, MFC는 참 많은 것을 이야기해준다.
그중 하나가 클래스 구조이다.
잘만든 상용 C++ 클래스 설계를 어디가면 볼수 있을까? 파랑새는 1px옆에 있다, 바로 MFC가 그것이다.

MFC는 좆뉴비들이 착각하듯, 게임 스프라이트 툴 만드는 도구가 아니다.
그속에서 적절히 훔쳐와야 할 것이 무궁무진하다.
니 친구가 만든 듣보잡 3D 엔진의 구조를 파악하기보다는 차라리 MFC의 구조를 파악하고 숨겨진 의미를 알아내는게 프력증강에 도움이 된다.

일단 여기에서 훔쳐올 것은 최상단 클래스와 파생 클래스들의 관계이다. 우리가 잘쓰는 CView, CFrameWnd등은 아래와 같이 상속을 받았다.
파워포인터까지 동원한 포스트...

파워포인터까지 동원한 포스트...

왜 이렇게 나누었을까?
또 CView 이전에 클래스는 무슨 역활을 하나?

CObject : 거의 모든 MFC 클래스의 기반 클래스로 직렬화(Serialization), 런타임 클래스 정보(Runtime class information), 객체 진단 출력(Object Diagnostic Output) 기능을 제공한다.
CCmdTarget : 명령 메시지를 받는 기능을 갖고 있다.
CWinApp : 프로그램을 구동하는 기능
CDocument : 데이터를 저장하는 기능
CWnd : 눈에 보이는 속성을 지닌 객체에 관련한 모든 기능
CFrameWnd : 윈도우 프레임 와꾸을 관리하는 기능
CView : DC를 포함하여 데이터를 보여주는 윈도우를 관리하는 기능

이중 가장 유명한 것이 바로 CWnd일 것이다. 300개 이상의 멤버 함수를 갖고 있는 이 클래스는 눈에 보이는 모든 윈도우 객체들이 CWnd를 상속받는다. CView는 물론이고 CDialog나 CContolBar, CEdit, CButton 등 친숙한 각종 클래스들을 낳고 낳은 인기 최고, MFC의 퀸 에일리언, 컨트롤 클래스의 여왕벌 정도 되시는 클래스겠다.

그리고 CObject는 최상단임에도 불구하고 잘 알려지지 않고 있다. 오죽했으면 CWnd를 최상단 클래스라고 믿는 뉴비들이 많을 지경이니까. 아마 직접 사용하는 일이 없기에 그럴지도 모르겠다. 그러나 DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC, DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE, DECLARE_SERIAL/IMPLEMENT_SERIAL등의 매크로를 이용하여 클래스 런타임시 클래스 정보와 객체 생성 여부, 그리고 직렬화를 사용할 수 있게 해준다.
굳히 기존의 자신의 MFC 프로젝트를 뒤져보지 않더라도 MSDN에서 http://msdn.microsoft.com/ko-kr/library/38z04tfa.aspx를 살펴보면 CObject에서 직접 상속받아 객체를 관리하는 모습을 볼수 있다.

이렇듯 MFC에서 1차적으로 배울수 있었던 것 하나는 가장 최상단 클래스는 직접 눈에 보이거나 입출력을 하는 일을 하지 않는다 하더라도 객체 관리를 위한 작업 준비를 해둔 것임을 알수 있다.

이 싯점에서 지난 시간 만들다 멈춘 클래스를 좀더 작업해보자. 지난 시간, 우리는 CCharacter에서 CGameCharacter를 상속받은후, CAmazon과 CAssasion과 CSorceress를 각각 상속받았다. CObject 와 같은 일을 해줄 클래스를 CGObject라는 이름으로 하나 만들었다. 특별히 'G'를 더 추가한 이유는 MFC의 CObject와 혼선을 막기 위해서이다.
또한 CGameCharacter는 저절로 MFC의 CWnd와 같이 눈에 보이는 것을 다루기 위한 클래스가 되었음을 잊지말자.

객체 관리를 위한 클래스가 최상단으로.

객체 관리를 위한 클래스가 최상단으로.

다시 게임으로 되돌아가보자. 아마존, 어새신같은 직업을 나타나는 클래스가 있지만 아직 이 녀석들에게서 직접 객체를 선언하는 것은 안된다. 지난 시간에 언급한 것처럼 적이라도 아마존이나 어세신, 소서리스같은 특성을 나타낼수 있기 때문이다. 비단 적뿐만이 아니다. 아이템을 주고 도움을 주거나 경비를 서주는 NPC 역시 직업을 가질 수 있다. 

"얼래? 제가 만들 게임에서는 NPC 직업은 상인인데요, 플레이어는 절대로 상인이 되지 못하거든요?"
라는 질문이 있을수 있다. 그럴 경우 "상인"이라는 클래스 역시 CChraracter나 CGameCharacter의 파생 클래스로 넣으면 된다.

또한 싱글 게임이 아니라 네트워크 혹은 MMO 게임이라면 상대편 역시 이들 클래스에서 파생된다. 따라서 아직도 CAmazon같은 직업 클래스는 추상 클래스로 나타내주어야 한다. 그럼 필요한 것은 아마존, 어세신같은 직업을 상속받은 클래스가 실제 플레이어인지, NPC인지, 적인지를 구분할수 있어야 한다.

다시 말해 플레이어, NPC, 적이란 것도 각각의 클래스도 나타내야 한다는 것이다. MO를 위한 "다른 플레이어"도 클래스로 나타낼수 있으나 이 강좌에서는 일단 싱글 플레이용이라 간주하자.
아 복잡해진다 ㅠㅠ

아 복잡해진다 ㅠㅠ


이제 실제 눈에 보이는 캐릭터(인스턴스가 존재하는 진짜 객체!)를 위한 마지막 파생 클래스가 필요하다.
이때 다중 상속을 하면 된다. 오호~ 다중 상속 구현 숙제는 이렇게 구현이 되었다.

사실 Rhea君은 극단적인 다중상속 반대주의자다. 그렇다고 다중상속을 100% 안한다는 이야기가 아니라,
엔진과 같은 UI부분과 데이터 처리를 위한 패턴 구현으로 인해 결국 다중상속을 받게 되게 되므로
가급적 설계단계에서는 논리적인 다중상속은 피하자는다는 이야기이다.
결국 편하게 가자는 이야기인데 이건 다음 시간에 마법 속성이 들어가며 다시 논하겠다.

예컨데 CPlayer와 CAssassin을 다중 상속 받게되면 플레이어를 위한 어세신 캐릭터가 만들어진다.
CNPC와 CMerchant를 다중 상속 받게되면 CPU가 움직여주는 상인 NPC가 만들어진다.
CEnemy와 CSorceress를 다중 상속 받게되면 마법사 적이 만들어진다.

어세신 하악하악

어세신 하악하악



그런데 어느날, 기획자가 달려와 고민이 하나 생길 수도 있다.

쵝오의 먼치킨 급 캐릭터인데요, 아마존과 소서리스, 혹은 어세신과 아마존의 능력치를 동시에 쓸수 있는 새 캐릭터를 하나 넣기로 하지요, 아, 물론 현질 해야하는 유료 캐릭터로요!

라는 막장으로 가는 기획이 끼어들수 있다.

막장에 대해 좀더 자세히 알고 싶다면
이 문서(http://ko.uncyclopedia.info/wiki/%EC%95%84%EB%82%B4%EC%9D%98_%EC%9C%A0%ED%98%B9) 를 추천한다.

아마 분명 상속을 써먹을 좋을 기회라 판단한 뉴비 개발자는 CPlayer + CAssassion + CAmazon을 다중 상속 받아 CPlayerAssassinAmazon 클래스를 만들 것이다. 그림으로 보면,

이른바 죽음의 다이아몬드!

이른바 죽음의 다이아몬드!

CPlayer를 배제하더라도 CPlayerAssassinAmamzon은 이른바 죽음의 다이아몬드(DOD, Diamond of Death. 게임프로그래머를 위한 C++ 2장 참조)라는 다중 상속의 폐해, 즉 모호성 문제를 고스란히 떠안게 된다.

그럼 CPlyaerAssassin과 CPlayerAmazon을 상속받으면 어떻게 될까?
엎어치나 메치나~란 단어가 적절하다.

엎어치나 메치나~란 단어가 적절하다.

이렇게 해본들, 다이아몬드가 길어질 뿐, 나아지진 않는다.
의도하지 않게 CGameCharacter는 CPlayerAssassinAmazon의 부모 클래스가 되어 예측하지 못한 결과를 초래하는 결과를 낳는다. 이건 좆망하는 실패 사례로 가는 지름길이며 KGC에서 우린 이렇게 망했어요~라며 작년처럼 울부짖을수 있는 아이템이 된다.

이를 해결하는 방법으로 가상 상속(vitual inheritance), 추상 인터페이스의 사용(AddRef(), Release()), 플러그인 기법 등이 있지만 지나치게 복잡도을 증가시키게 되며 배보다 배꼽이 더 커질 수도 있다.
여고생치킨 : IT전문 용어로 배보다 배꼽이 더 큰 경우를 가르킨다. 그래도 강남에서 12만원이면 아주 적절한거다(응?).

여고생치킨 : IT전문 용어로 배보다 배꼽이 더 큰 경우를 가르킨다. 그래도 강남에서 12만원이면 아주 적절한거다(응?).

따라서 Rhea君 기준으로 가장 좋은 방법은 이 구조를 피하는 것이다!
이처럼 CAssassin과 CAmazon이 동시에 필요한 경우에는 CAssAmazon(어? 뜻이 좀 -_-;;;)라는 클래스를 아예 하나 만드는 것이다. 어차피 똥꼬아마존AssAmazon은 하나의 캐릭터 클래스이기 때문이다. 마치 C같네~, 멤버가 겹치네~, 같은 함수를 사용할 수 있네~라는 유혹이 뒤따른다. 하지만 그 개발자스런 유혹을 벗어나야 유지보수 단계가 편해진다.
이 단계에서 이런 일이 발생한다.

이 단계에서 이런 일이 발생한다.

사실 방금의 사례는 실제 개발자 혼자 개발하는 기간에도 자주 일어난다.
흔히 보아온 사례중 하나가 CRecordset 같은 DB 테이블을 하나의 클래스로 생성시킨 경우이다.
두가지 이상의 CRecordset 파생 클래스(의 객체)를 갖고 놀다가 어느 순간 두 클래스를 하나의 클래스로 다중 상속 시켜 작업을 하는 사례가 있다. 무엇 때문에 어떤 설계 방침을 갖고 그런 구조가 나오게 되었는지 모르겠지만... 상상력 하나만큼은 끝내준다.
이 이외에도 소켓 객체를 따로따로 갖고 놀다가 하나로 합치거나 아주 기가막힌 것을 볼때가 있는데 이런 경우 대부분, "왠지 그렇게 하면 될것 같은" 유혹에 빠져서가 아닐까 생각해본다.

또다른 다중상속의 슬픈 예

또다른 다중상속의 슬픈 예


아뭏든 본론으로 돌아와보면,
이게 정답이다.

이게 정답이다.


상기와 같은 클래스가 정답이다.
이는 IS-A, HAS-A 공식과도 맞아 떨어진다.

갑자기 나온 IS-A, HAS-A? 이게 뭘까?
이 이야기는 다음 시간 아이템을 다루며 상속과 포함이야기에서 다시 언급하겠다.

탐구생활 :
Rhea君은 귀찮아서 클래스 관계만 기술했을뿐, 각각의 멤버 함수와 멤버 변수를 적지 않았다.
또한 public과 protected, private 도 명시하지 않았다.
이제까지를 설명한 아래 소스에서 각자가 생각하는 멤버들을 채워보자.
또한 인스턴스를 직접 생성할 수 없도록 특정 클래스는 추상 클래스로 만들어보자.

class CGObject
{
};

class CCharacter : public CGObject
{
};

class CGameCharacter: public CCharacter
{
};

class CNPC: public CCharacter
{
};

class CPlayer: public CCharacter
{
};

class CEnemy: public CCharacter
{
};

class CAmazon: public CGameCharacter
{
};

class CAssassin: public CGameCharacter
{
};

class CSorceress: public CGameCharacter
{

};

class CAssassinAmzon: public CGameCharacter
{
};

class CMerchant: public CGameCharacter
{
};

class CPlayerAssassin : public CPlayer, public CAssassin
{
};

class CPlayerAmazon : public CPlayer, public CAmazon
{
};

class CNPCMerchant : public CNPC, public CMerchant
{
};

class CEnemySorceress : public CPlayer, public CSorceress
{
};



'소프트웨어 > C# & ASP.NET' 카테고리의 다른 글

C# 강좌  (0) 2010.01.12
C# 4.0의 새로운 기능  (0) 2009.12.15
잉여들을 위한 클래스설계 이야기 1/4  (0) 2009.12.11
Data Access Application Block  (0) 2009.12.09
문자열 비교 - 팁  (0) 2009.12.09