- 이전 이야기(사실은 뻥!!) -
만순이(24, 전문대 2학년. 잉여+찌질이)는 복학후, C++ 수업을 듣다가 클래스 설계 과제를 받게 되었다.
잉여답게 나베르(구글은 영어가 딸려 접속 불가) 검색을 하다가 대강 클래스 소스를 받아 들고
대강대충 상속과 포함을 넣어 레포트를 제출했다.

But, Copy & Paste의 보답인지, 결과는 D0.
제출했다는 것에 의의를 두지 않는 교수의 만행이었다.

그럼 만순이의 과제를 살려 C0를 B+이상의 성적으로 만들어보자!
(A는 교수의 주관이거나 교수에게 잘보인 여학생-또한 같은 과 남학생들과는 절대 놀지 않는-에게만 주는 점수이므로 감히 A0 이상을 받자고 뻥치진 못하겠다.)

대한민국에서 일반적인 남여차이

대한민국에서 일반적인 남여차이



지난 시간까지 상속이야기는 마쳤다.
AS-A 관계 이상으로, 조금더 세부적인 관계성 이야기가 있지만 솔직히 다 필요없다.
IS-A, AS-A만 잘 구별해서 쓰면 좋은 습관이 된다.
여대생 필기 노트답게 더 자세하고 빡빡히 나눠봤자 아무런 의미가 없다.

마직막으로 다룰 주제는 캡슐화(Encapsulation = 은닉화(Hidden))와 폴리모피즘(Polymorphism =  다형성)이 되겠다.

흔히 OOP의 3대 요소로 은닉, 다형성, 상속을 뽑는데
Rhea君 기준으로 그중 가장 중요한 것은 다형성이라 생각한다.
디자인 패턴, 인터페이스, COM, Plug In 등등, 가장 많이 사용되는 것이 바로 다형성이다.

지극히 개인적이고 다른 시각에서 보면 다형성을 위해 은닉이 존재하고
다형성 때문에 상속 구현이 복잡해지는 것이란 생각까지 들 정도이다.


캡슐화
C++ 자체에서 가장 많이 사용되는 캡슐화는 public으로 놓인 멤버변수를 public 속성의 Get~()/Set~()으로 바꾸는 것이다.
간혹 나름 개발 회사 밥 조금 먹어봤다는 잉여는 "실전에선 그딴거 안써~!" 하며 public 속성으로 멤버 변수들을 막 넣어주신다.
이런 경험 누구나 있을 것이다. 그리고 의문이 든다, 실전에선 그런거 정말 안쓰는지, 그리고 캡슐화를 왜 하는지.

캡슐화를 이해하기 위해서 한 프로젝트 내에 사이좋게 들어있는 C++코드를 잊고 DLL이나 COM 인터페이스를 생각해보길 바란다(그런거 해본적 없다면 단순히 ActiveX 컨트롤을 생각해도 되겠다.).

ActiveX 컨트롤을 포함한 COM 객체의 가장 큰 장점은, 그 내부 구현 과정을 전혀 몰라도 원하는 기능을 전부 사용할 수 있다는 것이다. 예컨데 IE의 통신 모듈이나 렌더러, DOM 구조를 몰라도 누구나 IE 컨트롤을 끌여당겨 IE가 해주는 모든 기능을 사용할 수 있다.

그런데 평소 그런 컨트롤을 사용할 때, 직접 멤버 변수를 조작하는 경우가 있던가?
멤버 변수를 다룰 때는 속성(Attribute)이라는 성격으로 객체의 동작을 정의할수 있지만 대부분의 기능은 메서드(멤버함수)로 구현되어 있었을 것이다.

즉, 어떤 객체를 사용하자 할때 그 내부 기능을 몰라도 사용하는데 편리하게끔 해주는 것이 캡슐화이다.
이것은 혼자 코딩하는 것이 아닌 대규모 코딩시 더더욱 빛을 발하게 된다.
결국 자기 혼자만 사용하는 클래스거나 별로 복잡하지 않은 클래스라면 Get~인지 Set~인지는 별로 중요한 사항이 아니다.
특히 같은 솔루션 내의 상황이라면 굳히 private과 Get~, Set~을 사용할 필요도 없을 것이다.

