왜 게임 엔진은 Component 기반 구조를 사용할까?

게임을 개발하다가 Player, Monster, NPC 클래스를 설계해야 하는 상황이 발생했다고 가정해봅시다.

아래와 같은 형태로 클래스를 설계할 수 있습니다.

class Player
{
};

class Monster
{
};

class NPC
{
};

그리고 프로젝트가 점점 커지기 시작하면 자연스럽게 상속 구조를 사용하게 됩니다.

이동 기능이 필요한 객체들은 Character 클래스를 상속받고,
비행 기능이 필요한 객체들은 FlyingCharacter 같은 클래스를 만들기 시작합니다.

class Character
{
};

class Player : public Character
{
};

class Monster : public Character
{
};

굉장히 자연스럽게 보입니다.

객체 지향 프로그래밍을 공부할 때도 상속은 굉장히 중요한 개념으로 등장합니다.

공통 기능을 부모 클래스에 모아두고, 필요한 기능을 자식 클래스에서 확장하는 방식은 직관적입니다.

그런데 게임 규모가 점점 커지기 시작하면 문제가 생기기 시작합니다.


게임은 생각보다 조합이 많은 프로그램이다

게임에는 굉장히 다양한 종류의 객체들이 등장합니다.

예를 들어 아래와 같은 특징들을 생각해볼 수 있습니다.

  • 움직일 수 있는 객체
  • 체력을 가진 객체
  • 총을 발사하는 객체
  • 아이템을 줍는 객체
  • 비행 가능한 객체
  • 네트워크 동기화가 필요한 객체

처음에는 단순하게 상속으로 해결할 수 있을 것처럼 보입니다.

하지만 문제는 이런 기능들이 서로 계속 섞이기 시작한다는 점입니다.

  • 날아다니는 몬스터
  • 총을 쏘는 NPC
  • 체력을 가진 문(Door)
  • 아이템을 줍는 로봇

이와 같이 여러 특징의 조합이 필요한 객체들이 등장하기 시작합니다.

그 순간부터 클래스 구조가 급격하게 복잡해집니다.


상속 구조는 점점 비대해지기 시작한다

아래와 같은 구조를 생각해볼 수 있습니다.

class Character
{
};

class FlyingCharacter : public Character
{
};

class ShootingCharacter : public Character
{
};

class FlyingShootingCharacter : public Character
{
};

처음에는 괜찮아 보일 수 있습니다.

그런데 기능이 조금만 늘어나도 상황이 급격하게 복잡해집니다.

  • 비행 가능
  • 총 발사 가능
  • 아이템 사용 가능
  • 수영 가능
  • 네트워크 동기화 가능

위의 예시와 같이 여러 기능들이 계속 추가되기 시작하면 어떻게 될까요?

조합 가능한 클래스 수가 계속 증가하기 시작합니다.

그리고 어느 순간부터는 “이 객체는 어떤 클래스를 상속받아야 하지?”라는 고민에 대한 답을 내리기가 굉장히 어려워집니다.

게임은 생각보다 기능 조합이 굉장히 많은 프로그램이기 때문입니다.


그래서 최근 엔진들은 조합 중심 구조를 사용하기 시작했다

이런 문제를 해결하기 위해 등장한 대표적인 구조 중 하나가 Component 기반 구조입니다.

핵심 아이디어는 생각보다 단순합니다.

기능을 상속으로 계속 확장하는 것이 아니라, 필요한 기능을 객체에 붙이는 방식입니다.

예를 들어 아래와 같은 구조를 생각해볼 수 있습니다.

class Component
{
};

class TransformComponent : public Component
{
};

class RenderComponent : public Component
{
};

class HealthComponent : public Component
{
};

class InventoryComponent : public Component
{
};

필요한 기능을 컴포넌트라는 단위로 구성한 것입니다.

그리고 게임 객체(Actor나 GameObject)는 이런 컴포넌트들을 조합해서 구성합니다.

class GameObject
{
public:
    void AddNewComponent(std::shared_ptr<Component> newComponent) { ... }
    
    template<typename T>
    std::shared_ptr<T> GetComponent() { ... }

protected:
    std::vector<std::shared_ptr<Component>> components;
};

즉, “무엇을 상속받는가” 보다, “어떤 기능들을 조합하는가”가 더 중요해지기 시작합니다.


Unity를 사용해봤다면 이미 익숙한 구조

Unity를 사용해본 개발자라면 사실 이미 이 구조에 익숙합니다.

Unity는 대표적인 Component 기반 엔진이기 때문입니다.

Unity에서는 GameObject에 여러 컴포넌트를 붙여서 게임 객체를 구성합니다.

  • Transform
  • Rigidbody
  • Collider
  • AudioSource

즉, 플레이어 클래스 하나가 모든 기능을 직접 가지는 구조가 아니라, 필요한 기능들을 조합하는 구조입니다.

이런 방식은 굉장히 유연합니다.

예를 들어 Rigidbody만 제거하면 물리 기능이 사라집니다.

Collider를 추가하면 충돌 처리가 생깁니다.

AudioSource를 붙이면 사운드 재생 기능이 생깁니다.

즉, 기능을 독립적으로 관리할 수 있게 됩니다.


언리얼 엔진도 결국 비슷한 방향

언리얼 엔진은 Unity의 GameObject와 비교해 조금 더 Actor를 중심으로 설계되어 있습니다.

하지만, 언리얼도 Component 기반 구조를 굉장히 적극적으로 사용합니다.

