프로그래밍

The Modernest C++ #5 - 컴포넌트와 템플릿 메타프로그래밍 - 下

터너 (TerNer) 2016. 1. 27. 20:54

[이전 글]

The Modernest C++ #0 - 시작하며

The Modernest C++ #1 - 템플릿 메타프로그래밍 (Template Metaprogramming)

The Modernest C++ #2 - 타입을 가지고 놀아보기, type_traits

The Modernest C++ #3 - 수학 라이브러리

The Modernest C++ #4 - 컴포넌트와 템플릿 메타프로그래밍 - 上



컴포넌트와 템플릿 메타 프로그래밍 - 下


본 게시물에 첨부된 모든 이미지는 클릭시 확대됩니다. 참고하세요!


저번 시간에는 정적 멤버의 포인터를 활용하여 컴포넌트의 식별자(identifier)로 사용함으로서 오타 발생 방지, 속도, 생산성 면에서 획기적으로 도움을 받을수 있는 방법을 배웠습니다. 이로써 모든 작업이 끝난것 같지만 게임에 이용하기에는 아직 많이 허술합니다. 여러가지 방면으로 보강을 해줘야할 필요가 있습니다. 우선 문제를 직면하는 상황을 만들어서 그때 대처하는 방법을 알아보도록 합시다.





만일 컴포넌트 구조가 다음과 같은 상속 구조를 가진다고 할때,





위와 같은 코드는 널 포인트 참조에 의해 크래쉬가 일어날수 있습니다, 왜일까요? 상식적으로 이미지 렌더 컴포넌트는 렌더 컴포넌트를 상속했기 때문에 기본적으로 C++에서는 부모 컴포넌트로의 캐스팅이 자유롭습니다. 하지만 이미지 렌더 컴포넌트는 상위 클래스의 식별자가 아닌 자기 자신의 고유 식별자가 있으므로 렌더 컴포넌트의 식별자가 아닌 이미지 렌더 컴포넌트의 식별자로 저장이 되었기 때문에 상위 컴포넌트의 식별자로 컴포넌트를 가져오려고 해도 실패합니다. 또한 위와 같은 코드가 오류를 발생하게 되면 아래와 같은 상황에서 큰 문제가 발생할수도 있습니다. 





위 코드는 게임 내부에 존재하는 모든 게임 오브젝트에 대해서 순회하며 렌더링을 수행하는 코드 입니다. 모든 게임 오브젝트는 렌더링하는 타겟이 다를수 있습니다. 3D 메쉬, 문자열, 이미지등 렌더링 할수 있는 모든 상황이 존재하기 때문에 위와 같은 코드는 필수적으로 실행이 될수 있어야 합니다. 따라서 각자 렌더링하는 타겟에 따라 다른 컴포넌트 타입을 이용해서 컴포넌트를 가져오는건 설계 방법에 있어서 매우 비효율적이기 때문에 상위 클래스로 가져오는 것을 가능하게 해야합니다.


하지만 어떻게 할까요? 마땅한 방법이 떠오르지 않습니다. 저는 여기서 본격적으로 템플릿 메타프로그래밍을 활용해야 할 때라고 말씀드리고 싶습니다. ‘is_base_of’ 라는 메타함수를 이용하면 특정 타입이 다른 특정 타입의 부모 클래스인지를 확인 할 수 있습니다. 약간 감이 안오실수 있는데 예제와 함께 보여드리도록 하겠습니다.





보시는대로 is_base_of 메타함수는 상속관계를 추론할때 굉장히 유용하게 사용됩니다. 위 메타함수를 이용해서 컴포넌트를 삽입하는 과정에 있어서 부모 클래스를 판단하는 방법을 알아봅시다.





뭔가 좀 길지만 천천히 읽어보신다면 충분히 이해가 가능하시리라 믿습니다. 추가된것은 is_base_of 메타함수를 이용하여 컴포넌트가 CRenderComponent를 상속한다면 그 컴포넌트의 식별자로 작업을 하는것일 뿐 기본적인 동작 과정은 이전 글의 내용과 동일합니다. 잘 작동하는지 테스트를 해볼 차례입니다.





