게임 프로그래밍 데이터 지향 설계와 ECS의 진화
캐릭터 수를 조금 늘렸을 뿐인데 프레임 시간이 갑자기 치솟고, 기능 하나를 수정할 때마다 상속 구조 전체가 흔들린다면 문제는 개별 코드보다 데이터를 배치하고 처리하는 방식에 있을 가능성이 큽니다. 최근 게임 프로그래밍에서는 객체가 무엇인지 설명하는 설계보다, 같은 데이터를 얼마나 연속적으로 읽고 한꺼번에 처리할 수 있는지가 더 중요한 판단 기준으로 자리 잡고 있습니다.
그 중심에 데이터 지향 설계(Data-Oriented Design)와 ECS(Entity Component System)가 있습니다. 다만 ECS를 유행하는 엔진 기능이나 상속을 대체하는 패턴 정도로 이해하면 기대한 성능을 얻기 어렵습니다. 이 글에서는 최신 엔진 개발 흐름이 왜 데이터 중심으로 이동하는지, 어떤 프로젝트에서 효과가 크며 앞으로 게임 개발자의 역할이 어떻게 달라질지를 실무 관점에서 살펴봅니다.
게임 프로그래밍이 객체보다 데이터 흐름을 보는 이유
CPU 속도보다 메모리 접근이 문제가 되는 장면
전통적인 객체 지향 게임 코드는 플레이어, 적, 투사체가 각자의 상태와 동작을 가진다는 점에서 읽기 쉽습니다. 그러나 수천 개 객체가 힙의 여러 위치에 흩어지고 매 프레임 가상 함수를 호출하면 CPU는 필요한 데이터를 기다리는 데 많은 시간을 씁니다. 계산식 자체가 단순해도 캐시 미스와 분기 실패가 겹치면서 실제 프레임 시간은 예상보다 커질 수 있습니다.
데이터 지향 설계는 질문의 출발점을 바꿉니다. “슬라임 객체가 어떤 메서드를 갖는가?”보다 “이번 프레임에 위치와 속도를 갱신해야 하는 개체가 몇 개이며, 그 배열을 어떤 순서로 읽는가?”를 먼저 묻습니다. 위치와 속도를 연속된 메모리에 배치하면 프로세서는 캐시 라인 하나로 여러 개체의 값을 가져오고, SIMD나 작업 병렬화에도 유리한 형태를 얻습니다.
이 변화는 단순한 미세 최적화가 아닙니다. 오픈월드의 군중, 탄막 슈팅의 투사체, 전략 게임의 유닛, 물리 기반 파편처럼 동일한 연산을 대량 데이터에 반복하는 장면이 늘면서 설계 단계부터 처리량을 고려해야 하는 시대가 됐습니다. 반대로 메뉴 화면처럼 개체 수가 적고 이벤트가 드문 영역까지 데이터 지향 구조로 바꾸는 것은 복잡성만 키울 수 있습니다.
- 객체 중심 질문: 이 적 캐릭터가 어떤 행동과 상태를 소유하는가?
- 데이터 중심 질문: 이동 시스템이 이번 프레임에 읽고 쓰는 배열은 무엇인가?
- 성능 판단 기준: 연산 횟수뿐 아니라 캐시 미스, 메모리 대역폭, 분기 패턴을 함께 확인합니다.
- 적용 우선순위: 개체 수가 많고 같은 갱신이 반복되는 시뮬레이션부터 후보로 삼습니다.
ECS는 데이터 지향 설계를 담는 하나의 그릇
ECS에서 엔티티는 대개 고유 식별자이고, 컴포넌트는 위치·체력·속도처럼 데이터만 보관하며, 시스템은 필요한 컴포넌트 조합을 조회해 로직을 실행합니다. 상속 트리에서 기능을 찾지 않고 조합으로 개체를 표현하므로, 불타면서 이동하는 상자처럼 기존 분류에 잘 들어맞지 않는 대상도 비교적 자연스럽게 만들 수 있습니다.
하지만 모든 ECS가 자동으로 빠른 것은 아닙니다. 컴포넌트를 포인터로 흩어 놓거나, 시스템마다 임의 접근을 반복하거나, 매 프레임 구조 변경을 남발하면 이름만 ECS인 느린 구조가 됩니다. 핵심은 특정 컴포넌트 조합을 가진 엔티티를 묶어 저장하는 아키타입, 데이터 변경 시점을 통제하는 명령 버퍼, 읽기·쓰기 의존성을 드러내는 시스템 스케줄링입니다.
실무 팁: ECS 도입 여부를 클래스 수로 판단하지 마세요. 프로파일러에서 반복적으로 비싼 루프 하나를 찾고, 그 루프가 읽는 데이터만 연속 배열로 옮겨 보는 작은 실험이 훨씬 정확한 출발점입니다.
ECS 생태계에서 뚜렷해진 기술 변화
아키타입 저장소와 병렬 스케줄러의 결합
2026년 시점의 흐름을 보면 ECS는 단순한 엔티티 관리 라이브러리에서 작업 스케줄링과 메모리 레이아웃을 함께 설계하는 실행 모델로 발전하고 있습니다. Unity 계열의 데이터 중심 기술, Rust 생태계의 Bevy ECS, 자체 제작 C++ 엔진의 아키타입 저장소처럼 구현은 달라도 읽기 전용 접근과 쓰기 접근을 명시해 병렬 실행 가능성을 찾는 방향은 비슷합니다.
예를 들어 이동 시스템이 Position을 쓰고 Velocity를 읽으며, 수명 시스템이 Lifetime만 쓴다면 두 작업은 충돌 없이 동시에 실행할 수 있습니다. 반면 충돌 해결과 이동이 모두 Position을 수정한다면 순서를 정하거나 중간 결과 버퍼를 둬야 합니다. 개발자가 숨겨진 전역 상태를 줄이고 데이터 접근 권한을 정확히 표현할수록 스케줄러가 활용할 수 있는 병렬성이 커집니다.
구조적 변경도 중요한 화두입니다. 엔티티에 컴포넌트를 추가하거나 제거하면 다른 아키타입 저장소로 데이터를 옮겨야 하므로 비용이 발생합니다. 전투 중 상태를 매 프레임 붙였다 떼는 대신 활성 플래그를 사용하거나, 변경 요청을 명령 버퍼에 모아 안전한 시점에 일괄 처리하는 식으로 비용과 동기화 지점을 관리해야 합니다.
| 기술 흐름 | 얻는 효과 | 주의할 비용 |
|---|---|---|
| 아키타입 기반 저장 | 동일 컴포넌트 조합의 연속 순회 | 잦은 컴포넌트 추가·삭제 시 데이터 이동 |
| 접근 권한 기반 스케줄링 | 안전한 작업 병렬화 | 숨은 전역 상태가 있으면 의존성 추적 실패 |
| 명령 버퍼 | 구조 변경의 일괄 처리 | 변경 결과가 즉시 보이지 않는 지연 |
| 변경 감지 | 수정된 데이터만 선택적으로 처리 | 추적 메타데이터와 잘못된 갱신 조건 |
GPU와 대규모 시뮬레이션을 잇는 데이터 구조
데이터 중심 구조는 GPU 기반 렌더링과도 가까워지고 있습니다. CPU에서 각 객체의 그리기 명령을 하나씩 제출하는 대신 인스턴스 데이터, 가시성 정보, 머티리얼 식별자를 버퍼에 모으고 GPU가 컬링과 간접 드로우를 수행하는 방식이 확산하고 있습니다. 게임플레이 데이터와 렌더링 데이터가 곧바로 같아지는 것은 아니지만, 둘 사이의 변환 경계를 명확하게 만들기 쉬워집니다.
앞으로는 CPU ECS의 모든 데이터를 GPU로 복사하기보다, 렌더링에 필요한 스냅샷만 압축해 전달하고 GPU 친화적인 별도 표현으로 유지하는 설계가 더 중요해질 가능성이 큽니다. 위치를 64비트 월드 좌표로 보관하더라도 화면 주변 렌더링 청크에는 상대 좌표를 사용할 수 있습니다. 하나의 완벽한 데이터 모델을 강요하기보다 각 처리 장치에 맞는 표현과 동기화 비용을 선택하는 접근입니다.
- CPU 영역: 게임 규칙, 권한 판정, 정밀한 월드 상태를 관리합니다.
- GPU 영역: 인스턴싱, 컬링, 파티클처럼 대량 병렬 처리에 적합한 데이터를 둡니다.
- 변환 경계: 전체 복사 대신 변경된 청크나 가시 범위만 전달합니다.
- 측정 항목: 잡 실행 시간뿐 아니라 버퍼 업로드, 동기화 대기, 구조 변경 횟수를 기록합니다.
이런 기술은 업계 발표를 통해 빠르게 공유됩니다. 게임 개발 기술 교류의 대표 무대를 이해하려면 GDC의 성격과 역할을 함께 살펴볼 만합니다. 발표의 화려한 결과만 복제하기보다 해당 스튜디오의 개체 규모, 플랫폼, 제작 기간이 자신의 프로젝트와 비슷한지를 먼저 확인해야 합니다.
도입 비용을 낮추는 엔진 아키텍처 선택법
전체 전환보다 뜨거운 경로부터 분리하기
ECS를 검토할 때 가장 위험한 결정은 기존 엔진을 통째로 다시 만드는 것입니다. 이미 안정적으로 작동하는 UI, 퀘스트 스크립트, 저장 시스템까지 한 번에 옮기면 성능과 무관한 변환 작업이 늘고 디버깅 기반도 동시에 사라집니다. 먼저 프로파일링으로 프레임 시간의 큰 비중을 차지하는 데이터 처리 경로를 찾고, 독립된 하위 시뮬레이션으로 분리하는 편이 안전합니다.
탄막 게임이라면 플레이어와 보스의 고수준 로직은 기존 객체로 유지하고, 수만 개 투사체의 위치·속도·수명만 구조체 배열이나 ECS 월드로 처리할 수 있습니다. 도시 게임이라면 모든 건물을 엔티티로 바꾸기 전에 교통량 계산이나 시민 욕구 갱신처럼 대량 반복되는 부분을 선택할 수 있습니다. 이 하이브리드 방식은 경계 변환 비용이 있지만, 성능 개선 폭과 개발 난도를 실제 수치로 비교하게 해줍니다.
도입 예산은 라이선스 가격만으로 계산하면 부족합니다. 새로운 디버거, 인스펙터, 저장 포맷, 테스트 방식, 교육 시간이 필요하고 팀원이 데이터 수명 주기와 지연된 구조 변경을 익히는 동안 생산성이 떨어질 수 있습니다. 기능별 편익과 비용을 묶어 판단한다는 점에서는 계획예산 제도의 개념처럼 목표와 자원을 연결하는 사고가 도움이 됩니다.
- 기준 장면을 고정합니다. 개체 수, 카메라 경로, 입력, 난수 시드를 동일하게 만들어 전후 결과를 비교합니다.
- 현재 병목을 기록합니다. 평균 FPS보다 프레임 시간 중앙값, 상위 백분위, 캐시 미스, 할당량을 살핍니다.
- 작은 데이터 루프를 옮깁니다. 이동이나 수명 갱신처럼 입출력이 명확한 시스템부터 구현합니다.
- 도구 비용을 포함합니다. 디버깅 시간과 신규 기능 제작 시간도 성능 수치와 함께 기록합니다.
- 중단 조건을 정합니다. 개선 폭이 목표보다 작거나 제작 복잡성이 지나치면 기존 구조로 되돌릴 수 있어야 합니다.
기획 변화와 디버깅 가능성을 아키텍처에 포함하기
데이터 지향 설계의 숨은 난점은 플레이 도중 상태를 추적하기 어렵다는 것입니다. 객체 방식에서는 특정 인스턴스를 열어 필드를 살펴보면 되지만, ECS에서는 엔티티가 여러 청크 사이를 이동하고 시스템이 병렬로 실행될 수 있습니다. 따라서 엔티티 검색, 컴포넌트 변경 이력, 시스템 실행 시간, 구조 변경 발생 지점을 보여주는 도구가 엔진 기능만큼 중요합니다.
기획자가 “독 상태인 적은 물에 들어가면 정화되고 이동 속도가 회복된다”는 규칙을 추가한다고 가정해 봅시다. Poisoned, InWater, MovementSpeed 사이의 데이터 흐름이 명확하면 작은 시스템으로 조합할 수 있지만, 실행 순서가 보이지 않으면 한 프레임 동안 속도가 튀는 오류가 생깁니다. 게임 규칙을 만드는 직무와 프로그래밍 구조의 접점을 이해할 때는 게임 기획자의 역할도 참고할 수 있습니다.
도구 우선 원칙: 새 시스템을 추가하기 전에 “이 데이터가 언제, 어떤 시스템에 의해 바뀌었는가?”를 몇 분 안에 답할 수 있는지 확인하세요. 답할 수 없다면 기능 확장보다 관찰 도구가 먼저입니다.
- 엔티티 식별자는 재사용 여부를 구분할 수 있도록 세대 번호를 함께 둡니다.
- 시스템별 읽기·쓰기 컴포넌트를 표시해 순서 의존성을 눈에 보이게 만듭니다.
- 구조 변경 횟수와 이동한 데이터 양을 프레임 단위로 집계합니다.
- 재현 가능한 버그를 위해 입력, 난수 시드, 주요 명령 버퍼를 기록합니다.
- 세이브 데이터에는 런타임 메모리 배치를 그대로 노출하지 않고 별도의 안정적인 스키마를 둡니다.
붐비는 항구 도시의 프레임 시간을 되찾는 과정
시민 시뮬레이션을 데이터 단위로 다시 나누기
실제 적용 장면을 가정해 보겠습니다. 작은 항구 도시를 운영하는 게임에서 시민 800명까지는 부드럽지만 5,000명이 되면 낮 시간대에 프레임 시간이 14ms에서 38ms로 튑니다. 각 Citizen 객체는 이동, 배고픔, 직업, 애니메이션, 목적지 탐색 기능을 모두 가지고 있으며 Update에서 개별 판단을 수행합니다. 프로파일러를 확인하니 복잡한 길찾기보다 모든 시민이 매 프레임 욕구와 목적지 상태를 확인하는 루프가 더 큰 비중을 차지합니다.
개발팀은 전체 게임을 ECS로 바꾸지 않고 시민의 Position, Velocity, Hunger, WorkSchedule, Destination만 별도 시뮬레이션 월드로 옮깁니다. 건물 UI와 퀘스트, 플레이어가 선택한 시민의 상세 정보는 기존 객체 구조를 유지합니다. 첫 구현에서는 모든 시스템을 매 프레임 실행했기 때문에 메모리 배치는 좋아졌지만 기대만큼 빨라지지 않았습니다. 여기서 갱신 빈도를 데이터 성격에 맞게 나누는 두 번째 변화가 들어갑니다.
이동은 매 시뮬레이션 틱 처리하고, 배고픔은 시민을 여러 그룹으로 분할해 몇 틱에 걸쳐 갱신합니다. 근무 일정은 게임 시간의 분이 바뀔 때만 검사하며, 목적지 탐색 요청은 큐에 넣어 프레임당 처리량을 제한합니다. 화면에 보이지 않는 시민은 세밀한 애니메이션 상태 대신 구역 식별자와 도착 예정 시간만 유지합니다. 플레이어가 항구를 확대하면 해당 구역 시민만 상세 표현으로 승격됩니다.
- 첫 측정: 시민별 Update 호출과 흩어진 객체 접근이 주 병목임을 확인합니다.
- 첫 전환: 반복 처리되는 다섯 컴포넌트만 연속 저장소로 이동합니다.
- 빈도 분리: 이동, 욕구, 일정, 경로 요청을 서로 다른 주기로 갱신합니다.
- 거리별 표현: 보이지 않는 시민은 집계형 데이터로 낮추고 가까운 시민만 상세화합니다.
- 안전장치: 선택된 시민의 상태는 기존 UI 객체에 읽기 전용 스냅샷으로 전달합니다.
측정 결과를 다음 기능의 설계 규칙으로 바꾸기
변경 후 같은 카메라 경로와 난수 시드로 다시 측정하자 혼잡 시간대 프레임 시간은 38ms에서 19ms로 내려갑니다. 단순히 ECS를 사용해서 얻은 결과는 아닙니다. 연속 메모리 순회, 불필요한 갱신 제거, 작업량 제한, 거리별 시뮬레이션 수준 조절이 함께 작동한 결과입니다. 반면 선택한 시민이 직장을 바꾼 직후 UI에 한 틱 늦게 표시되는 문제가 발견되며, 팀은 사용자 조작과 연관된 이벤트만 즉시 스냅샷에 반영하도록 경계를 수정합니다.
다음 업데이트에서 날씨에 따라 노동 효율이 변하는 기능이 추가됩니다. 예전 구조라면 모든 Citizen 객체에 날씨 확인 코드를 넣었겠지만, 이제는 WeatherZone과 Worker가 함께 있는 청크만 처리하는 시스템을 만듭니다. 비가 시작될 때 관련 구역에 변경 표시를 남기고, 표시된 구역의 노동자만 다시 계산합니다. 기능 요구가 데이터 조회와 실행 빈도로 번역되면서 성능 비용도 구현 전에 대략 예측할 수 있게 됩니다.
마지막으로 개발팀은 자동 성능 테스트에 시민 수 5,000명, 선박 120척, 비 오는 출근 시간이라는 장면을 고정합니다. 빌드마다 프레임 시간 상위 백분위, 구조 변경 횟수, 경로 요청 대기열 길이를 저장하고 임계치를 넘으면 변경 내역을 확인합니다. 어느 날 시장 축제 기능이 추가된 뒤 경로 요청 큐가 급증하자, 수치가 곧바로 원인을 가리킵니다. 팀은 축제 참가자를 한 프레임에 모두 재탐색시키지 않고 구역별로 분산 등록하며, 항구 광장에 시민이 모이는 연출과 안정적인 프레임 시간을 함께 지켜냅니다.
- 축제 시작 이벤트는 참가 대상 시민에게 즉시 경로를 계산하지 않고 요청 상태만 부여합니다.
- 요청 수집 시스템은 시민을 구역과 우선순위별로 묶어 제한된 수만 큐에 넣습니다.
- 경로 결과가 준비된 시민부터 Destination을 갱신하고 이동 청크에 합류시킵니다.
- 화면 가까이 있는 시민은 즉시 반응시키고 먼 구역은 몇 틱에 걸쳐 전환합니다.
- 테스트 장면에서 대기열과 프레임 시간 임계치를 통과하면 축제 기능을 배포 후보에 포함합니다.

- 다음글프레임 드롭을 잡아야 하는 게임 개발자라면 성능 예산부터 26.08.21
등록된 댓글이 없습니다.
