알라딘MGG와이드바


데이터 중심 디자인 (혹은 OOP의 위험성)

C++ for game programmer 라는 책의 저자이자 나에게는 unittest++이라는 C++용 단위테스트 프레임워크 개발자로 더 익숙한 Noel Llopsis가 쓴 Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP)이란 글을 김학규님이 번역한 내용을 펌글한다. 원래 원본글은 여기에 있었어야 하나 링크가 깨져있어서 자료의 영속성을 위해 내 블로그에 옮긴다. 혹시 김학규님이 이 블로그 글을 내리기를 요청하시거나 원본글 링크가 살아난다면 이 블로그 글은 사라질 수도 있다.

데이터 중심 디자인 (혹은 OOP의 위험성)
------------------------------------------------------

이런 상황을 생각해보자. 개발 기간의 막바지에 다다랐는데, 우리의 게임은 느려터져 버벅대고 있다. 하지만 profiler를 돌려봐도 특별히 한군데에서 시간을 잡아먹는 지점은 찾을 수 없다. 범인은? random memory access pattern과 상습적인 cache miss 다. 성능을 끌어올리기 위해 코드의 부분을 병렬화를 시도해보지만 엄청난 노력이 필요하다. 그리고 성능향상의 상당부분은 동기화 부분에서 다시 까먹게 된다. 거기에 코드는 훨씬 더 복잡해져서 버그를 잡다보면 더 많은 문제가 생겨날 지경이고, 무슨 기능을 추가하고 싶은 생각은 바로 사그러들고 만다. 익숙한 상황 아닌가?

위의 시나리오는 필자가 지난 10년간 참여한 대부분의 개발프로젝트에서 접한 상황을 거의 정확하게 설명한 것이다. 이런 문제의 원인은 우리가 사용하는 프로그래밍 언어때문도 아니고, 개발 툴 때문도 아니고, 경험이 부족해서도 아니었다. 내 경험에 의하면 원인은 OOP (객체 지향 프로그래밍) 와 그와 관련된 관습들에 있다. OOP는 사실 당신의 프로젝트를 돕기보다는 방해를 하고 있는 것이다!


모든 것은 데이터에 대한 것이다
------------------------------------------

OOP는 현재 게임개발 문화에 너무나 깊숙히 파고들어 있어서 게임을 만들때 오브젝트를 빼놓는다는 것은 생각하기조차 힘들다. 무엇보다도 우리는 탈것, 플레이어, 상태머신등을 클래스로 만드는 일을 몇년동안 해오지 않았던가? 대안은 있나? 재래의 절차형 프로그래밍? 함수형 언어? 아니면 희안한 신흥 프로그래밍 언어들?

데이터 중심 디자인(Data Oriented Design, 이하 DOD)은 이런 문제들을 해결하기 위한 다른 방식의 접근이다. 절차형 프로그래밍은 프로시저 호출을 문제의 중심에 두고 있고, OOP는 객체를 다루는 것을 기본으로 하고 있다. 두가지 접근의 중심에는 code가 있다는 것이 공통점이다. 전자는 평범한 프로시저나 함수들, 후자는 어떤 내부적인 상태를 둘러싼 코드들의 모임이라는 점이 다른 정도다. DOD는 프로그래밍의 관점을 오브젝트에서 데이터 그 자체로 옮긴다. 즉, 데이터의 타입, 메모리에 어떻게 배치될 것인가, 그리고 게임 내에서 어떻게 읽어서 처리할 것인가를 중심에 놓는 것이다.

프로그래밍은 원래 데이터를 변환시키는 것을 의미했다. 즉, 어떤 입력값을 받아서 일련의 기계 명령을 통해서 어떤 특정한 출력 값을 생성시키는 것을 말한다. 게임이라고 해서 보통의 프로그램과 특별히 다른 점이라면, 인터렉티브한 주기로 작동한다는 점 정도다. 그러니 코드보다 데이터에 관심을 가지는게 그렇게 특별할 일도 없다.

나는 DOD가 딱히 데이터 주도형 디자인 (Data driven Design) 과는 관계가 없다는 점을 밝혀두고자 한다. 데이터 주도형 디자인은 게임의 기능을 최대한 코드 밖으로 빼내서 데이터쪽으로 옮기는 것을 의미하고, 이 개념은 데이터 중심 디자인과 독립된 개념으로서, 어떤 방식의 프로그래밍과도 함께 쓰일 수 있다. 혼동하지 마시길.


