게임 아키텍처 관점에서 보는 ECS 전환기
ECS는 모든 게임에 적용할 만능 해법이 아니라 데이터 접근 방식과 변경 비용을 다시 설계하는 선택이다. 객체 중심 구조에서 ECS로 옮길 때 판단 기준과 점진적 전환 방법을 정리한다.

ECS 전환은 문법 교체가 아니다
ECS(Entity Component System)는 게임 오브젝트 상속 구조를 컴포넌트와 시스템으로 바꾸는 기법으로 알려져 있다. 하지만 실제 전환의 핵심은 클래스 이름을 바꾸는 일이 아니다. 게임 데이터를 어디에 두고 어떤 단위로 순회하며 기능 추가가 어느 계층의 변경을 일으키는지를 재정의하는 일에 가깝다.
전통적인 객체 중심 구조에서는 플레이어, 적, 투사체가 각자 상태와 동작을 함께 가진다. 초기에는 직관적이지만 화면 안에 비슷한 규칙을 따르는 객체가 많아지고 시스템 간 결합이 늘면 구조가 빠르게 복잡해질 수 있다. ECS는 이를 엔티티, 컴포넌트, 시스템으로 분리한다.
- 엔티티는 정체성을 나타내는 식별자다.
- 컴포넌트는 위치, 체력, 속도처럼 데이터를 담는다.
- 시스템은 필요한 컴포넌트 집합을 순회하며 규칙을 실행한다.
flowchart LR
E[엔티티 ID] --> P[Position]
E --> V[Velocity]
E --> H[Health]
P --> M[Movement System]
V --> M
H --> D[Damage System]
이 분리는 특히 이동, 충돌, 렌더링처럼 많은 객체에 반복 적용되는 규칙을 다룰 때 의미가 크다. 한 시스템은 자신이 필요한 데이터만 읽고 쓰므로 동작의 범위를 파악하기 쉬워진다.

ECS가 잘 맞는 신호
ECS 도입을 고려할 만한 상황은 보통 다음과 같다.
같은 규칙을 따르는 객체가 많다
수백 개의 적, 탄막, 군중, 아이템처럼 비슷한 갱신 규칙을 가진 객체가 많다면 시스템 단위 순회가 유리할 수 있다. 객체마다 가상 메서드를 호출하는 방식보다 필요한 컴포넌트를 연속적으로 처리하는 구조가 데이터 지역성 측면에서 이점을 얻기 쉽다.
조합 가능한 상태가 계속 늘어난다
FlyingEnemy, PoisonedFlyingEnemy, BossFlyingEnemy처럼 타입 조합이 늘어나기 시작했다면 상속보다 컴포넌트 조합이 자연스럽다. 비행 여부, 독 상태, 보스 행동을 독립 데이터로 두면 새로운 조합을 타입 추가 없이 표현할 수 있다.
기능 간 책임이 서로 얽혀 있다
예를 들어 적 객체가 이동, 피격, 애니메이션, 사운드, 드롭 처리까지 직접 호출한다면 작은 변경도 여러 객체에 파급된다. ECS에서는 이동 시스템, 피해 시스템, 애니메이션 시스템처럼 규칙을 분리해 변경 지점을 좁힐 수 있다.
다만 객체 수가 적고 상호작용이 매우 개별적이라면 ECS의 관리 비용이 더 클 수 있다. 대화 중심 어드벤처, 소수 유닛의 전술 게임, 툴 제작처럼 복잡한 데이터 순회보다 명확한 객체 관계가 중요한 영역에서는 단순한 컴포넌트 기반 객체 구조가 더 적합할 수 있다.
성능은 결과이지 출발점이 아니다
ECS를 선택하는 이유로 성능이 자주 언급된다. 실제로 컴포넌트를 연속 메모리에 저장하고 동일한 연산을 묶어 처리하면 캐시 효율과 병렬 처리 가능성을 높일 수 있다. 그러나 이는 구현 방식에 따라 달라진다.
엔티티마다 해시 맵으로 컴포넌트를 찾고 시스템마다 임시 목록을 만들고 프레임마다 컴포넌트를 추가·제거한다면 기대한 이점이 줄어들 수 있다. 먼저 프로파일러로 병목을 확인하고 그 병목이 대량 객체의 반복 처리인지 확인하는 편이 안전하다.
간단한 이동 처리는 다음처럼 시스템이 데이터 묶음을 대상으로 실행하는 형태가 된다.
type Position = { x: number; y: number };
type Velocity = { x: number; y: number };
function updateMovement(
positions: Map<number, Position>,
velocities: Map<number, Velocity>,
deltaTime: number,
) {
for (const [entity, velocity] of velocities) {
const position = positions.get(entity);
if (!position) continue;
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
}
}
이 예제는 개념 설명을 위한 최소 형태다. 대규모 처리에서는 Map 대신 컴포넌트별 밀집 배열이나 아키타입 기반 저장소를 검토할 수 있다. 반대로 작은 프로젝트에서 처음부터 복잡한 저장소를 만들 필요는 없다.
전환기의 가장 현실적인 전략: 경계부터 만든다
기존 프로젝트를 한 번에 ECS로 바꾸는 작업은 위험하다. 게임 규칙을 옮기는 동안 버그가 생기고 기존 씬과 도구가 함께 흔들릴 수 있기 때문이다. 전환 대상은 가장 반복적이고 독립적인 도메인부터 고르는 것이 좋다.
1. 데이터와 부수 효과를 분리한다
우선 기존 객체에서 위치, 체력, 상태처럼 비교적 순수한 데이터를 분리한다. 사운드 재생, UI 갱신, 씬 전환처럼 외부에 영향을 주는 작업은 이벤트나 명시적인 서비스 경계에 남긴다.
type DamageEvent = {
target: number;
amount: number;
};
function applyDamage(
events: DamageEvent[],
healths: Map<number, { current: number }>,
) {
for (const event of events) {
const health = healths.get(event.target);
if (!health) continue;
health.current -= event.amount;
}
}
피해 시스템은 체력만 갱신하고 사망 연출이나 보상 지급은 이후 시스템이 처리하도록 두면 순서와 책임을 테스트하기 쉬워진다.
2. 한 종류의 대량 객체로 검증한다
투사체, 파편, 단순 몬스터처럼 규칙이 명확한 대상을 작은 ECS 영역으로 옮긴다. 프레임 시간, 디버깅 난이도, 콘텐츠 제작 흐름을 비교해 실제 이득을 확인한다. 성능만 보지 말고 새 행동을 추가하는 데 필요한 수정 범위도 함께 기록하는 편이 좋다.
3. 객체와 ECS의 연결 계층을 둔다
렌더러나 엔진의 씬 오브젝트를 곧바로 엔티티로 동일시하면 생명주기 관리가 어려워질 수 있다. 게임 로직의 엔티티 ID와 엔진 오브젝트 사이에 매핑 계층을 두고 생성·파괴·표시 동기화를 한곳에서 관리한다.

