객체지향 상속 구조는 왜 점점 복잡해질까?

이전 글에서는 왜 최근 게임 엔진들이 Component 기반 구조를 사용하기 시작했는지 정리했습니다.

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

그리고 기능이 계속 추가되기 시작하면,
전통적인 객체 지향 상속 구조 만으로는 관리하기 어려워지는 상황들이 등장하기 시작합니다.

아래 예시와 같이 다양한 조합을 갖는 객체를 다룰 때 상속은 문제를 더 복잡하게 만들 수 있습니다.

  • 날아다니는 몬스터
  • 총을 쏘는 NPC
  • 수영 가능한 보스
  • 네트워크 동기화가 필요한 플레이어

상속 구조가 굉장히 자연스럽게 느껴질 수 있습니다.

실제로 객체 지향 프로그래밍(OOP)을 공부할 때도 공통 기능은 부모 클래스에 모으고,
필요한 기능은 자식 클래스에서 확장하는 방식을 굉장히 이상적인 구조처럼 배우게 됩니다.

하지만 게임 규모가 커지고 기능 조합이 많아질수록, 좋아 보였던 상속 구조가 점점 복잡해지기 시작합니다.

이번 글에서는 왜 게임 개발에서 상속 구조가 점점 복잡해지는지,
그리고 왜 최근 엔진들이 Component 기반 구조를 선호하기 시작했는지를 조금 더 자세하게 정리해보려고 합니다.


처음에는 모든 것이 자연스럽다

우리는 객체 지향 프로그래밍(Object Oriented Programming)이 너무나 익숙한 시대에 프로그래밍을 시작했습니다.

C++, C#, Java로 프로그래밍을 시작하신 분들이 많으실 겁니다.

그리고 무엇보다 대표적인 상용 게임 엔진인 유니티와 언리얼 엔진 모두에서 객체 지향 프로그래밍 언어를 주 언어로 채택했습니다.

그렇기 때문에 자연스럽게 객체 지향 프로그래밍(OOP)을 이상적인 방법처럼 받아들이기 쉽습니다.

공통 기능은 부모 클래스에 모으고, 필요한 기능은 자식 클래스에서 확장하는 상속 구조가 우리에게는 너무나 자연스럽습니다.

아래 코드는 상속을 활용해 캐릭터 타입의 Player와 Monster 타입을 작성하는 예를 보여줍니다.

class Character
{
public:
    void Move()
    {
    }
};

class Player : public Character
{
};

class Monster : public Character
{
};

Character를 상속했기 때문에 두 클래스 모두에서 Move 함수를 사용할 수 있습니다.

Move 기능은 Character가 공통으로 가지고,
Player와 Monster는 필요한 기능만 추가하면 되기 때문에 코드 중복을 줄일 수 있어 효율적으로 보이기도 합니다.

그리고 객체 지향 프로그래밍에서 “상속”은 굉장히 중요한 개념입니다.

하지만 세상의 모든 개념들이 그렇듯 이렇게 강력하고 좋은 상속 또한 잘 사용해야 합니다.

게임의 규모가 커지기 시작하면, 좋아 보였던 상속 구조가 점점 복잡해지기 시작합니다.


처음에는 모든 것이 자연스럽다

캐릭터가 비행할 수 있는 기능이 필요해졌다고 가정해보겠습니다.

그러면 자연스럽게 아래와 같은 코드를 생각할 수 있습니다.

Character 타입을 상속한 후에 Fly 함수를 추가해 필요한 기능을 더하는 방식입니다.

class FlyingCharacter : public Character
{
public:
    void Fly()
    {
    }
};

여기에 총을 발사할 수 있는 기능도 추가되었다고 가정해 봅시다.

Character를 상속하는 ShootingCharacter 타입을 생성하고, Shoot 함수를 추가해 기능을 보완합니다.

class ShootingCharacter : public Character
{
public:
    void Shoot()
    {
    }
};

여기까지는 문제가 없습니다.

그런데 기능의 조합이 복잡해지기 시작했다고 가정해 봅시다.

  • 날아다니면서 총을 쏘는 적
  • 수영도 가능한 몬스터
  • 비행하면서 아이템도 사용하는 보스

이렇게 다양한 조합을 갖는 객체들이 등장하기 시작합니다.

물론 상속을 기반으로 설계를 하더라도 복잡한 문제를 잘 풀 수는 있습니다.