C#에서는 internal 이라는 재미있는 액세스 한정자가 있는데 이것은 같은 어셈블리(솔루션이라 이해하자.)에서는 접근이 가능하지만 다른 어셈블리에서는 접근을 제한하는 역활을 하고 있다.
다시 말해 같은 솔루션이라면 굳히 private이 중요한 문제는 아니란 사실이다.

진짜 캡슐화를 신경써야 할때는 역시 DLL이나 COM처럼 어떤 함수를 파일 밖으로 노출 시킬 것인지 결정할 때다.
아무리 잉여라도 DLL을 만들면서 여러 변수를 그대로 노출시키는 짓을 하고 싶진 않을 것이다.
왜냐면 이용하기가 너무 귀찮으니까. 그래서 DLL을 이용한다는 특성-편리한 사용법-이 줄어드니까.

이쯤에서 답은 나왔다. 클래스를 만들때, 그 클래스 하나가 DLL 하나라는 생각, 혹은 이 클래스는 방대하므로 나중에 DLL로 따로 뽑는다는 계획을 갖는다면 어떤 것을 노출할 것인지에 대한 답은 스스로가 명확하게 알수 있을 것이다.

friend 지정자는 맘대로 볼수 있지만 사용하지 않는 것이 좋다.

friend 지정자는 맘대로 볼수 있지만 사용하지 않는 것이 좋다.


그럼 캡슐화에 대해 약간 더 생각해보자.

class CGameFileManager
{
public:
 void Save(string strUserID, int& iLevel, LONG64 lExp); 
 void Load(string strUserID, int& iLevel, LONG64 lExp);

 string m_strUserID;
 int m_iLevel;
 LONG64 m_lExp;
}

이것은 이제까지 만든 우리의 게임에 대한 세이브/로드를 해주는 클래스를 생각해본 것이다.
그러나 문제는 무엇일까?

1) Save(), Load()는 게임 데이터가 저장되는 데이터베이스 or 저장장소를 직접 액세스하고 있으므로 게임 저장이 특정한 구현에 얽매이게 된다.
2) 저장하고자 하는 게임 정보가 늘어날수록 관리가 힘들어진다. ID, 레벨, 경험치만 넣었지만 RPG에서 이것만으로는 택도 없다는 사실은 유치원생도 직감으로 알고 있을 것이다.

그래서 아래와 같이 바꿔보았다.

class CGameSaveInfo
{
public:
 string GetUserID();
 void SetUserID(string UserID);
 int GetLevel();
 void SetLevel(int Level);
 LONG64 GetExp();
 void SetExp(LONGLONG Exp);

private:
 string m_strUserID;
 int m_iLevel;
 LONGLONG m_lExp;
};


class CGameFileManager
{
public:
 void SetGameInfo(GameSaveInfo& gameInfo);
 void Save(GameSaveInfo& gameInfo);
 void Load(GameSaveInfo& gameInfo);

private:
 GameSaveInfo m_GameInfo;
};

구현부는 없지만 CGameSaveInfo를 따로 만든 것은 저장위치에 관한 투명성을 제공해준다. CGameSaveInfo를 추상클래스로 만들고 CGameSaveInfoDB, CGameSaveInfoHDD, CGameSaveInfoStorge식의 파생클래스를 만들어 사용하면 실제 저장에 대한 구현은 CGameSaveInfo로 캡슐화시킬수 있으며 Save(), Load()는 자료형에 관계없이 사용할 수 있다. 즉, 서버에서든 클라이언트에서든지 Save(), Load()는 똑같이 사용할 수 있다.

또한 자기 자신을 감시하기 편하므로 m_GameInfo가 달라질때마다 자동 저장하거나 혹은 작업에 대한 히스토리를 만들기 유리해진다!

당연한 말이지만 세이브시의 자료형이 늘어나도 CGameFileManager 클래스는 바뀔 게 없다(그래서 구조체를 사용하잖아.). 여기에서 좀더 욕심을 부려보는 것이 자료형의 추상데이터형(ADT, abstract data type)이다.
결론적으로 클래스에게 넘겨주는 자료형을 하나의 단일한 자료형으로 사용하는 것인데 이미 COM에서는 VARIANT라는 자료형이 그 역활을 하니 정의를 찾아보기 바란다(OAIDL.h에 정의되어 있다.).