이상적인 데이터
----------------------

우리가 프로그램을 데이터의 관점에서 본다면, 이상적인 데이터는 어떤 모습일까? 이것은 데이터의 종류와 그 사용 방식에 달려있다. 일반화해서 말하자면, 최소한의 노력으로 처리될 수 있는 데이터가 이상적이다. 가장 최소한의 노력이 소요되는 경우는 데이터의 입력값이 그대로 출력값으로 나가는 경우라고 생각할 수 있다. 대개의 경우 우리의 이상적인 데이터 형태는 연속적이면서(contiguous), 같은 구조로 짜여진 (homogeneous) 데이터가 큰 블럭으로 짜여져서 순차적으로 (sequential) 처리하게 되어 있는 경우다. 어떤 경우에건 목표는 변환의 양을 줄이는 것이고, 가능하다면 데이터를 빌드과정에서 선처리해서 내놓는 것이 좋다.

DOD에서는 데이터를 가장 최우선에 놓기 때문에 관련 코드들도 그 원칙을 따르게 된다. 항상 이상적인 경우만을 생각할 수는 없을지라도, 데이터가 최우선 목표라는 것은 기억해야 한다. 일단 그렇게 하기만 하면 이 글의 서두에서 말했던 대부분의 문제들은 사라지게 된다. 그 이유는 차차 설명할 것이다.

우리가 오브젝트를 생각하게 되면, 여러가지 형태의 트리들이 떠오른다. 상속 트리, 트리식 포함관계, 메시지 전달 트리 구조등, 그리고 우리의 데이터는 자연스럽게 트리형태로 정렬된다. 그 결과로 우리가 어떤 오브젝트에 대한 연산을 수행하게 되면, 트리상에 엮인 다른 오브젝트를 액세스 하게 된다.  한 묶음의 오브젝트를 처리하다보면 트리구조를 따라 각각의 오브젝트에 대해 서로 다른 연산을 수행하게 된다 (그림 1a 참조)


가장 최선의 데이터 형태를 만들기 위해서는 오브젝트 하나하나를 컴포넌트별로 쪼개는 것이 좋다. 그리고 같은 컴포넌트끼리 그룹을 지어서 메모리 상에 합쳐지게 해놓는 것이다 (그 컴포넌트가 어느 오브젝트에 속해있는지와는 무관하다). 이런식으로 데이터를 같은 종류끼리 (homogeneous) 연속적으로 (contiguous) 배치하게 되면 순차적 대량 처리가 가능해진다. (그림 1b 참조) 


DOD가 강력한 이유는, 오브젝트의 수가 많아질 수록 성능이 좋아지기 때문이다. 반면 OOP에서는 하나의 오브젝트 단위로 처리하는 것이 보통이다. 잠시 생각해보자. 실제의 게임 속에 등장하는 것 중에 단 한개만 등장하는 것이 있던가? 한마리의 적? 한대의 차량? 길찾기 노드 한개? 총알 한발? 파티클 한개? 그런 경우는 거의 없고 뭔가 나오게 되면 항상 복수로 등장한다. OOP는 이러한 특성을 무시하고 개개의 오브젝트를 독립적으로 다룬다. 하지만 DOD에서는 하드웨어의 특성을 살려서 우리의 데이터를 같은 타입끼리 복수개를 묶게 된다.

이런 접근 방식이 이상하다고 생각하는가? 사실 우리는 이런 식으로 처리하는 부분을 이미 만들어 왔다. 바로 파티클 시스템! DOD는 전체의 코드를 거대한 파티클 시스템처럼 다루게 하자는 것이다. 어쩌면 게임 프로그래머들에게는 DOD 대신 particle-driven programming이라는 말이 더 잘 와닿을 지도 모르겠다.


DOD의 장점
----------------

데이터 우선의 사고방식과 프로그램 설계는 우리에게 많은 잇점을 가져다준다.

- 병렬화 (Parallelization) -