언리얼의 Actor를 보면 컴포넌트를 기반으로 하는 여러 구조를 확인할 수 있습니다.

  • SceneComponent
  • StaticMeshComponent
  • CameraComponent
  • AudioComponent
  • CharacterMovementComponent

언리얼도 결국 “객체 하나가 모든 기능을 직접 구현하는 방식” 보다는, 컴포넌트를 조합하는 방향으로 발전한 것입니다.

최근의 게임 엔진에서 사용하는 게임 객체의 구조는 결국 비슷한 방향으로 흘러가고 있다는 것을 알 수 있습니다.


Component 기반 구조가 왜 유지보수에 유리할까?

Component 구조의 가장 큰 장점 중 하나는 기능 분리가 가능하다는 점입니다.

예를 들어 체력 시스템을 수정한다고 가정해보겠습니다.

상속 기반 구조에서는 여러 클래스들을 함께 수정해야 할 수도 있습니다.

  • Player
  • Monster
  • NPC
  • Boss

하지만 Component 기반 구조에서는 HealthComponent 하나만 수정하면 되도록 설계할 수 있습니다.

즉, 기능이 독립적으로 분리되기 시작한 것입니다.

그리고 이런 구조는 유지보수 측면에서 굉장히 큰 장점을 가지게 됩니다.

게임 규모가 커질수록 이 차이는 더욱 커집니다.

아래는 언리얼 엔진 공식 문서에서 확인할 수 있는 내용입니다.

소프트웨어의 총 수명 비용 중 80%는 유지보수에 소모됩니다.

이 내용은 유지 보수라는 게 소프트웨어 개발에서 너무나 중요하다는 것을 잘 보여줍니다.

그리고 컴포넌트 기반 구조는 이런 유지 보수에서 강점을 보이는 설계 방식입니다.


최근 엔진 구조는 점점 데이터 중심으로 이동하고 있다

객체 지향 프로그래밍 패러다임이 널리 보급된 이래로 객체를 중심으로 구조를 설계하는 방법이 많이 사용되었습니다.

그런데 최근에는 CPU와 데이터를 중심으로 설계의 방향을 바꾸는 움직임이 보이기 시작했습니다.

그리고 최근 엔진 구조가 단순히 Component 구조에서 끝나지 않고, 한 단계 더 나아갑니다.

최근에 상용 게임 엔진 관련해서 ECS(Entity Component System), Data Oriented Design 같은 용어를 자주 접하게 되실 겁니다.

그리고 이런 구조들은 결국 비슷한 방향과 연결됩니다.

객체를 중심으로 설계하는 것에서 나아가 더 높은 성능을 내기 위해 다른 부분을 더 중요하게 보기 시작한 것입니다.

  • 데이터를 어떻게 배치할 것인가
  • 기능을 어떻게 분리할 것인가
  • 어떻게 반복 처리할 것인가

Component 구조는 단순한 설계 패턴 정도가 아니라, 최근 게임 엔진 구조 변화의 시작점 중 하나라고 볼 수도 있습니다.


상속이 잘못된 것은 아니다

여기까지 읽으셨다면, 상속 구조가 잘못된 것처럼 여겨질 수 있습니다.

하지만, 상속 구조가 잘못되었다는 뜻은 절대 아닙니다.

설계 관점에서 상속이 더 자연스러운 경우도 분명 존재합니다.

문제는 게임처럼 기능 조합이 굉장히 많아지는 환경에서는,
상속만으로 모든 문제를 해결하려고 하면 구조가 지나치게 복잡해질 수 있다는 점입니다.

“상속이 나쁘다”라는 의미가 아니라 “상황에 따라 더 적절한 구조가 존재한다”고 이해하는 게 더 적절합니다.

그리고 최근 게임 엔진들은 점점 조합 중심 구조 즉, Component를 기반으로 하는 구조를 선호하기 시작했습니다.

참고로, 언리얼 엔진은 전통적으로 상속 구조로 설계된 대표적인 엔진이었습니다.
그런데 언리얼 엔진 4를 시작으로 Component 기반 구조로 게임 객체의 설계가 바뀌었습니다.


게임 엔진 구조를 이해하는 중요한 시작점

Component 기반 구조가 단순히 편리한 설계 방식처럼 느껴질 수도 있습니다.

하지만 게임 엔진 구조를 조금 더 깊게 보다 보면 많은 시스템들도 결국,
컴포넌트 기반 구조를 도입하게 되었던 고민의 방향과 연결되어 있다는 것을 알 수 있습니다.

ECS, Delegate, Event System, Object Pool, Data Oriented Design 같은 구조들도 결국,
“기능과 데이터를 어떻게 분리하고 조합할 것인가”를 고민한 결과라고 볼 수 있습니다.

그리고 이런 흐름을 이해하기 시작하면 왜 최근 엔진들이 지금과 같은 방향으로 발전하고 있는지도 이해하실 수 있으실 겁니다.


마무리

게임 엔진 그리고 게임은 생각보다 굉장히 복잡한 프로그램입니다.

수많은 객체들이 등장하고, 기능이 계속 추가되며, 객체들 사이의 관계도 점점 복잡해집니다.

그리고 이런 환경에서는 “기능을 어떻게 잘 분리할 것인가”가 굉장히 중요한 문제가 됩니다.

최근 게임 엔진들이 Component 기반 구조를 중요하게 사용하는 이유도 결국 이런 고민의 흔적이라고 볼 수 있습니다.

단순히 편리한 구조처럼 느껴질 수 있지만,
조금 더 깊게 들여다보면 최근 게임 엔진 구조 변화의 핵심 흐름 중 하나와 연결되어 있다는 것도 알게됩니다.


👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다