이것으로 언급한 1), 2)번의 문제가 해결되었다.
캡슐화가 진행되며 알게모르게 데이터와 함수의 분리가 이뤄졌는데 데이터에 얽매이지 않는 함수가 재사용의 첫시발점이란 사실을 잊지말자.

단순히 Get~/Set~ 함수 만들기 노가다가 아닌 캡슐화의 사용 목적이 담긴 설계를 해가면 만순이의 잉여력은 감소하며 점수는 오르지 않을까 싶다.


폴리모피즘
폴리모피즘의 두가지 조건은 virtual로 선언된 함수 만이 폴리모피즘에 참여한다는 것이며, 객체의 포인터(혹은 참조자)를 통해서만 함수 호출이 이뤄진다는 것이다. 다시 말해 기본 클래스의 메법 함수를 가상 함수로 선언하고 기본 클래스 타입의 포인터를 가지고 함수를 호출하는 것이다. 이건 상속이 이미 구현된 우리 게임에서 써먹을 일들이 참 많다.

부모클래스의 virtual에게만 허용한다.

부모클래스의 virtual에게만 허용한다.

#include "stdafx.h"
#include <iostream>
#include <conio.h>
#include <WTypes.h>
using namespace std;


class CWeapon
{
public:
 virtual BOOL IsMissile() const { return FALSE; }; 
};

class CArrow : public CWeapon
{
public :
  BOOL IsMissile() const { return TRUE; };
};

class CSword : public CWeapon
{
public :
 BOOL IsMissile() const { return FALSE; };
};

class CAxe : public CWeapon
{
public :
 BOOL IsMissile() const { return FALSE; };
};


int _tmain(int argc, _TCHAR* argv[])
{
 CArrow arrow;
 CSword sword;
 CAxe   axe;

 CWeapon* WeaponArray[] = { &arrow, &sword, &axe };

 for (int i = 0; i < 3; i++)
 {
  cout << "Weapon " << i + 1;
  if(WeaponArray[i]->IsMissile() == TRUE)
   cout << " is Missile.\r\n";
  else
   cout << " is Meele.\r\n" ;
 }

 getch();
 return 0;
}

위의 소스는 화살, 검, 도끼가 날으는 무기인지를 판별하는 초간단한 예제이다.
밑줄 친 세번째 줄에서 하나의 객체(최상단)로 파생 클래스들의 객체들을 관리하게 모습을 보여준다.
이것을 어떤 식으로 적용하면 유용한지는 각자의 상상에 맡기겠다.

거듭말하지만 이런 방법은 캡슐화와 마찬가지로 "한 C++ 솔루션내 클래스"들이라는 사고의 제약을 넘을 때 보다 막강해진다.
여기에서 각 클래스들이 DLL 파일로 바꾼 것이 플러그인(Plug In)이다.
윈도우의 폰트나 포토샵의 필터, 3D MAX의 각각의 기능들이 전부 플러그인 기법으로 만들어져 있는데
EXE 파일이 호출하는 하나의 함수를 위 소스와 같이 로컬 내의 DLL들에게 날려주고 거기의 걸맞는 DLL이 갖고 있는 함수를 실행시키는 개념이다.

잘못된 DLL이 있습니다.

잘못된 DLL이 있습니다.