하지만, 복잡한 조합의 문제를 상속으로 풀면 풀수록 구조가 복잡해질 가능성이 높아집니다.


기능 조합이 많아질수록 클래스 수가 폭발하기 시작한다

위의 상황을 상속 기반으로 해결해보겠습니다.

조합이 더해질수록 아래와 같은 클래스들이 계속 필요해질 수 있습니다.

class FlyingShootingCharacter : public Character
{
};

class FlyingSwimmingCharacter : public Character
{
};

class FlyingShootingSwimmingCharacter : public Character
{
};

처음에는 단순해 보였던 상속 구조가 점점 복잡해지기 시작합니다.

기능이 하나 추가될 때마다 새로운 조합을 나타내는 클래스가 필요해질 가능성이 생깁니다.

  • 비행 가능
  • 총 사용 가능
  • 수영 가능
  • 인벤토리 사용 가능
  • 네트워크 동기화 가능

다양한 기능들이 계속 늘어나는 것은 게임 개발 과정에서 매우 빈번합니다.

객체 종류보다 기능 조합이 더 많아지기 시작합니다.

그리고 어느 순간부터는 “이 객체는 어떤 클래스를 상속받아야 하지?”라는 고민 자체가 너무 어려워집니다.

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


다중 상속은 해결책처럼 보일 수도 있다

이 시점에서 자연스럽게 다중 상속(Multiple Inheritance)을 떠올릴 수도 있습니다.

예를 들어 아래처럼 구성하는 방식입니다.

class Flyable
{
};

class Shootable
{
};

class Swimmingable
{
};

class Boss : public Flyable, public Shootable
{
};

꽤 좋은 방식이라고 생각할 수 있고,
역사적으로 다중 상속을 통해서 위에서 언급한 복잡한 조합의 문제를 해결했던 시절도 있습니다.

겉보기에는 꽤 좋아 보입니다. 필요한 기능들을 여러 개 상속 받을 수 있기 때문입니다.

하지만, 다중 상속을 사용하는 이 방법은 이미 문제가 있다는 것이 검증되었습니다.

다중 상속을 지원하는 대표적인 언어인 C++의 최신 문법 설명서를 보시면 다중 상속을 설명은 하지만, 사용하지 말라는 내용을 확인할 수 있을 겁니다.

그리고 C++를 계승 발전시킨 C#과 Java는 모두 클래스의 다중 상속을 문법 차원에서 막았습니다.


다중 상속은 생각보다 복잡한 문제를 만든다

다중 상속을 사용하면 대표적으로 아래와 같은 문제들이 등장하기 시작합니다.

  • 함수 이름 충돌
  • 메모리 구조 복잡화
  • 가상 상속 문제
  • 객체 생성 구조 복잡화
  • 유지보수 난이도 증가

특히, Diamond Problem 같은 문제는 객체 지향 프로그래밍을 공부할 때 굉장히 유명한 사례라는 것을 잘 아실 겁니다.

class Character
{
};

class FlyableCharacter : public Character
{
};

class ShootableCharacter : public Character
{
};

class Boss : public FlyableCharacter, public ShootableCharacter
{
};

위의 코드에서 Boss 클래스 안에는 Character가 두 번 존재할 수도 있습니다.

물론 C++은 virtual inheritance 같은 기능으로 해결할 수 있습니다.

하지만 상속을 기반으로 이런 복잡한 조합의 문제를 해결하려 할수록 구조가 복잡해지는 것은 피할 수가 없습니다.

상속을 기반으로 기능 조합을 계속 확장하는 방식은
프로젝트 규모가 커질수록 유지보수를 어렵게 만들 수 있습니다.


게임은 계속 변화하는 프로그램

중요한 부분이 또 있습니다.

우리는 게임 또는 게임 엔진을 만들기 위해 프로그래밍의 복잡한 문제를 해결하려 노력합니다.

그런데 우리가 다루는 프로그램은 기능이 계속 추가되는 프로그램입니다.

처음 기획 단계에서는 없었던 기능이 계속 등장합니다.

  • 탈것 시스템
  • 스킬 시스템
  • 네트워크 기능
  • 물리 기능
  • 상태 이상 시스템

다양한 요소들이 개발이 진행될수록 계속 추가될 수 있습니다.

문제는 상속 기반 구조에서는 이런 변화가 전체 클래스 구조를 흔들 수도 있다는 점입니다.

예를 들어서 Character 클래스를 수정했는데, Character를 상속하는 여러 클래스에서 영향을 받을 수 있습니다.

  • Player
  • Monster
  • NPC
  • Boss