아주 잘 작동합니다. 이제 자식 컴포넌트로 삽입을 해도 부모 컴포넌트로 가져올수 있게 되었습니다. 하지만 여기서 끝이 아닙니다. 위 예제에서는 CRenderComponent만 존재하지만 실제 프로젝트 상에서는 여러개가 될수 있습니다. 만일 부모 클래스가 기하 급수적으로 많아지는 상황에 iks_base_of를 계속 추가해주는 것은 해결책이 되지 못합니다. 코드의 길이가 길어질 뿐만 아니라 부모 컴포넌트를 추가해줄때마다 계속 분기 코드를 추가해줘야 하기 때문에 생산성에 큰 영향을 줄수 있습니다. 


아래의 경우를 한번 생각해봅시다 :





자, CPositionComponent 라는 컴포넌트가 생겼고 그 컴포넌트를 부모 컴포넌트로 하는 상대위치, 절대위치 컴포넌트가 생겼습니다. 이때 프로그래머는 위 방법을 이용한다면 is_base_of를 이용하여 CPositionComponent를 상속하는지를 알아보는 코드를 추가를 해줘야 합니다. 그럼 분기문이 두개가 되고 프로그래머는 혼란스러워 질수 있습니다. 어떻게 하면 컴포넌트를 추가 할때마다 분기문을 추가해주지 않고도 부모 컴포넌트의 식별자 값으로 저장할 수 있을까요?


추가적으로 여기서 문제점이 또 발생하게 됩니다. 우리가 이미지 렌더 컴포넌트를 게임 오브젝트에 삽입할때 부모 컴포넌트인 일반 렌더 컴포넌트의 식별자 값으로 삽입을 해줄수 있었습니다. 하지만 논리적으로는 말이 되지 않지만 프로그래머가 상대 위치 컴포넌트를 삽입한 후에 절대 위치 컴포넌트를 삽입하면 두 컴포넌트는 부모 컴포넌트의 식별자 값으로 저장이 되기 떄문에 절대 위치 컴포넌트가 상대 위치 컴포넌트를 덮어 씌우게 됩니다.


코드로 설명해보자면 아래와 같습니다 :





그렇다면 일반 위치 컴포넌트를 상속하는 모든 컴포넌트는 논리적으로도 한 오브젝트가 상대위치와 절대위치를 동시에 가진다는 것은 알맞지 않기에 (상대위치와 절대위치 두 위치가 다를수 있기 때문) 게임 오브젝트에 단 하나만 존재해야 하는 컴포넌트가 됩니다. 이제부터 이런 컴포넌트를 유니크 컴포넌트 (Unique Component) 라고 부르도록 하겠습니다.


어떻게 하면 분기문을 추가하지 않고 유니크 컴포넌트들을 쉽게 추가할수 있을까요? 우리는 컴포넌트 식별자 객체로 눈을 돌려야 합니다. 이전 시간까지는 아무런 멤버도 가지고 있지 않았지만 이제는 기능을 추가해줘야 하며 메타 함수를 직접 구현해봐야 합니다.





컴포넌트 식별자 객체에는 몇몇 멤버를 추가하였습니다. 객체를 정의한 타입을 저장하는 current_component_t 와 부모의 타입을 저장하는 parent_component_t, 마지막으로 유니크 컴포넌트인지 여부를 저장하는 is_unique 총 세가지를 추가하였습니다. 프로그래머가 특정 컴포넌트 자신과 그것을 상속하는 모든 컴포넌트에 있어서 유니크 컴포넌트로 만들어 주고 싶다면 _is_unique 매개변수로 true를 넘겨주면 될것입니다. 자, 이제 이걸 컴포넌트 마다 추가해주는 작업을 거쳐봅시다.





모든게 완벽하지만 하나 문제가 생겼습니다. 최상위 컴포넌트는 부모 컴포넌트가 없는데 무엇을 추가해줘야 할까요? 우리는 C/C++ 뿐만 아니라 다른 언어를 사용할때도 아무 값도 정해주고 싶지 않을때나 값이 비어있다는 것을 명시해주기 위하여 null, NULL, nullptr, nil등을 이용해왔습니다. 타입에서도 그것이 가능한데 단순하게 빈 타입을 하나 정의해주면 됩니다. 