오늘날, 멀티 코어는 더 이상 피할 수 없는 대세다. OOP코드를 가져다가 병렬화를 시도해본 사람이라면 어렵고, 실수하기 쉽고, 생각만큼 효율이 오르지 않을 수 있다는 것을 체감했으리라. 여러개의 쓰레드에서 데이터에 접근할 때 생기는 race condition을 막기 위해 동기화를 여기저기 붙이다보면 쓰레드들은 락을 기다리느라 대기하는 시간이 늘어나게 되고 성능은 기대 이하로 떨어지게 된다.
DOD를 적용하게 되면, 병렬화는 훨씬 간단해진다. 우리는 입력 데이터와 그것을 처리할 작은 함수들, 그리고 출력 데이터를 갖게 된다. 이런 작업은 여러개의 쓰레드에 쉽게 나눌 수 있으며 동기화도 별로 필요하지 않다. 이 방식은 자체 메모리를 가진 병렬 프로세서 (PS3의 SPU 같은) 에서 실행시키는 것도 별로 어렵지 않다

- 캐쉬 활용 (Cache Utilization) - 

멀티 코어에 덧붙여, 최신 다단계 캐쉬로 설계된 하드웨어의 성능을 최대한 끌어올리는 핵심은 캐쉬 친화적인 메모리 액세스다. DOD 는 명령 캐쉬의 활용에 매우 효율적인데, 그 이유는 같은 코드를 반복해서 실행하게 되기 때문이다. 또한, 데이터를 커다란 연속적인 블럭에 배치하게 되면, 데이터를 순차적으로 처리할 수 있게 되어, 거의 완벽하게 데이터 캐쉬를 활용해서 높은 성능을 낼 수 있게 된다. 우리가 오브젝트 단위나 함수단위로 생각을 하게 되면 최적화라는 것은 함수단위나 알고리즘 단위에서 막히게 된다. 함수 호출의 순서를 바꾸거나 알고리즘을 바꾸거나, 혹은 c 코드를 어셈블리로 바꾸는 정도가 고작이다. 이런 최적화는 약간의 도움이 되기는 하겠지만, 데이터 중심으로 생각할 때에야 우리는 한 발짝 물러서서 정말 큰 단위의 중요한 최적화를 할 수 있게 된다. 게임이 하는 일은 어떤 데이터 (아트 데이터, 입력 값, 상태) 를 다른 형태로 바꾸는 것 (그래픽 명령, 새로운 게임 상태) 에 불과하다는 것을 기억하라. 데이터의 흐름에 집중하면 우리는 더 상위레벨에서 데이터가 변환되고 사용되는 방식에 대한 현명한 판단을 내릴 수 있게 된다. 이런 종류의 최적화는 전통적인 OOP방법론에서는 매우 어렵거나 시간이 많이 소요되는 일이다.

- 모듈화 (Modularity) - 

지금까지 DOD 의 장점은 주로 캐쉬활용, 최적화, 병렬화 같은 성능에 대한 것이었다. 게임 프로그래머에게는 성능만큼 중요한 요소도 없지만, 성능과 코드의 유지보수성이 서로 상충되는 경우도 있다. 예를 들면, 코드를 어셈블리어로 바꾸어놓으면 성능은 조금 올라갈 수 있겠지만 그로 인해 코드를 읽기 어려워지는 점은 문제로 남게 된다.

다행스러운 점은 DOD는 성능에도 도움이 되지만, 개발의 편이에도 도움이 된다는 점이다. 코드를 데이터 변환이라는 관점에서 작성하게 되면, 결국, 서로 의존성이 많지 않은 작은 함수들로 만들게 되고, 결국 코드들은 '평평하고(flat)' 의존성이 별로 없는 함수들의 모음이 된다. 이런 모듈화는 이해하기 쉽고, 바꾸거나 고치기 쉬운 코드들이 된다.

- 테스트 (Testing) - 

DOD 의 가장 마지막 장점은 테스트 편의성이다. 예전 컬럼에서도 지적했든이 오브젝트의 상호작용에 대한 유닛 테스트를 만드는 것은 쉽지 않은 일이다. 모의 객체 (Mock Object)를 세팅하고 간접적인 방법들을 써야 하는데, 사실 고생스러운 일이 아닐 수 없다. 반면, 데이터를 직접 다루게 되면 유닛테스트를 짜는 것은 너무나 간단해진다. 약간의 입력 데이터를 만들어서 변환함수를 호출하고, 출력값이 우리의 기대값과 일치하는지만 확인하면 된다. TDD를 하는 중이건, 개발이 끝난 후에 테스트를 만드는 것이건 코드가 아주 테스트하기 편해진다.


