만순이(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~을 사용할 필요도 없을 것이다.
다시 말해 같은 솔루션이라면 굳히 private이 중요한 문제는 아니란 사실이다.
진짜 캡슐화를 신경써야 할때는 역시 DLL이나 COM처럼 어떤 함수를 파일 밖으로 노출 시킬 것인지 결정할 때다.
아무리 잉여라도 DLL을 만들면서 여러 변수를 그대로 노출시키는 짓을 하고 싶진 않을 것이다.
왜냐면 이용하기가 너무 귀찮으니까. 그래서 DLL을 이용한다는 특성-편리한 사용법-이 줄어드니까.
이쯤에서 답은 나왔다. 클래스를 만들때, 그 클래스 하나가 DLL 하나라는 생각, 혹은 이 클래스는 방대하므로 나중에 DLL로 따로 뽑는다는 계획을 갖는다면 어떤 것을 노출할 것인지에 대한 답은 스스로가 명확하게 알수 있을 것이다.
그럼 캡슐화에 대해 약간 더 생각해보자.
{
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로 선언된 함수 만이 폴리모피즘에 참여한다는 것이며, 객체의 포인터(혹은 참조자)를 통해서만 함수 호출이 이뤄진다는 것이다. 다시 말해 기본 클래스의 메법 함수를 가상 함수로 선언하고 기본 클래스 타입의 포인터를 가지고 함수를 호출하는 것이다. 이건 상속이 이미 구현된 우리 게임에서 써먹을 일들이 참 많다.
#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이 갖고 있는 함수를 실행시키는 개념이다.
여기에 더해, 단순 클래스 대신 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에 대한 고민을 계속 진행해보자.