이제 최상위 컴포넌트에 있어서 부모 컴포넌트가 없으므로 null_t로 정해줬으며 모든게 완벽합니다. 이제 갖가지 기능을 하는 메타 함수를 제작해볼 차례입니다. 우리는 위에서 배운 내용으로 따르면 is_base_of 메타함수를 이용하여 대상 컴포넌트가 유니크 컴포넌트일때 부모 컴포넌트의 식별자 값을 구해와서 저장을 했습니다. 대상 컴포넌트가 유니크 컴포넌트인지 검사하는 메타 함수를 먼저 제작해봅시다.


우선 위에 새롭게 구현한 컴포넌트 식별자 객체에서 세번째 컴포넌트 매개변수로 true를 넘겨주면 자신과 자신을 상속하는 모든 컴포넌트가 유니크 컴포넌트가 된다고 언급 했었습니다. (유니크 컴포넌트 여부 자체를 상속하는 의미) 그럼 특정 컴포넌트의 부모 컴포넌트를 계속 따라 올라가면서 is_unique 멤버가 true로 설정된 컴포넌트 식별자를 가진 컴포넌트를 찾으면 될 것입니다. 





자, 내용이 비교적 난해할수 있으실텐데 하나하나 들여다보는 시간을 가져봅시다. 일단 컴포넌트 타입 하나를 매개변수로 받아와서 코드를 간략하게 하기 위하여 그 컴포넌트의 식별자 객체와 부모 컴포넌트를 얻어와서 typedef로 저장해둡니다. 그 다음 std::_If 메타함수를 이용하여 분기를 해줍니다. 분기의 내용은 이렇습니다 :


  1. 타겟 컴포넌트의 식별자 객체에 유니크 컴포넌트 여부를 나타내는 값이 true로 설정되있는지 확인

  2. 만일 true로 설정되있다면 타겟 컴포넌트는 유니크 컴포넌트 이므로 true_type를 리턴 (true와 동일)

  3. false로 설정되어있다면 타겟 컴포넌트의 부모 컴포넌트를 타겟 컴포넌트로 설정한후 메타 함수를 다시 실행 (재귀 호출)


위와 같은 과정을 거쳐서 부모 컴포넌트를 계속 쫓아 올라가면서 식별자 객체의 is_unique 멤버가 true로 설정 되있는지 여부를 모두 확인한 뒤, 하나라도 true로 설정되어 있다면 거기서 재귀 호출을 멈추고 타겟 컴포넌트가 유니크 컴포넌트임을 반환하게 됩니다. 그리고 템플릿 특수화를 이용하여 부모 컴포넌트를 계속 쫓아 올라가다가 null_t를 만나게 되면 강제적으로 false를 반환하게 됩니다.





아주 잘 작동하는 것을 확인할수 있습니다. 자 이제 컴포넌트가 유니크 컴포넌트임을 확인하는 방법까지 알았으니 유니크 컴포넌트가 되도록 해준 컴포넌트, 그러니까 컴포넌트 식별자 객체에 유니크 컴포넌트 여부를 설정한 컴포넌트를 얻어오는 방법만이 남았습니다. 예를 들면 이미지 렌더 컴포넌트로 따지면 일반 렌더 컴포넌트가 되겠죠? 이는 is_unique_component의 구현방식과 동일합니다. 재귀적인 호출을 해주면서 식별자 객체의 is_unique 멤버가 true로 설정된 컴포넌트를 반환해주기만 하면 됩니다.



 


get_unique_component의 작동 방식은 아래와 같습니다 :


  1. 타겟 컴포넌트의 식별자 객체에 유니크 컴포넌트 여부를 나타내는 값이 true로 설정되있는지 확인

  2. 만일 true로 설정되있다면 타겟 컴포넌트는 유니크 컴포넌트 이므로 타겟 컴포넌트를 리턴

  3. false로 설정되어있다면 타겟 컴포넌트의 부모 컴포넌트를 타겟 컴포넌트로 설정한후 메타 함수를 다시 실행 (재귀 호출)


