프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다.
Posted by 친절한티스
안녕하세요. 친절한티스 라는 필명(?)을 쓰고 있는 박기헌입니다. 다른 네임드 필자분들이 워낙 고급 주제로 글을 올려주시는 바람에 저의 쩌렙티가 팍팍나는 글을 읽고 많은 분들이 실망하실까봐 걱정이 이만 저만 아닙니다. 그래도 작게나마 이제 막 프로그래밍에 입문하시는 분들에게라도 도움이 되겠지 생각으로 글을 써볼까 합니다.

RTTI란?
Run-Time Type Infomation 또는 Run-Time Type Identification 이라고도 합니다. 한글로 풀이하자면 "실시간 형식 정보" 라고 할수 있겠죠. 그럼 이게 무엇에 쓰는 물건인가? 라고 하면, 일단 다형성에 대해 알아야 할 것 입니다. C++에서는 대표적인 특징 중 하나로 다형성이라는 것이 있습니다. 부모 객체의 포인터로 자식 객체를 가리킬수 있는 것이죠. 간단하게 코드로 봐보자면 밑에와 같습니다.
class Parent
{
public:
	virtual void Print()
	{
		printf( "i'm your father!!\n" );
	}
};

class Child : public Parent
{
public:
	Child() : m_iMagicNum(777), m_pMagicPointer(NULL) {}

	virtual void Print()
	{
		printf( "i'm succeeding you father!!\n" );
	}

	void PrintMemVar()
	{
		printf( "Magic~ Magic~ : %d, %d\n" );
	}
private:
	int m_iMagicNum;
	int* m_pMagicPointer;
};

void main()
{
	Parent *pParent = new Child;
	pParent->Print();
}
위의 main 함수를 보시면 부모 클래스인 Parent 포인터 변수를 통해 Child 객체를 생성하여 Print 함수를 호출하고 있습니다. 여기서는 "i'm succeeding you father!!" 가 출력되겠죠. 이와 같은 코드는 실무에서 빈번하게 일어납니다. 실제 객체가 어떤 모양이든 간에 상관없이 인터페이스 클래스를 통해 객체들을 통합적으로 관리할수 있는 이점을 제공하기 때문이죠.

그런데 여기서 Child의 PrintMemVar 함수를 호출할 일이 생긴다면 어떨까요? pParent는 실제 Child 객체이니까  PrintMemVar 함수를 호출할 수 있어야 합니다. 그런데 pParent는 현재 Parent 형이니  PrintMemVar 함수에 대한 정보가 없습니다. Child 형으로 형 변환을 해주어야   PrintMemVar 함수를 호출 할 수 있죠.
void main()
{
	Parent *pParent = new Child;
	pParent->Print();

	// Child* 형으로 형변환
	Child *pChild = (Child*)pParent;
	pChild->PrintMemVar();
}
잘 작동합니다. pParent가 애초에 Child 객체이니 당연한 결과죠. 하지만 이 코드는 굉장히 위험한 코드입니다. 위의 코드야 앞서 pParent가 Child 객체인 것을 알고 있으니 이런 식으로 코드를 작성할 수 있는 것이지 실무에서는 현재 가리키고 있는 객체가 Child 형인지 Parent인지 또는 전혀 다른 상속 객체인지 알 수가 없습니다.

pParent가 Child가 아닌 Parent로 객체를 생성한 후, Child*형으로 형변환 하여 PrintfMemvar를 호출 해보면 전혀 예상치 못한 값들이 출력됩니다. 실제 프로젝트에서 이런 상황이 발생하게 되면 최악의 경우 크래시가 발생할 확률이 커집니다. 이런 문제를 방지 하기 위해 형변환을 하기 전 객체의 형식 정보를 확인할 필요가 있습니다.

