이 글은 놀개영 http://www.gamedevforever.com 에서 연재 중인 글입니다.
객체 지향 방식에서의 데이터 처리
위의 Player 클래스는 그 하나만 놓고 보자면, 클래스 1개로 보여지지만, 상위 클래스로부터 파생된 만큼 상위 클래스의 데이터를 모두 가지고 있습니다. 상위 클래스들을 모두 합쳐 놓은 모습이라고 볼수 있습니다. Player 클래스가 보유 하고 있는 데이터들을 간략하게 표현 한다면 밑의 그림과 같을 것 입니다.
이 Player 클래스는 매 프레임 마다 자신의 상태 갱신을 수행하게 됩니다. 상태 갱신은 주로 자신의 클래스로 부터 상속 받은 상위 클래스를 따라 순차적으로 진행 됩니다. 먼저 Player 상태가 업데이트 되고, 이동과 현재 위치 계산을 위한 MovableObject 클래스, 화면에 오브젝트를 그리기 위한 RenderableObject 클래스, 마지막으로 GameObject 클래스 순으로 업데이트 됩니다.
위의 업데이트 과정에서 Player 클래스의 데이터들이 처리 되는 방식을 살펴 보겠습니다. 이와 같은 객체 지향 프로그램에서는 주로 객체 단위로 데이터를 처리합니다. Player 클래스가 여러개 있다면 각 Player 클래스 마다 위의 그림 처럼 데이터를 가지고 있을 것이고, 이 데이터들이 Update() 흐름도 처럼 순차적으로 처리가 됩니다. Player 캐릭터의 상태 데이터, 현재 위치 데이터, 출력을 위한 애니메이션 데이터 식으로 말입니다.
구성 요소 기반 객체 방식에서의 데이터 처리
구성 요소 기반 객체에서는 데이터를 컴포넌트가 관리합니다. 객체 지향 방식에서와 다르게 구성 요소 기반 객체 방식에서는 Player 클래스가 따로 존재 하지 않으며, 기본 객체 클래스에 HealthComponent, MoveComponent, RenderComponent 등의 컴포넌트를 조합하여 위의 Player 클래스와 같은 형태의 객체가 생성되는 방식입니다. 데이터들은 각 컴포넌트에 캡슐화 되어있고, 이 데이터들은 객체가 아닌 컴포넌트에 의해서 처리가 됩니다.
각 컴포넌트들은 카테고리로 분류 되어있습니다. 위치나 애니메이션이 갱신 되기전에 먼저 그려지면 안되는 것 처럼 처리 순서를 지켜주기 위해 분류를 해두고 있는 겁니다. 이 분류는 컴포넌트 매니저 같은 관리자 클래스에 의해 관리됩니다. 각 관리자 클래스는 자신이 관리하는 종류에 맞는 컴포넌트들을 컨테이너에 가지고 있으며 이를 이용해 컴포넌트들을 갱신합니다.
컴포넌트 처리 순서를 지키기 위해 Logic Component Manager에 의해서 먼저 Health Component 들이 갱신 되고, 그다음 Move Component Manager를 통해 통해 Move Component들이 갱신 되는 식으로 동작을 하게 됩니다. 여기서 객체 지향 방식과 큰 차이가 납니다.
일괄된 데이터 처리의 강점
다시 위의 객체 지향 방식의 데이터 처리 방식을 상기해주시기 바랍니다. 객체가 갱신 되면서 내부 데이터들을 처리 하게 되는데, 보시면 각기 서로 다른 데이터가 같이 있습니다. 그에 반해 구성 요소 기반 방식의 데이터들은 관리자 클래스에 같은 종류의 데이터들이 연속된 형태로 묶여 있는 것을 확인 할수 있습니다. 여기서 구성 요소 기반 객체의 병렬 처리 강점이 다시 한번 드러나게 됩니다.
CPU는 데이터를 읽을 때 데이터를 캐쉬에 올려놓습니다. 만약 캐쉬에 데이터가 없으면 캐쉬 미스가 일어나 메모리, 디스크에서 데이터를 읽어들이게 됩니다. 그런데 메모리나 디스크를 통해 데이터를 읽어오게 캐쉬에서 데이터를 읽어 오는 것보다 몇십배 되는 시간이 소요 됩니다. 캐쉬 미스가 자주 일어나게 되면 퍼포먼스가 그만큼 떨어지게 되는 것 입니다.
데이터가 캐쉬에 올라올때는 보통 해당 데이터의 주변 데이터를 같이 불러오는 경우가 많습니다. 여러 경우가 있겠지만 보통 근처에 있는 데이터가 사용될 확률이 높기 때문입니다. 이 때문에 같이 처리될 데이터를 배열 같은 방식으로 한데 묶어 사용하게 되면 그만큼 캐시 미스가 적게 일어나게 됩니다.
다음 이미지는 다이스(Dice)에서 DOD (Data Oriented Design) 라는 데이터 중심 디자인에 관한 발표 자료 중 일 부분입니다. 봇(Bot)이라는 클래스를 예를 들어 봇이 타겟을 조준 하는 과정이 4번 일어난다고 했을 때 캐쉬 미스로 인해 지연율(Latency)이 얼마나 생기는지를 나타내고 있습니다.
함수가 호출되면서 명령어 캐싱이 일어나고, 그 다음 함수 내에서 사용되는 값들이 캐싱 됩니다. 함수 호출이 총 4번이 일어나므로 캐싱도 4번 반복을 합니다. 총 7280 사이클이 소요되었습니다. 그에 반해 동일한 데이터를 한데 묶어 일괄적으로 데이터를 처리하게 되면 어느정도 성능이 향상되는지 살펴보겠습니다.
바뀐 부분을 보시면 기존에 함수를 4번 호출해서 처리하던 데이터를 배열로 만들어 동일한 데이터를 묶어 일괄적으로 처리 할수 있도록 변경한 것을 확인 할수 있습니다. 덕분에 캐시 미스가 줄어들고, 단지 1980 사이클 소요만으로 데이터 처리가 끝났습니다. 장점은 이것만 아닙니다. 위 코드를 잘 보면 이 코드는 루프에 대해 병렬적인 것을 확인 할수 있습니다. 기존의 코드에 비해 병렬성이 증가한 것입니다.
이처럼 동일한 데이터를 한데 묶어 처리하게 되면 많은 장점이 생깁니다. 객체 지향 방식에서 이동 처리, 애니메이션 처리 등을 한데 묶어 처리하게 되면 데이터 의존성 때문에 병렬화가 힘들지만, 구성 요소 기반 방식에서 같은 데이터만을 묶어 처리 하게 되면 의존성도 줄어들어 병렬 처리가 수월해짐과 동시에 성능도 올릴 수 있게 됩니다.
참조
게임 오브젝트 설계.. 나도 잘하고 싶다! by 끼로
프로그래머가 몰랐던 멀티 코어 CPU 이야기 김민장님저
Introduction to Data Oriented Design by Dice 발표 자료
Multiprocessor Game Loops Uncharted2 by Naughty Dog 발표 자료