여기에 더해, 단순 클래스 대신 1) interface가 추가되고, 2) 인터페이스를 이름 대신 GUID로 판별하고, 3) DLL의 물리적인 위치를 레지스트리에 기록해 두며, 4) 클래스 생성시 팩토리 패턴이 붙는 것이 바로 COM 기법이다. DLL이 다른 PC에 둔다면 TCP/IP로 통신하며 이를 DCOM, COM+이라 부른다.
(인터페이스 사용법은 http://rhea.pe.kr/171 를 참조)
또한 C++내에서 이런 상속, 캡슐, 폴리모피즘  기법을 적절하게 사용해서 장난친 것이 디자인 패턴이니 몇가지 패턴 사례를 살펴보기 바란다.

위의 폴리모피즘은 각자의 프로그램 내에서 사용해봄직한 곳이 많을 것이다.
특히 게임처럼 객체들의 현재 런타임 확인이 들어가며 다수의 객체를 관리하는 프로젝트에서는 상속과 폴리모피즘의 두드러진다.

이상으로 4회에 걸쳐 잉여들을 위한 클래스설계 이야기를 다뤄보았다.
결론적으로 이 이야기에서 흔히 말하는 고급기법(?)은 적지 않았는데 이 연재 자체가 나중에 고급기법을 위한 기초 과정이자 떡밥이기 때문이며 실제로 이 정도만 잘 적용해도 깔끔한 설계 및, 대부분의 학교에서 가르키는 OOP의 채점 기준에는 적합하기 때문이다.

마지막으로 언제나 하는 말, Simple is the best란 말을 잊지말고 OOP에 대한 고민을 계속 진행해보자.

끝이 아닌건가?

끝이 아닌건가?




오늘 소스는 'Waite Group의 COM/DCOM 프라이머 플러스'에서 상당부분 발췌했습니다.


크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Rhea君

지난 시간 이야기에서 한가지 의아한 점이 떠오르는 잉여도 있을지 모르겠다.
(그렇다, 사실 의문점이 떠오르면 이미 잉여를 탈티한 것일지도!)
머냐면 MFC의 CCmdTarget같은 클래스가 없었다는 것이다.

CCmdTarget를 뺀 이유는 이 클래스 설계 이야기는 일단 논리적인 설계 이야기이기 때문이다.
CCmdTarget같이 메시지를 처리하는 클래스가 들어간다는 것은 UI가 붙는다는 이야기이며
메시지 처리와 UI가 붙게 되면 이건 논리적인 클래스 설계 이야기가 아니라 엔진 이야기로 흘러간다.
그것도 범용 엔진인지, 엔진과 게임 컨텐츠를 합쳤는지에 따라 복잡도가 나뉘며
(당연히 범용 엔진이 훨~~~~얼~~~~씬 더 크다. 범용 윈도우 라이브러리인 MFC를 생각해보라.)
대강 구현해도 클래스랑 인터페이스 수십개는 마구마구 쏟아져 나온다.

그만큼 실제 게임 프로젝트와 학교나 학원에서 배우는 OOP는 실로 차이가 크다고도 할수 있겠다.

CCmdTarget을 빠뜨린 것에 질문을 하지 않은 여고생은 이렇게 한다.

CCmdTarget을 빠뜨린 것에 질문을 하지 않은 여고생은 이렇게 한다.


머 어찌됐던, 3번째 이야기로 들어가보자.

다만 이제부터는 디아블로2와 게임 컨텐츠가 좀 다르다고 까진 말아주세욘. ^^
원래 디아블로2 클래스 만들기가 절대 아니었고 예로써 디아블로2를 든 것일 뿐이니까 말이져~

게임에는 아이템이 있다. 그걸로 밥먹고 사는 우리들로썬 아이템의 중요성은 설명하지 않아도 될 것이다.
그리고 게임에는 비단 마법사 뿐만 아니라 적 캐릭터를 포함한 모든 직업 클래스에 마법속성이 부여된다.

처음부터 특정 캐릭터 클래스는 무슨 속성이라고 정해줄수도 있으며.
모든 속성(파이어, 아이스, 대지, 공기, 정령계....)들을 수치로 가지고 있고 그 속성들 중 특정 속성이 상대적으로 높고 낮냐에 따라 결정될 수도 있겠다.

비단 캐릭터 뿐이겠는가?
아이템에도 속성이 있고 장비와 무기에도 속성이 있다.
예컨데 노템(홀랑 벗었을)일 경우, 파이어 속성을 지닌 법사가
얼음 속성을 지닌 옷과 무기 및 장비들을 한가득 입고 나서 아이스계 법사가 될수도 있을 것이다.

밀리 속성인 타이가가 모에 속성을 지닌 오버니삭스를 착용하고 있다. 그러므로 타이가는 모에하다.

밀리 속성인 타이가가 모에 속성을 지닌 오버니삭스를 착용하고 있다. 그러므로 타이가는 모에하다.

머, 그럴 경우 원래 장비속성 빼기 캐릭터 속성 어쩌고 해서 공식이 만들어지겠지만
그런건 시스템 기획자의 보구인 엑셀로 알아서 잘해줄 것이다.

결론적으로 이 게임은 모든 직업 클래스와 무기 및 아이템 클래스는 속성을 갖고 있어야 한다라는 대명제가 생겼다.
따라서 개발자인 우리는 지난 시간의 클래스 구조도를 대폭 손질해야 한다.

어떤 방법이 있겠는가?
가장 쉬운 방법은 지난 시간에 없었던 아이템 클래스를 먼저 추가시키고,
모든 CGObject 아래에 속성값을 지니는 클래스를 끼워넣는 것이다.

먼저 아이템을 넣자.
아이템은 캐릭터와 함수와 변수를 함께 써 먹을 일이 없다. 따라서 지금까지의 상속관계와 무관하다.

여기 아이템과 무기 추가요~

여기 아이템과 무기 추가요~

흠... 일단 아이템을 넣었다.

이제 위에서 언급한 대로 CGObject 밑에 속성 클래스를 두자.
게임내 등장하는 모든 객체들은 속성을 지닌다.

게임내 등장하는 모든 객체들은 속성을 지닌다.


이것으로 모든  캐릭터와 아이템은 속성을 지니게 되었다! 와~~~!! 행복하다!!!
이번 강좌는 이것으로 끝!!!!





...이면 얼마나 좋을까?

세상은 이렇게 단순하지가 않다.

그러나 이 복잡한 세상을 단순화 시켜주는 클래스 설계 공식이 있으니 이른바 IS-A 관계HAS-A 관계가 그것이다.

응?

응?


먼저 IS-A란 클래스 설계시 상속이 적절한가를 판단할 때 사용하는 것으로 "파생 클래스는 슈퍼 클래스이다" 라는 논리가 파당하다면 상속해도 된다는 의미이다. IS-A는 지난 시간 만든 클래스에 딱 맞아떨어지는데, 모든 클래스를 두고 한번 이 공식에 넣어보자 생각해보자.

(       ) 클래스는  (       ) 클래스이다.

여기에 대입해보면,

아마존은 게임 캐릭터이다.
상인은 게임 캐릭터이다.
NCP는 캐릭터이다.
어세신은 객체이다.
......

등의 관계가 적절함을 알 수 있다.

지난 시간, 잘못된 상속의 사례였던 CAssAmamzon을 생각해보자.
CAssAmamzon는 CAssassin과 CAmazon을 다중상속받았다.

어세신-아마존은 어세신이다.
어세신-아마존은 아마존이다.


흠... 이러한 관계는 적절치 못하다. 어세신-아마존은 결코 어세신도 아니며 아마존도 아닌 제 3의 클래스이기 때문이다.

얼마 전, 어떤 잉여의 상속 설계 숙제에서 전형적으로 실수한 부분을 본적이 있는데,
육식동물과 초식동물을 다중상속받은 잡식동물 클래스를 본적이 있다.

아줌마~ 여기 잉여 하나 추가요~ ^--^

아줌마~ 여기 잉여 하나 추가요~ ^--^


육식동물과 초식동물의 속성과 메서드를 한꺼번에 상속받으면 그럴싸한 것 같지만 절대 상속해선 안되는 좋은 사례가 된다.
특히 앞으로 해볼 다형성(Polymorphism, 폴리모피즘) 구현에서 치명적인 실수가 드러나게 되며 절대 개발할수 없는 설계로 가는 지름길이다. 문제는 많은 좆뉴비들과 잉여들이 자신도 모르게 이렇게 해버리는 경향이 많은데, 프로그래밍 이전에 논리력이 의심이 된다. 찰스 다윈의 종의 기원이나 철학책을 읽으며 기본적인 논리공부를 병행하길 바란다.

아뭏든 앞으로 상속할지 여부는 이 IS-A 공식에 대입해보면 된다.
그래서 논리가 타당하면 상속해도 되며, 논리가 맞지 않다는 의심이 들면 상속을 피하길 바란다.

그럼 오늘의 클래스 설계는 어떤가?
상기대로라면 실제 인스턴스 생성시 "아마존은 파이어이다.", "양손검은 아이스이다."라는 논리가 생겨버리게 된다.
당연히 논리에 맞지 않다!!

정확한 논리는 "아마존은 파이어속성을 갖고 있다.", "양손검은 아이스 속성을 갖고 있다."라고 불려야 한다. 이것이 바로 HAS-A 관계이다. 즉, 속성은 상속의 대상이 아니라 속해있는 대상이므로 하나의 클래스가 다른 클래스를 상속받는 것이 아닌 포함의 관계를 만들어야 한다. 물론 이것은 일반적인 클래스 상속으로도 구현할 수는 있다. 그러나 전형적인 결합도(coupling) 상승의 원인이 되므로 피하는 것이 좋다.

"소서리스는 아이스속성을 갖고 있다"라는 명제를 나타내보자.
귀찮아서 아이스는 int 100 이다. =_=;;

#include <iostream>
using namespace std;

//............

class CAttribute : CGObject
{
public:
 int GetCurrentAttribute() { return 100; }   // 아 귀찮...ㅠㅠ
};

class CSorceress: public CGameCharacter
{
public:
 virtual void foo() = 0;
 CAttribute* m_Attribute;           // 포함
};

class CPlayerSorceress : public CPlayer, public CSorceress
{
public:
 CPlayerSorceress()
 {
  m_Attribute = new CAttribute;    // CSorceress 에서 멤버로 갖고 있다. 포함의 위치는 각자 알아서 구현하자.
 }
 ~CPlayerSorceress()
 {
  delete m_Attribute;
 }
 
 void foo() {}
 int GetAttribute()
 {
  return m_Attribute->GetCurrentAttribute();
 }
};

int _tmain(int argc, _TCHAR* argv[])
{
 CPlayerSorceress Sorc;
 cout << Sorc.GetAttribute() << endl;
 return 0;
}

CAttribute에 대한 물리적인 상속구현은 없었지만 멤버로 유효하며 그 결과 역시 잘 나온다.

포함으로 구현한 속성...응?

포함으로 구현한 속성...응?

이렇듯 HAS-A 관계에서는 결합도를 낮추면서도 원하는 멤버를 사용할 수 있게 되었다.
아쉽게도 이제까지 써온 VS2008의 다이어그램 기능은 C++에서 포함을 나타내주진 못하고 있기에 다이어그램은 생략한다.
(되는 방법이 있으면 알려주세요.)

여기에 좀더 생각해볼 것은, COM에서는 애초에 다중상속을 지원하지 않는다는 사실이며 이와 같은 방법으로 구현한다는 점이다.
그 천재들이 만든 COM이 왜 다중상속을 지원하지 않으며 굳이 포함과 통합이란 방법을 쓰는지 생각해볼 일이다.

다시 정리하자면 IS-A로 나타낼수 있으면 그 상속은 타당하다.
그리고 HAS-A로 나타나면 물리적인 상속 대신 논리적인 상속, 즉 해당 객체를 멤버로 가지도록 하자.

오늘의 탐구생활~
1) CItem과 CWeapon에서 상속받은 실제 아이템과 무기 클래스를 만들자.