RTTI는 바로 이 확인 작업을 위한 장치로 현재 객체의 형식 정보를 검사할 수 있는 명령어를 제공하고 있습니다. 대표적으로 dynamic_cast가 그것입니다. ( 이 외에, typeid, type_info 등이 있습니다. 자세한 것은  http://msdn.microsoft.com/en-us/library/b2ay8610.aspx 참고 )
void main()
{
	Parent *pParent = new Child;
	pParent->Print();

	// pParent가 Child* 형으로 변환 가능한지 검사
	// pParent가 Child* 형이 아니면 NULL 이 반환된다.
	Child *pChild = dynamic_cast<Child*>(pParent);
	if( pChild )
		pChild->PrintMemVar();
}
위의 메인 함수에 추가된 코드 입니다. dynamic_cast를 통해 형 변환을 수행하면 pParent가 가리키고 있는 객체가 Child 이라면 포인터를 반환하고 그렇지 않으면 NULL 포인터를 반환하게 됩니다. 이를 통해 안전하게 Child 형으로 변환하여 Child에서 정의한 함수를 호출할 수 있습니다.

dynamic_cast의 문제 
dynamic_cast 로 안전하게 다운캐스트( down cast, 부모형에서 자식형으로 형변환 )를 할수 있다면 모든 문제가 다 해결된거 같습니다. 그런데 문제가 있습니다. dynamic_cast가 느리다는 겁니다. dynamic_cast 를 사용하면 내부적으로 RTTI 정보를 체크하고, 변환할 객체에 대한 형식 정보를 비교하는데 이 수행 비용이 큰게 문제입니다. 

게임에서 퍼포먼스는 생명과도 같습니다. 아무리 멋진 게임이라도 프레임이 뚝뚝 끊기거나 느리면 하기 싫어지죠. 그래서 많은 프로그래머들이 1프레임이라도 더 빠르게 하기 위해 많은 노력을 합니다. 느린 dynamic_cast 대신에 빠르면서도 안전하게 다운캐스트 할수 있는 방법은 없을까 생각해볼 수 있죠.

만들어 봅시다
dynamic_cast 보다 빠르면서 같은 기능을 수행할 수 있는 방법은 의외로 간단하게 구현 할 수 있습니다. 
class CRTTI
{
public:
	CRTTI( const wstring strClassName, const CRTTI* pBaseRTTI );
	inline const wstring& GetClassName() const;
	inline const CRTTI* GetBaseRTTI() const;

private:
	const wstring		m_strClassName;
	const CRTTI*		m_pBaseRTTI;
};
위 클래스가 새로운 RTTI 검사를 위한 클래스입니다. 정말 간단하죠? 단순히 클래스 이름과 부모 클래스를 위한 멤버변수 하나가 전부입니다. 그러면 이것을 어떻게 이용해서 객체의 형식을 검사할지 보겠습니다.
// 최상위 클래스에 선언
#define DeclRootRTTI(classname) \
	public: \
		static const CRTTI ms_RTTI; \
		virtual const CRTTI* GetRTTI() const { return &ms_RTTI; } \
		static bool IsKindOf( const CRTTI* pRTTI, const classname *pObject ) \
		{ \
			if( NULL == pObject ) \
			{ \
				return false; \
			} \
			return pObject->IsKindOf( pRTTI ); \
		} \
		bool IsKindOf( const CRTTI* pRTTI ) const \
		{ \
			const CRTTI* pTmp = GetRTTI(); \
			while( NULL != pTmp ) \
			{ \
				if (pTmp == pRTTI) \
				{ \
					return true; \
				} \
				pTmp = pTmp->GetBaseRTTI(); \
			} \
			return false; \
		} 

// 자식 클래스들에 선언
#define DeclRTTI \
	public: \
	static const CRTTI ms_RTTI; \
	virtual const CRTTI* GetRTTI() const {return &ms_RTTI;}

#define ImplRootRTTI(classname) \
	const CRTTI classname::ms_RTTI(L#classname, NULL)

#define ImplRTTI( classname, baseclassname) \
	const CRTTI classname::ms_RTTI(L#classname, &baseclassname::ms_RTTI)


// Parent 클래스는 최상위 클래스다
class Parent
{
public:
	DeclRootRTTI(Parent); // RootRTTI 선언

public:
	virtual void Print()
	{
		printf( "i'm your father!!\n" );
	}
};
ImplRootRTTI(Parent);

// Child 클래스는 Parent로부터 상속 되었다
class Child : public Parent
{
public:
	DeclRTTI;

public:
	Child() : m_iMagicNum(777), m_pMagicPointer(NULL) {}

	virtual void Print()
	{
		printf( "i'm succeeding you father!!\n" );
	}

	void PrintMemVar()
	{
		printf( "Magic~ Magic~ : %d, %d\n" );
	}
private:
	int m_iMagicNum;
	int* m_pMagicPointer;
};
ImplRTTI(Child, Parent); // Parent에서 상속됨을 알린다
매크로 때문에 코드가 복잡해 보일 수 있지만 잘 보시면 별거 없습니다. CRTTI를 각 클래스에 정적 변수로 선언을 해주고, 상위 클래스가 있는 경우 CRTTI의 m_pBaseRTTI 멤버 변수에 상위 클래스의 CRTTI 정적 멤버 변수를 저장하는 것이 전부 지요. 그리고 이 각 클래스의 CRTTI 정적 멤버 변수를 비교함으로서 클래스의 형식 정보를 알아 낼 수 있습니다. 단순 포인터 비교이기 때문에 속도도 빠르지요.

그럼 실제 사용 예제를 보겠습니다.
#define IsKindOf(classname, pObject) \
	classname::IsKindOf(&classname::ms_RTTI, pObject)

void main()
{
	Parent *pParent = new Child;
	pParent->Print();

	// pParent가 Child* 형으로 변환 가능한지 검사
	if( IsKindOf( Child, pParent ) )
	{
		Child *pChild = (Child*)pParent;
		pChild->PrintMemVar();
	}
}


// 좀더 dynamic_cast 와 비슷한 방식으로 사용 하기 위한 방법
// DeclRootRTTI 매크로에 밑의 코드를 추가
static classname* DynamicCast( const CRTTI* pRTTI, \
	const classname* pObject ) \
	{ \
		if( !pObject ) \
		{ \
			return NULL; \
		} \
		return pObject->DynamicCast( pRTTI ); \
	} \
	classname* DynamicCast( const CRTTI* pRTTI ) const \
	{ \
		return ( IsKindOf( pRTTI ) ? ( classname* ) this : NULL ); \
	}

#define DynamicCast(classname, pObject) \
	((classname*) classname::DynamicCast(&classname::ms_RTTI, pObject))

void main()
{
	Parent *pParent = new Child;
	pParent->Print();

	// pParent가 Child* 형으로 변환 가능한지 검사
	Child *pChild = DynamicCast(Child, pParent);
	if( pChild  )
		pChild->PrintMemVar();
}
밑의 DynamicCast 부분을 보시면 dynamic_cast와 같은 방식으로 사용하면서도 수행 속도에서는 훨씬 빠른 형식 정보 검사를 할수 있습니다. 

해결해야 할 점
기존 dynamic_cast에 비해서 빨라졌다 하나 아직은 개선의 여지가 있습니다. 상위 클래스와 비교시 상속 깊이만큼 루프를 돈다는 점이나 메모리 비친화적이라는 점, 매크로를 많이 쓴다는 등이 있겠네요. 이런 점등은 좀더 고민해봐서 개선을 해봐야 할 것입니다. 

스샷 하나 없는 것이 심심해서 울겜 일러스트 한장 투척

 

댓글을 달아 주세요

  1. Favicon of https://gamedevforever.com ozlael 2011.12.21 17:21 신고  댓글주소  수정/삭제  댓글쓰기

    우와 소시다!! 티스 바람핀다!! 현아를 놔두고 소시를!!

  2. Favicon of https://gamedevforever.com zinzza 2011.12.22 19:29 신고  댓글주소  수정/삭제  댓글쓰기

    음... 일러가 좋군요... 위에 내용은 나중에 읽어야겠어요... 머리아파서 ㅠㅠ

  3. Favicon of https://hgcoder.tistory.com hgcoder 2012.01.03 10:25 신고  댓글주소  수정/삭제  댓글쓰기

    좋은글 잘 봤어요 감사합니다 ㅎㅎ
    부모 클래스이니 i'm your father!!라고 하는군요
    ㅎㅎㅎㅎㅎㅎ

  4. sgpro 2012.01.03 11:41  댓글주소  수정/삭제  댓글쓰기

    고맙습니다~ ^^
    (페이스 북처럼 "좋아요" 기능이 있으면 좋을꺼 같아요. ㅎㅎㅎ;; 왠지 "고맙습니다"만 적으면 장난같아 보이거나 성의 없어 보일까봐 왠지 죄송합니다. ㅠㅠ 정말 감사하고 고마운데 말이죠.. ㅠㅠ)

  5. Favicon of https://gamedevforever.com zinzza 2012.01.03 14:27 신고  댓글주소  수정/삭제  댓글쓰기

    윗분 말씀듣고보니 구글+ 의 +1 기능도-_-;;;
    아... 지금 읽어봤는데 머리아파요.
    전 아직 이런 글을 읽을 마음의 준비가 안돼있어요 ㅜㅜ

  6. Favicon of http://summerlight.tistory.com summerlight 2012.01.03 16:18  댓글주소  수정/삭제  댓글쓰기

    성능으로 인한 수동 RTTI의 구현은 항상 이슈가 되는 사항이죠 :) LLVM에서도 비슷한 구현을 합니다. 다만 이 쪽은 "POD를 사용하면서" "예상 가능한 성능"을 목표로 하기 때문에 타입 비교 루틴을 전부 수동으로 작성합니다.

    http://llvm.org/doxygen/Casting_8h-source.html

    그 외에 약간의 제약을 주면 O(1)로도 dynamic_cast를 구현할 수도 있습니다. 관심 있으시면 아래 논문을 참조해보시면 좋을 듯 합니다.

    http://www2.research.att.com/~bs/fast_dynamic_casting.pdf

  7. 김영민 2012.01.03 16:27  댓글주소  수정/삭제  댓글쓰기

    제가 이 주제로 글을 작성하려고 했는데 선수를 치셨군요...ㅠㅠ 다른 주제를 찾아야 겠네요... 수고하셨습니다.

  8. Favicon of https://gamedevforever.com zinzza 2012.01.03 19:07 신고  댓글주소  수정/삭제  댓글쓰기

    C#이면... 이렇게...

    if(pParent is Child)
    {
    pParent = (pParent as Child);
    }
    느릴려나...ㅡ.ㅡ?

  9. Favicon of https://gamedevforever.com Rhea Strike 2012.01.03 22:34 신고  댓글주소  수정/삭제  댓글쓰기

    typeid를 이용한 기초는 http://rhea.pe.kr/260
    솔직히 이건 짤방을 올리고 싶어 억지로 쓴 티가 나네요.

  10. Favicon of https://gamedevforever.com cagetu 2012.01.03 23:31 신고  댓글주소  수정/삭제  댓글쓰기

    이왕 시작한 김에 Object System에 대해서, 연재로 정리하면 좋겠는데?! ㅎㅎ

  11. Favicon of http://bluekms21.blog.me 크로스 2012.01.04 16:16  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사히 잘 읽었습니다.
    다이나믹 케스트가 느리다는건 익히 알고 있었는데 이런 방법이 있었네요.
    그런데 MEC++에 보면 typename을 가지고 구현하기보다 typeid의 주소를 가지고 비교하는 방법이 호환성 측면에서 더 낫다고 주석달려 있던데.. 이 문제는 요즘엔 다 통일되어있나요? (MEC++가 이제는 꽤 옛날 책이 되버렸죠;;)
    게다가 가상함수가 없는 상속관계인 클래스들을 집어넣었을때 아마도 미정의동작이 발생할 것 같은 느낌인데.. 어떻게 예외라도 발생시켜서 막을 방법이 있을가요..? 아니면 그냥 캐스트 실패로 빠져나오나요??

  12. Favicon of http://Junios.net Junios 2012.01.04 16:59  댓글주소  수정/삭제  댓글쓰기

    잘보고 갑니다. ㅎㅎ 다른 방법도 고민을 해봐야겠군요.

  13. Favicon of http://www.twitter.com/BurningBottle 버들 2012.01.04 22:12  댓글주소  수정/삭제  댓글쓰기

    감사히 읽었습니다. 왜 나는 여태껏 이런 걸 몰랐는가 자책하면서 ㅜㅜ
    다운 캐스팅이 필요한 상황을 해결할 때에는 크게 두 가지 방법으로 나뉜다고 생각합니다.
    1. 위 글과 같이 RTTI를 사용한 안전한 다운 캐스팅
    2. 걍 인터페이스를 모두 통일해서 다운 캐스팅이 필요 없어지게 하는 방법

    두 가지 모두 장단점이 있고, 개인적으론 2번 방법을 더 선호해 왔습니다. 이에 대해 어떻게 생각하시는지 궁금합니당ㅎㅎ

    • Favicon of https://gamedevforever.com 친절한티스 2012.01.05 09:52 신고  댓글주소  수정/삭제

      개인적이 스타일이 필요한 곳에 필요한 것만 정의하자 여서 1번 방식을 더 선호하게 되네욤.

      위의 예제에서도 Child의 PrintMemvar 같은 함수는 Child에서만 사용하는 함수인데, 이것을 인터페이스쪽에 정의해버리면 인터페이스를 상속 받는 모든 클래스가 갖게 되잖아욤. 저는 이런게 좀 걸리더라구요.

  14. hova_moon 2012.01.17 22:15  댓글주소  수정/삭제  댓글쓰기

    다운캐스팅 안정성 보다는
    형 정보 찾기만 생각해서 짰었었는데..
    조금 더 고민해봐야 겠어요~~!! 감사합니다~!

  15. Favicon of http://bluekms21.blog.me 크로스 2012.05.17 10:54  댓글주소  수정/삭제  댓글쓰기

    사소해보일지 모르는 질문입니다만
    ms_RTTI
    에서 m은 맴버인것 같고.. s는 어떤것을 의미하는지 궁금합니다.
    혹시 아시는분은 답변좀 주세요.. ;ㅅ ;
    이 포스팅으로 RTTI하나는 확실히 잡고가네요 ^_^