이전 글에서는 게임 개발에서 객체 지향 상속 구조가 점점 복잡해질 수 있는 이유를 정리했습니다.
살펴본 것과 같이 게임은 생각보다 기능 조합이 굉장히 많은 프로그램입니다.
프로젝트 규모가 커질수록 복잡도 역시 빠르게 증가하기 시작합니다.
- 새로운 기능이 계속 추가되고
- 기존 기능이 수정되며
- 서로 다른 시스템들이 연결되기 시작합니다.
특히 상속 기반 구조에서는 기능 하나를 수정하는 일이 예상보다 훨씬 넓은 범위에 영향을 주는 경우도 많습니다.
그래서 최근 게임 엔진들은 Component 기반으로 게임 객체를 설계했습니다.
이번 글에서는 왜 Component 기반 구조가 유지보수에 유리한지,
그리고 왜 최근 엔진들이 기능을 “상속”보다 “조합” 중심으로 바라보기 시작했는지를 조금 더 자세하게 정리해보려고 합니다.
상속 구조에서는 기능이 서로 강하게 연결되기 쉽다
클래스 간의 결합도 즉, 강하게 연결되는가 여부는 유지보수에 큰 영향을 주는 요인 중 하나입니다.
Character 타입을 상속하는 Player와 Monster 클래스가 있다고 가정해 봅시다.
class Character
{
public:
void Move()
{
}
void Attack()
{
}
int health;
};
class Player : public Character
{
};
class Monster : public Character
{
};
Player와 Monster 모두 Move와 Attack을 할 수 있고, health를 가진다면, 굉장히 자연스러운 구조라고 생각할 수 있습니다.
공통 기능은 Character에 모아두고, Player와 Monster는 이를 상속 받아서 사용하면 되기 때문입니다.
그런데 프로젝트가 커지기 시작하면 문제가 생길 수 있습니다.
요구 사항이 더 늘어나면서 다양하게 고려해야 하는 내용이 추가되기 시작합니다.
- 플레이어만 사용하는 기능
- 몬스터만 사용하는 기능
- 특정 NPC만 사용하는 기능
그리고 어느 순간부터 Character 클래스가 굉장히 많은 기능을 가지게 됩니다.
하나의 클래스가 점점 비대해지기 시작한다
이렇게 요구하는 기능이 늘어나게 되면 아래와 같이 한 클래스가 담당하는 책임이 매우 커질 수 있습니다.
class Character
{
public:
void Move()
{
}
void Attack()
{
}
void PlayAnimation()
{
}
void OpenInventory()
{
}
void UseSkill()
{
}
void SyncNetwork()
{
}
};
단순했던 Character 클래스가 점점 거대한 클래스가 되어갑니다.
그리고 여기서 더 큰 문제는 기능들이 서로 강하게 연결되기 시작했다는 점입니다.
예를 들어 인벤토리 기능을 수정했는데, 애니메이션 시스템이나 네트워크 코드에도 영향을 줄 가능성이 생깁니다.
즉, 클래스 하나가 너무 많은 책임을 가지게 됩니다.
이런 구조는 유지보수 난이도를 점점 높이게 됩니다.
게임은 계속 바뀌는 프로그램이다
게임은 한 번 만들고 끝나는 프로그램이 아닙니다.
기획이 계속 변경됩니다.
새로운 기능이 추가됩니다.
기존 시스템이 수정됩니다.
- 점프 기능 추가
- 장비 시스템 추가
- 멀티플레이 기능 추가
- AI 시스템 수정
다양한 변화들이 계속 발생할 수 있습니다.
문제는 상속 기반 구조에서는 이런 변화가 기존 클래스 구조 전체를 흔들 가능성이 있다는 점입니다.
특히 부모 클래스 수정은 영향 범위가 굉장히 커질 수 있습니다.
그리고 프로젝트 규모가 커질수록 이런 문제는 더욱 심각해집니다.
Component 구조는 기능을 분리하기 시작했다
최근의 게임 엔진들은 이 문제를 해결하기 위해 기능을 독립적으로 분리하기 시작했습니다.
아래와 같은 구조를 생각해볼 수 있습니다.
class Component
{
};
class TransformComponent : public Component
{
};
class HealthComponent : public Component
{
};
class InventoryComponent : public Component
{
};
class NetworkComponent : public Component
{
};
그리고 게임 객체는 필요한 기능만 조합합니다.
class GameObject
{
public:
template<typename T>
void AddNewComponent<T>() { ... }
template<typename T>
std::shared_ptr<T> GetComponent() { ... }
protected:
std::vector<std::shared_ptr<Component>> components;
};
필요한 기능들을 하나의 클래스 안에 몰아넣는 것이 아니라,
각 기능을 독립적으로 구현하고,
최종 클래스인 게임 객체 클래스에서 각 컴포넌트를 조합해 완성합니다.
기능 수정 범위를 줄일 수 있다
이 구조의 가장 큰 장점 중 하나는 수정 범위를 줄일 수 있다는 점입니다.
예를 들어 체력 시스템을 수정한다고 가정해보겠습니다.
상속 기반 구조에서는 부모 클래스인 Character를 변경하면, 이를 상속하는 여러 클래스들이 영향을 받을 수 있습니다.
- Player
- Monster
- Boss
- NPC
하지만 Component 기반 구조에서는 HealthComponent 하나만 수정하면 되는 경우가 많습니다.
기능이 독립적으로 분리되기 때문에,
유지보수 측면에서도 굉장히 큰 장점을 가지게 됩니다.
프로젝트 규모가 커질수록 이 차이는 더욱 커집니다.
재사용성도 높아지기 시작한다
Component 구조의 또 다른 장점은 재사용성입니다.
예를 들어 체력 기능이 필요한 객체에서는 HealthComponent를 붙이면 됩니다.
인벤토리 기능이 필요하다면 InventoryComponent를 추가하면 됩니다.
즉, 기능을 독립적으로 여러 객체에서 재사용할 수 있게 됩니다.
- 플레이어
- 몬스터
- 상자(Box)
- 파괴 가능한 오브젝트
이런 객체 모두에서 HealthComponent를 사용할 수도 있습니다.
상속 기반 구조였다면 이런 기능 공유가 생각보다 복잡해질 수도 있습니다.
하지만 Component 구조에서는 비교적 자연스럽게 해결할 수 있습니다.
유니티를 사용해봤다면 이미 익숙한 구조다
유니티를 사용해본 개발자라면 사실 이미 이런 구조에 익숙합니다.
대표적인 Component 기반 엔진이기 때문입니다.
필요한 여러 기능들을 GameObject에 붙이는 방식으로 사용합니다.
- Rigidbody
- Collider
- AudioSource
- Animator
플레이어 클래스 하나가 모든 기능을 직접 구현하는 구조가 아니라, 필요한 기능들을 조합하는 방향에 가깝습니다.
이런 구조는 기능을 독립적으로 관리하기 굉장히 편리합니다.
언리얼 엔진도 결국 비슷한 방향이다
언리얼 엔진 역시 Component 구조를 굉장히 적극적으로 사용합니다.
- SceneComponent
- StaticMeshComponent
- AudioComponent
- CameraComponent
언리얼 엔진 역시 결국 기능을 조합하는 방향으로 발전하고 있습니다.
최근 게임 엔진들을 보다 보면 상속 중심 구조보다,
조합 중심 구조를 점점 더 중요하게 생각하기 시작했다는 것을 느끼게 됩니다.
Component 구조에도 단점은 존재한다
물론 Component 구조가 모든 문제를 해결해주는 것은 아닙니다.
컴포넌트가 지나치게 많아질 수도 있습니다.
오히려 객체 간 관계가 복잡해질 수도 있습니다.
컴포넌트 의존성 문제가 생길 수도 있습니다.
예를 들어 TransformComponent가 반드시 필요하다거나,
PhysicsComponent가 특정 컴포넌트에 의존하는 상황이 생길 수도 있습니다.
Component 구조 역시 잘못 사용하면 복잡해질 수 있습니다.
중요한 것은 어떤 구조가 절대적으로 정답이라는 것이 아닙니다.
프로젝트 특성과 문제 상황에 따라 더 적절한 구조가 존재할 수 있다는 점을 이해하는 것이 중요합니다.
어떤 설계 기법이나 구조가 복잡한 모든 문제를 해결해주지 않는다는 것을 이해하고,
현재 내가 직면한 문제가 어떤 것인지 그리고 이 문제에 가장 적합한 답을 찾을 수 있는 관점을 갖도록 노력해야 합니다.
최근 엔진 구조는 점점 더 작은 단위로 기능을 분리하기 시작했다
최근 게임 엔진의 구조는 여기에서 더 발전하고 있습니다.
최근에는 ECS(Entity Component System), Data Oriented Design과 같은 구조들이 계속 등장하고 있습니다.
그리고 이런 구조들도 결국 비슷한 방향과 연결됩니다.
거대한 객체 하나가 모든 것을 처리하는 방식보다,
기능과 데이터를 더 작은 단위로 분리하려고 하기 시작한 것입니다.
결국 유지보수는 복잡도를 관리하는 문제다
프로젝트 규모가 작을 때는 어떤 구조를 사용해도 큰 문제가 없는 경우가 많습니다.
하지만 게임 규모가 커지고,
기능이 계속 추가되기 시작하면,
복잡도를 어떻게 관리할 것인가가 굉장히 중요한 문제가 됩니다.
그리고 최근 게임 엔진들이 Component 기반 구조를 중요하게 생각하는 이유도 결국 이런 고민과도 연결됩니다.
기능을 독립적으로 분리하고,
필요한 기능만 조합하고,
수정 범위를 최소화하려는 것입니다.
즉, Component 구조는 단순한 설계 패턴 정도가 아니라, 복잡도를 관리하기 위한 고민의 결과 중 하나라고 볼 수도 있습니다.
마무리
게임은 생각보다 굉장히 빠르게 복잡해지는 프로그램입니다.
기능이 계속 추가되고, 시스템들이 서로 연결되며, 프로젝트 규모도 점점 커집니다.
그리고 이런 환경에서는 “기능을 어떻게 잘 분리할 것인가”가 굉장히 중요한 문제가 됩니다.
최근 게임 엔진들이 Component 기반 구조를 선호하기 시작한 이유도 결국 이런 흐름과 연결됩니다.
처음에는 단순히 편리한 구조처럼 느껴질 수도 있습니다.
하지만 조금 더 깊게 들여다보면 최근 엔진 구조 변화의 핵심 흐름 중 하나와 연결되어 있다는 것도 알게됩니다.