시스템 순서와 소유권을 먼저 정하자
ECS에서는 데이터가 열려 있는 만큼 여러 시스템이 같은 컴포넌트를 수정하기 쉽다. 따라서 시스템 실행 순서와 쓰기 권한을 설계하지 않으면 구조가 다시 얽힌다.
예를 들어 입력이 속도 목표를 만들고 이동이 위치를 갱신하며 충돌이 위치를 보정한다면 이 순서는 명시돼야 한다. 더 나아가 Position을 누가 최종적으로 쓸 수 있는지 정해 두는 것이 좋다.
const simulationStages = [
"input",
"ai",
"movement",
"collision",
"damage",
"cleanup",
] as const;
명시적인 단계는 멀티스레드 실행을 고려할 때도 도움이 된다. 읽기만 하는 시스템은 함께 실행할 여지가 있지만 같은 컴포넌트에 쓰는 시스템은 순서나 동기화가 필요하다. 병렬화는 구조가 안정된 뒤에 검토해도 늦지 않다.
테스트와 디버깅 방식도 달라진다
ECS 시스템은 입력 컴포넌트와 출력 컴포넌트를 분리하기 쉬워 단위 테스트에 잘 맞는다. 특정 월드 상태를 만들고 시스템을 한 번 실행한 뒤 결과를 검증하면 된다.
it("속도에 따라 위치를 갱신한다", () => {
const positions = new Map([[1, { x: 0, y: 0 }]]);
const velocities = new Map([[1, { x: 10, y: -5 }]]);
updateMovement(positions, velocities, 0.5);
expect(positions.get(1)).toEqual({ x: 5, y: -2.5 });
});
대신 런타임 디버깅에서는 엔티티의 컴포넌트 조합과 시스템 실행 순서를 보여 주는 도구가 중요해진다. 최소한 엔티티 ID, 보유 컴포넌트, 최근 이벤트, 시스템별 처리 수를 확인할 수 있게 만들어 두면 전환기 문제를 크게 줄일 수 있다.
ECS는 아키텍처의 목적이 아니다
좋은 ECS는 컴포넌트 수가 많은 구조가 아니라 게임 규칙과 데이터 흐름이 팀에 이해되는 구조다. 객체 중심 설계를 완전히 버릴 필요도 없다. UI, 에디터 도구, 퀘스트 대화처럼 수명주기와 관계가 뚜렷한 영역은 기존 객체 구조가 더 읽기 쉬울 수 있다.
핵심 시뮬레이션이나 대량 객체 처리에는 ECS를 적용하고 그 밖의 영역에는 익숙한 구조를 남기는 혼합형 설계가 실무적으로 자주 유효하다. 전환의 성공 기준은 ECS 사용 여부가 아니라 새 기능을 추가할 때 변경 범위가 예측 가능해지고 성능 병목을 측정 가능한 단위로 다룰 수 있게 되었는지에 있다.



