ECS(Entity Component System) 패러다임으로의 사고 전환기

객체 중심 설계에 익숙한 개발자가 ECS로 문제를 다시 나누는 과정과 작은 게임 기능부터 안전하게 적용하는 방법을 정리한다.

✓ 복사 완료!
챕터시스템 아키텍처 & 패턴
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
ECS(Entity Component System) 패러다임으로의 사고 전환기

ECS는 상속 구조를 바꾸는 기술이 아니다

ECS(Entity Component System)는 게임 속 대상을 ‘무엇인가’로 정의하기보다 ‘어떤 데이터가 있고 어떤 규칙이 그 데이터를 처리하는가’로 바라보는 방식이다. 그래서 ECS를 도입할 때 가장 큰 변화는 API나 프레임워크가 아니라 문제를 분해하는 기준에서 일어난다.

객체 지향 방식에서는 Player, Enemy, Projectile처럼 명사 중심의 클래스로 시작하기 쉽다. 기능이 늘면 공통 부모 클래스, 인터페이스, 조건문이 차례로 늘어난다. 반면 ECS에서는 이동 가능 여부, 체력 보유 여부, 추적 대상 보유 여부처럼 독립적인 특성을 데이터로 쪼갠다. 그리고 시스템이 필요한 데이터 조합만 선택해 처리한다.

flowchart LR
  E[Entity: 식별자] --> C1[Position]
  E --> C2[Velocity]
  E --> C3[Health]
  E --> C4[Target]
  S1[Movement System] --> C1
  S1 --> C2
  S2[Damage System] --> C3
  S3[Targeting System] --> C1
  S3 --> C4

여기서 엔티티는 보통 고유 식별자일 뿐이다. 실제 게임 규칙은 컴포넌트의 데이터와 이를 읽고 쓰는 시스템에 있다. 이 구분이 명확해지면 ‘적이 플레이어와 충돌했다’는 사건도 특정 객체의 메서드 호출이 아니라 충돌 결과를 기록한 데이터와 피해 처리 규칙의 조합으로 표현할 수 있다.

위치와 속도 컴포넌트를 가진 여러 엔티티를 이동 시스템이 일괄 처리하는 ECS 개념 이미지

명사 중심 설계에서 질문을 바꾸기

ECS로 전환할 때는 클래스 이름부터 정하려는 습관을 잠시 멈추는 편이 좋다. 대신 다음 질문을 순서대로 던져 보자.

  • 이 기능이 읽어야 하는 상태는 무엇인가?
  • 이 기능이 변경하는 상태는 무엇인가?
  • 같은 규칙을 적용받는 대상은 어떤 데이터 조합을 공유하는가?
  • 프레임마다 처리해야 하는가, 이벤트가 있을 때만 처리해야 하는가?

예를 들어 추적형 미사일은 HomingMissile 클래스로 만들 수 있다. 하지만 ECS 관점에서는 위치, 속도, 추적 대상, 회전 속도라는 데이터가 먼저 보인다. 이동 시스템은 위치와 속도를 가진 모든 엔티티를 처리하고 유도 시스템은 추적 대상과 회전 속도가 있는 엔티티의 속도 방향만 갱신한다.

public struct Position
{
    public Vector2 Value;
}

public struct Velocity
{
    public Vector2 Value;
}

public struct HomingTarget
{
    public Entity Target;
    public float TurnSpeed;
}

이렇게 나누면 일반 탄환은 Position과 Velocity만 갖고 유도탄만 HomingTarget을 추가로 가진다. 두 종류를 위한 거대한 상속 계층이나 if (isHoming) 분기가 꼭 필요하지 않다.

시스템은 기능보다 데이터 흐름으로 나눈다

처음 ECS를 적용할 때 시스템을 화면이나 메뉴 구조에 맞춰 나누면 다시 객체 중심의 결합이 생기기 쉽다. 더 안정적인 기준은 데이터의 생성과 소비 순서다.

예를 들어 입력값이 이동에 영향을 주는 흐름은 다음처럼 구성할 수 있다.

  1. 입력 시스템이 MoveIntent를 기록한다.
  2. 이동 시스템이 MoveIntent와 이동 능력치를 읽어 Velocity를 계산한다.
  3. 물리 또는 이동 시스템이 Velocity로 Position을 갱신한다.
  4. 렌더링 시스템이 최신 Position을 화면에 반영한다.
public void Update(float deltaTime)
{
    foreach (var entity in Query<Position, Velocity>())
    {
        ref var position = ref GetComponent<Position>(entity);
        ref var velocity = ref GetComponent<Velocity>(entity);

        position.Value += velocity.Value * deltaTime;
    }
}

이 예제의 핵심은 반복문 문법이 아니다. 이동 시스템은 플레이어인지 적인지 알 필요가 없다는 점이다. 위치와 속도를 가진 엔티티라면 같은 규칙을 적용할 수 있다. 규칙의 재사용 범위가 클래스 계층이 아니라 데이터 조합으로 결정된다.

이벤트도 상태로 다룰 수 있다

객체 지향 코드에서는 충돌 콜백에서 곧바로 피해 처리, 사운드 재생, 이펙트 생성, 퀘스트 갱신을 호출하기 쉽다. 빠르게 만들 수 있지만 호출 순서와 의존성이 얽히면 수정 비용이 커진다.

ECS에서는 DamageEvent 같은 짧은 수명의 컴포넌트나 이벤트 버퍼를 두고 피해 시스템과 연출 시스템이 각자 필요한 데이터를 소비하게 만들 수 있다. 다만 모든 것을 이벤트 컴포넌트로 바꿀 필요는 없다. 입력 콜백, UI 버튼, 장면 전환처럼 즉시 처리하는 편이 더 단순한 흐름도 많다.

중요한 기준은 ‘이 정보가 여러 규칙에서 독립적으로 소비되는가’다. 그렇다면 이벤트를 데이터로 분리하는 편이 유리하다. 한 곳에서만 즉시 처리하고 끝나는 동작이라면 직접 호출이 더 읽기 쉬울 수 있다.

작게 도입하고 관찰할 것

ECS는 모든 게임에 같은 방식으로 적용해야 하는 규칙이 아니다. 엔티티 수가 많고 비슷한 규칙을 반복 적용하며 조합 가능한 특성이 계속 늘어나는 영역에서 특히 효과가 크다. 투사체, 군중 AI, 상태 효과, 전투 시뮬레이션은 좋은 출발점이 될 수 있다.

반대로 메뉴 UI, 컷신 순서 제어, 에디터 도구처럼 개별 객체의 흐름이 뚜렷한 영역은 기존 방식이 더 이해하기 쉬울 수 있다. ECS를 ‘전체 프로젝트를 갈아엎는 아키텍처’로 보지 말고 데이터 중심 처리가 필요한 경계를 선명하게 만드는 도구로 보는 편이 현실적이다.

처음에는 기존 객체 구조를 유지한 채 투사체 이동이나 체력 처리 하나만 ECS 방식으로 옮겨 보자. 컴포넌트가 지나치게 잘게 쪼개지는지 시스템의 실행 순서가 불명확한지 디버깅할 때 필요한 상태를 쉽게 확인할 수 있는지를 점검하면 된다. 이 작은 실험이 쌓이면 ECS는 낯선 패턴이 아니라 게임 규칙을 다루는 또 하나의 자연스러운 언어가 된다.