DOD의 단점
----------------

DOD가 게임 프로그래밍계의 모든 문제점을 해결할 은총탄은 아니다. DOD는 고성능 코드를 만들고 프로그램을 유지보수하기 편하게 만드는 장점은 있지만 단점이 없지는 않다.

가장 큰 문제점은 대개의 프로그래머들이 해왔거나 배운 방식과 다르다는 점이다. 이것은 우리가 프로그램을 짜는 방식을 전환하게끔 요구하며, 익숙해지기 위해서는 상당한 연습이 필요하다.

또한, 다른 접근방법이기 때문에 우리가 기존에 짜 놓은 OOP식으로 짠 코드들과 함게 동작하게 하는 것도 쉬운 일은 아니다. 프로그램을 함수단위로 짜는 것이 어려울 수는 있지만 그래도, 서브시스템 전체에 DOD가 적용되면 상당한 이점을 얻게 될 것이다.


DOD 적용하기
-------------------

이론 설명은 여기까지 하고, DOD를 어떻게 실제로 적용해볼 것인가? 일단 기존 코드의 특정 부분을 골라서 시작해보자. 네비게이션, 애니메이션, 충돌체크,등등.. 그 후에 게임 엔진의 중심에 데이터가 자리잡게 되면 한 프레임의 시작부터 끝까지 데이터가 변환되는 흐름에 대해 생각해야 한다.

다음의 단계는, 각 시스템마다 입력받는 데이터와 무엇을 생성할지를 명확하게 분리하는 것이다. 당분간은 OOP적 용어를 사용해서 데이터를 확인할 수 있다. 예를 들면 애니메이션 시스템에서는 입력값은 skeletons, base poses, animation data, 그리고 현재 상태등을 포함하고 있다. 그 결과물은 '코드가 애니메이션을 플레이한다'가 아니라, 현재 플레이중인 애니메이션에 의해 생성되는 데이터들이어야 한다. 이 경우에는 우리의 출력값은 아마 새로운 세트의 pose 와 갱신된 state일 것이다.

여기서 한발짝 더 나아가 입력되는 데이터가 어떻게 사용될지를 분류하는 것이 중요하다. 이것은 읽기 전용인가? 아니면 읽기/쓰기 겸용인가? 아니면 쓰기 전용인가? 이런 분류는 데이터를 어디에 저장하고 언제 프로그램상의 다른 부분과 맞물려서 처리할지를 결정하는데 중요한 역할을 한다.

이 시점부터는 데이터를 한개단위로 처리하는 대상이라고 생각하지 말고, 수천 수백개 단위로 처리될 대상으로 생각해야 한다. 이후부터는 skeleton 한개, base pose 한개, 현재 상태 한개에 대해서 처리하는 것이 아니라 많은 인스턴스 단위로 대량처리하는 기준으로 생각해야 한다

데이터가 입력되서 처리되고 출력되는 과정을 주의깊게 생각해야 한다. 구조체의 특별한 값을 참조해서 그 나머지 값을 처리할지 말지를 결정하는 부분이 있다는 것을 알게 되었다고 하자. 그런 경우에는 그 특별한 값을 나머지 구조체에서 떼어내서 독립적으로 처리되도록 함으로써, 캐쉬 활용성과 병렬화를 이룰 수 있다. 또는 코드의 일부분을 벡터화하는 것이 필요할 수도 있다. 그렇게 해야 벡터 연산 (SIMD같은)을 데이터의 추가적 변환없이 할 수 있다.

이제 우리가 다루는 데이터에 대해 충분한 이해를 하게 되었다면, 그 데이터를 변환하는 코드를 작성하는 일은 훨씬 쉬워진다. 마치 빈칸에 코드를 채워넣는 것과도 같다. 어쩌면 코드가 이전보다 훨씬 간단해지고 처음 생각보다 짧아진 것에 놀랄지도 모른다.

작년부터 이 컬럼에서 나온 내용을 돌아보면 우리는 꾸준히 이 방향을 지향해왔다는 것을 알 수 있을 것이다. 이제 데이터가 정렬되는 방식이나, 데이터를 더 효율적으로 활용할 수 있는 포맷으로 미리 구워넣거나 데이터가 쉽게 재배치될 수 있도록 포인터를 쓰지 않는 참조방식을 쓰는 것등을 고려해볼 단계다.