상속 구조로 클래스를 설계하면 클래스 사이의 결합도가 점점 강해질 수밖에 없습니다.


최근 엔진들은 조합 중심 구조를 선호하기 시작했다

이런 문제를 해결하기 위해 최근 게임 엔진들은 Component 기반 구조를 선호합니다.

Component 기반 구조의 핵심은 기능을 상속으로 계속 확장하는 것이 아니라,
필요한 기능을 조합하는 것입니다.

  • 이동 기능
  • 체력 기능
  • 렌더링 기능
  • 인벤토리 기능

각 기능을 독립적인 컴포넌트로 분리합니다.

그리고 게임 객체는 필요한 기능만 조합해서 사용합니다.

GameObject
 ├─ TransformComponent
 ├─ RenderComponent
 ├─ HealthComponent
 └─ InventoryComponent

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


Unity와 Unreal도 결국 비슷한 방향이다

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

GameObject에 Rigidbody, Collider, AudioSource 같은 컴포넌트를 붙여 기능을 구성합니다.

언리얼 엔진도 다양한 컴포넌트를 활용해 Actor를 구성할 수 있도록 설계되어 있습니다.

  • SceneComponent
  • MeshComponent
  • AudioComponent

즉, 최근 게임 엔진들은 결국 “기능 조합 중심 구조”로 점점 이동하고 있습니다.

이는 단순한 유행이 아니라, 대규모 게임을 유지보수하기 위해 노력해온 결과라고 볼 수 있습니다.


상속이 나쁜 것은 아니다

그렇다고 상속 자체가 나쁘고 사용하면 안된다는 것을 설명하는 것이 아닙니다.

상속도 필요한 곳에 잘 사용하면 너무 좋은 기능입니다.

  • 명확한 is-a 관계
  • 공통 인터페이스 구조
  • 추상화 계층 구성

이렇게 다양한 상황에서 상속은 여전히 강력합니다.

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

즉, “상속이 틀렸다”가 아니라 “상황에 따라 더 적절한 구조가 존재할 수 있다”고 이해하는 것이 중요합니다.


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

게임 엔진들은 여기에서 한 발 더 나아갔습니다.

최근 게임 그리고 게임 엔진 관련 내용을 살펴보면 “데이터 기반”이라는 용어를 많이 듣게 되실겁니다.

ECS(Entity Component System), Data Oriented Design이 바로 “데이터 기반” 설계에 해당합니다.

그리고 이런 구조들도 결국 설계적 고민의 결과입니다.

얼마 전까지 “객체” 관점에서 설계를 했다면,
최근에는 객체 자체보다 CPU와 데이터 관점에서 설계를 하는 추세입니다.

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

즉, 최근 게임 엔진 구조는 “거대한 상속 트리” 보다는 “작은 기능 조합”의 방향으로 설계의 관점이 이동하고 있습니다.


결국 중요한 것은 구조를 바라보는 관점이다

객체 지향 상속 구조가 굉장히 이상적으로 여겨질 수 있습니다.

작은 프로젝트에서는 꽤 효과적이기도 합니다.

하지만 게임 규모가 커지고, 기능 조합이 많아지기 시작하면,
상속만으로는 감당하기 어려운 문제들이 등장하기 시작합니다.

그리고 이런 문제들을 해결하기 위해 다양한 방법들이 등장했습니다.

  • Component 구조
  • ECS
  • Data Oriented Design

최근 게임 엔진 구조 변화는 단순한 유행이 아니라, 복잡도를 관리하기 위해 고민한 결과로 볼 수 있습니다.


마무리

게임 엔진은 굉장히 많은 기능들이 서로 연결되는 프로그램입니다.

그리고 프로젝트 규모가 커질수록 “기능을 어떻게 분리할 것인가”가 굉장히 중요한 문제가 됩니다.

어떤 문제를 해결할 때 상속 구조가 자연스럽게 느껴질 수 있습니다.

하지만 기능 조합이 많아질수록 클래스 구조는 점점 복잡해집니다.

최근 게임 엔진들이 Component 기반 구조를 선호하기 시작한 이유도 결국 이런 이유에서 입니다.

그리고 Component 기반 구조의 강점을 이해하기 시작하면,
유니티와 언리얼 엔진이 Component 기반 구조를 도입한 이유도 자연스럽게 공감되실 겁니다.


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

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

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

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

댓글 남기기

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