2) 혹시 누군가 "이 아이템은 땅에 버리게 되면, 지가 알아서 막 공격도 하고 도망가도록 해주세요~"라고 했을땐 어떻게 해야할지도 고민해보자.

3) 이제까지 CPlayer, CEnemy, CNPC는 CCharacter에서 상속을 받았다. 오늘 포스트에 기준해 이것들의 각각의 상속이 논리적으로 타당한지에 대해 치고 박고 싸워보자.

마지막으로 잉여에게 뇌세포가 딸릴 것이 분명한 고급 탐구생활!!!

4) 실제 CPlayerAmazon의 인스턴스는 무기와 아이템을 "가진다".
그렇다면 무기와 아이템을 CPlayerAmazon의 멤버로 포함시키면 어떨까?
그리고 실제 데미지를 주는 공격함수는 무기와 아이템들이 가질 것인가? 아니면 CPlayerXXXX가 가질 것인가?
이 문제에 대해 깊이있는 OOP에 대한 짱구를 돌려 보자. 뜬금없게도 이것은 나중에 게임 속 인공지능에 대한 기초가 된다.












따, 딱히 정답 요청 리플을 바, 바라는 건 아니라구...

따, 딱히 정답 요청 리플을 바, 바라는 건 아니라구...



자, 그럼 대망의 마지막회에서는 은닉(캡슐화)과 다형성에 대해 적어보게따~~*


폴리모피즘의 적절한 예

폴리모피즘의 적절한 예



크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Rhea君


본론으로 가기전 여기에서 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
{
};

퍼갈땐 퍼간다고 말씀하시고, 링크및 트랙백을 걸어주세요.
그리고 제발 본문 수정은 금지합니다.


 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Rhea君