OOP는 필요한가?
-----------------------

지금까지의 결론은 OOP 는 불필요하니 프로그래밍에서 절대 쓰지 말아야한다는 것일까? 난 아직 그렇게까지 단언할 수는 없다. 오브젝트가 단 한개씩만 존재하는 경우 (그래픽 디바이스, 로그 매니저 등) 에는 oop가 해롭다고 볼 수는 없다. 물론 그런 클래스들도 그냥 c스타일 함수를 이용해서 만들면 된다.

여전히 oop가 유용한 분야중 하나는 gui 이다. gui 라는 개념 자체가 처음부터 oop에 기반해서 설계된 탓도 있고, gui코드들은 성능에 덜 민감해서 그런 것도 있다. 내 경우는 gui api도 상속을 적게 쓰고, 포함관계를 많이 사용하는 쪽을 선호한다 (Cocoa나 CocoaTouch는 그런 사례에 속한다). 어쩌면 게임용으로 쓰기 좋은 data-oriented GUI시스템이 있으면 좋겠지만, 아직 그런 것을 보진 못했다.

마지막으로, 오브젝트라는 것을 기존의 관점으로 꼭 생각할 필요는 없다. 예를 들어 적군이라는 개체를 표현하는 데이터가 꼭 한 곳의 물리적 메모리 영역에 있지 않아도 된다. 그 대신, 더 작은 세부 컴포넌트로 나뉘어져 있고, 각각의 컴포넌트는 같은 종류끼리 큰 단위로 데이터 테이블에 들어있을 수 있다.

DOD는 기존의 프로그래밍 접근으로부터의 작별을 의미한다, 대신, 데이터를 생각하고, 그 데이터가 어떻게 변환되는가를 생각한다면 성능과 개발 편의에 있어서 큰 도약을 이룩할 수 있을 것이다.


참고할만한 관련 링크들 소개


http://www.gameenginebook.com/gfg2010-final.pdf
http://altdevblogaday.org/author/colin-riley/

http://www.slideshare.net/DICEStudio/introduction-to-data-oriented-design
http://gamesfromwithin.com/data-oriented-design-now-and-in-the-future

http://www.insomniacgames.com/research_dev/articles/2010/1530793
http://bitsquid.blogspot.com/2010/05/practical-examples-in-data-oriented.html
http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf



================================================================================================

 

현재 SI의 경우 대부분의 개발단이 data에 대한 접근을

최대한 DB단의 리소스를 활용하여 프로그램단에서의 추가적 data 가공을 배제함으로인해

서버의 리소스를 줄여 코드의 간편화 및 특정상황(동접자 폭주)에 대한 성능향상 을 꾀하는걸로 아는데요.

 

SI특성상 게임프로그램과는 다르게 간단한 쿼리의 쓰레드를 활용한 여러개의 동시작업보다는

업무의 복잡도와 쿼리의 복잡도가 비례하게 나가는 패턴에서 DOD의 개념이 도입 될 수 있을까요..

 

아무튼 페이지디자인적 프로그램이 아닌 양방향 통신의 모듈개발에서는

DOD의 개념이 도입된다면...... 높은 성능 향상과(DB 인터페이스의 경우)함께

개발자의 머리뽀개짐이 함께할 듯 싶습니다.


핑백

덧글

  • 떠리 2015/08/09 02:20 # 답글

    잘 보았습니다.
  • 앞서나가는 얼음요새 2015/08/10 02:04 # 답글

    잘 읽었습니다.
    마지막에 쓰신 것처럼 저도 읽으면서 이런 생각이 들었습니다. ^^
    "OOP처럼 트리형식으로 데이터를 다루는 것이 아니라
    RDB처럼 테이블 형식으로 데이터를 다루자는 얘기로군"
  • 박PD 2015/08/10 12:36 #

    뒤에 글은 김학규님이 쓰신거라서요, 제가 평가하신 그렇구요...
    보통은 본문에 있는 것처럼 배열에 포인터가 아닌 객체를 연속해서 넣은 뒤에 이를 순회하면서 원하는 작업을 호출하는 식으로 만듭니다. RDB도 너무 개념이 크고, 굳이 다른 개념으로 설명하자면 CSV에 가깝다고 볼 수 있겠네요.
댓글 입력 영역


Yes24위대한게임의탄생3

위대한 게임의 탄생 3
예스24 | 애드온2