이 메타함수도 is_unique_component와 같이 부모 컴포넌트를 계속 따라 올라가면서 식별자 객체의 멤버 값을 확인하고 맞다면 그를 반환하도록 하고, null_t를 만나면 메타함수가 더이상 처리를 하지 않고 null_t를 반환하도록 하였습니다. 이 메타함수가 잘 동작하는지 확인해볼까요?





테스트를 위하여 컴포넌트의 이름을 가져오는 컴포넌트의 이름을 가져오는 getComponentName 이라는 함수를 모든 컴포넌트에 정의해줬습니다. 이로서 컴포넌트가 유니크 컴포넌트인지 확인하고, 유니크 컴포넌트 여부 값이 설정되있는 컴포넌트를 구해오는 메타함수까지 모두 구현해줬습니다. 하지만 이것을 그냥 게임 오브젝트 객체에서 사용하기에는 분기문을 또 써줘야 하므로 모든 연산을 컴파일 타임에 밀어 넣는것은 불가능해질 수 있습니다.


get_unique_component와 is_unique_component를 이용하여 알아서 컴포넌트를 삽입하고 가져오는데에 적절한 컴포넌트를 가져오는 메타 함수를 제작해보도록 하겠습니다 :





뭔가 내용이 너무 길어져서 나름대로의 들여쓰기를 적용해봤습니다. 내용은 아주 단순한데, 만일 타겟 컴포넌트가 유니크 컴포넌트라면 타겟 컴포넌트를 유니크 컴포넌트로 만들어주는 컴포넌트를 가져오고 그렇지 않다면 자기 자신을 가져오게 됩니다. 코드는 정말로 복잡한데 정말로 간단하죠? (...) 이제 이것을 게임 오브젝트의 코드에 넣어주고 활용하는 예시를 보여드리도록 하겠습니다.





프로그래머는 해당 컴포넌트가 유니크 컴포넌트 여부를 따지지 않고 그저 메타함수에 타입만 넘겨주고 반환된 값으로 식별자 값을 얻어오기만 하면 알아서 적절한 컴포넌트 식별자 값을 찾아줍니다. 분기문을 쓸 필요도 없고 문자열이나 열거자등을 사용하여 생산성에 영향을 미치지 않고도 쉽게 구할수 있게 되었습니다.






위에서 사용된 getComponentName은 정적 멤버 함수였으나, 테스트를 위하여언더바(_)를 붙인 저 함수는 가상 멤버 함수로 정의 하였습니다. 개발중에 문제는 얼마든지 발생할수 있지만 컴포넌트 기반 개발을 이용하기 위한 모든 준비는 끝이 났습니다. 이로서 두 강좌에 걸친 컴포넌트와 템플릿 메타프로그래밍을 결합해보는 강좌를 마치도록 하겠습니다.


이번 강의에서 썼던 모든 내용을 올리도록 하겠습니다. 꼭 보시고 어디든 사용하셔도 좋으니 한번 코드를 분석해보시는 것도 좋을거라 생각합니다. 추가적으로 매크로를 이용하여 구현하였으니 꼭 코드를 확인해보시고 매크로를 이용하여 생산성까지 향상시키는 방법을 배워보시기 바랍니다.



Main.cpp






항상 제가 언급하는 것이지만 서도 템플릿 메타프로그래밍은 많은 이점을 줌과 동시에 많은 약점 또한 존재합니다. 가독성이 아마 가장 큰 요인으로 작용할 것입니다. 이 때문에 템플릿 메타프로그래밍을 프로젝트에 도입할지를 망설이고 계신 분들이 많을거라 생각됩니다.


제가 말씀드리고 싶은것은 템플릿 메타프로그래밍은 장단점을 모두 갖추고 있기 때문에 쉽게 결정하기 힘들다고 생각되며 가독성과 같은 단점을 감수할수 있다면 선택하는 것도 크게 도움이 될거라 생각하고 있습니다. 가장 좋은것은 상황에 맞게 사용 여부를 결정하는게 좋겠죠?


다음 강좌는 다시금 템플릿 메타프로그래밍에 대한 개념을 익혀보도록 하겠습니다.

반응형