본문 바로가기

설계/기술

ECS(Entity Component System)로 설계하는 프로그램 part.1

Unity에서는 LLVM 기반 컴파일러인 Burst compiler(이하 버스트 컴파일러)를 UPM(Unity Package Manager) 을 통해 제공하고 있다1. 유니티는 이 버스트 컴파일러를 이용한 C#을 네이티브 언어와 유사하고 성능을 극대화한 HPC# 을 소개하는 데, 일반적으로 사용하는 C#의 작성과는 거리가 있다. 버스트 컴파일러는 유니티의 DOTS(Data Oriented Technology Stack)이라는 DOD(Data-Oriented Design) 방식의 설계에서 매우 효과적으로 사용된다2.

 

Fig1. DOTS 라 불리는 유니티의 ECS.

 

데이터 기반 설계에서 사용되는 데이터들은 하나의 목적만을 가진 데이터들의 묶음으로 구성되어 일괄 처리되게 하는 것이 중요한데, 버스트 컴파일러는 이러한 작업을 사용자(프로그래머), 그리고 컴퓨터의 최적화 부분에서도 보다 효율적으로 만들어준다. 일반적으로 JIT(Just-In-Time), IL2CPP 컴파일러의 경우, 명시적으로 벡터화를 지정하지 않은 작업은 자동 벡터화가 이루어지지 않지만, .NET에서는 System.Numerics의 Vector를 사용한 경우에만 SIMD 연산을 수행시킬 수 있다3. 이러한 명시적인 연산 선언은 명확하게 구문을 해석할 수 있겠으나, 메모리와 수행할 작업이 명확히 정립되지 않은 일반적인 개발 상황에서는 사용하기 쉽지 않은 것이 현실이다. 버스트 컴파일러는 반복되는 구문을 해석하여 자동 벡터화를 지원한다.

 

Fig2. 벡터화, 모든 chunk는 같은 작업을 수행한다.

이전 OOD(Object-Oriented Design) 접근 방식으로는 DOD를 이해하기는 쉽지 않다. 객체 레벨에서 기능을 다루었다면, 데이터 조각들의 집합 단위로 기능을 수행하므로 임의의 데이터가 특정 오브젝트를 가리키지 않기 때문이다각주3. ECS(Entity Component System)에서는 이러한 사항들을 개선한 설계 방식 중 하나이다. Entity는 인스턴스를 가리키고, Component는 엔티티에 상속될 수 있으며, System에서는 컴포넌트들의 행동을 정의할 수 있게 하여 개발 편의성과 가독성을 증대한다. 또한 이러한 설계는 일반적인 대규모 데이터 집합 군 처리 방식과 유사한데, 이는 벡터화를 장려하여 연산 효율을 높이는 것에 있다.

Operation Flashpoint는 이 설계 철학을 충실히 반영한 게임 중 하나이다5. (ECS의 개념을 사실상 정립한 사람이 만든 게임이니 뭐..) 엔티티를 ID로 사용하여 오브젝트를 특정하고, 컴포넌트는 raw data, 즉 순수 데이터들만 담으며, system에서는 구현만 담당하도록 설계하였다. OOP에서는 클래스와 오브젝트가 있듯, ECS에서는 엔티티와 컴포넌트가 있다. 하지만 명확히 해야 하는 것은 클래스 != 엔티티, 오브젝트(객체) != 컴포넌트라는 점이다. 이를 동일시하여 이해할 경우 ECS를 이해하고 개발하는 데 큰 어려움을 겪게 될 확률이 높다6.

ECS, Entity Component System


엔티티는 그 자체로 어떠한 기능을 하지 않고, 데이터를 담지 않으며, 단순히 identifier(식별자)로서 존재한다. 월드에서 어떠한 "것" 이 있다면, 그것을 하나의 엔티티라 칭할 수 있을 것이다. 허나 여기서 이야기하는 "그것"이 특정한 행동이나 가시적인 상태, 즉 데이터나 구현을 의미하는 것이 아니다. 하나의 식별자로서 기능하여, 세계 내 어떤 것을 엔티티라 정의한다각주1. 엔티티가 데이터를 갖기 위해서는 새로운 개념이 추가되어야 하는데, 여기서 컴포넌트가 등장한다. 컴포넌트는 엔티티를 정의하는 값, 즉 순수 데이터를 의미한다. 컴포넌트는 그 자체로는 기능할 수 없다. 단순히 사실들만 담는 컨테이너로서 엔티티의 내용을 정의하는 데 사용된다. 여기서 가장 중요한 핵심은 엔티티를 분류될 수 있게 하여 어떠한 기능에 종속시키는 것에 있다.

마지막으로, 시스템에서 OOP와의 차별점이 확연히 드러나기 시작한다. OOP에서는 모든 것을 객체화하고, 이를 그대로 사용하는 반면, ECS에서는 정의와 구현이 확실히 구분된다각주2. 각 시스템에서는 각자의 독립적인 스레드를 갖고 동작하며, 컴포넌트를 가진 모든 엔티티에게 동일한 작업을 수행한다. 100개, 아니 100,000개의 오브젝트가 있다고 가정하자. 이 각각의 오브젝트들은 랜덤 한 좌표로 이동하는 기능을 수행할 것인데, 이론적으로 이 기능을 수행하기 위해서 동일한 100,000개의 이동하는 기능을 수행하는 메서드가 복사되어 실행될 것이다7, 각주4. 런타임뿐만 아니라 컴파일러 수준에서도 이런 작업을 최적화하기 위해 많은 메모리와 자원을 낭비할 것이다8.

 

Fig3. 엔티티와 컴포넌트, 그리고 이를 처리하는 시스템

엔티티에서는 각 인스턴스에 대한 기능을 전혀 복사하지 않는다. 엔티티와 컴포넌트는 기능을 전혀 갖지 않기 때문이다. 각 컴포넌트에 맞는 기능을 수행하는 기능은 외부에서 정의된 시스템에서 수행되기 때문에, 런타임과 컴파일러에서의 최적화가 필요 없어지고, 불필요한 메모리 낭비를 줄일 수 있게 된다6. Fig3에서 시스템 0 은 컴포넌트 0만 모아서 처리하고, 시스템 1은 컴포넌트 1과 2, 그리고 시스템 2는 컴포넌트 0과 1만을 처리한다. 이렇게 데이터를 다양한 시스템에서 원하는 형태로 재 가공하여 처리하기 용이하고, 치밀한 데이터, 메모리 설계가 가능하게 한다.

누가, 그리고 왜 ECS를 사용하고자 하는가? 독립된 개발 환경을 갖고자 하거나, 주 프로세스에서 벗어나 특정 부분만 작업하고 싶은 사람이 있을 것이다. 각 시스템은 독립적인 스레드를 갖기 때문에 다른 환경에 영향을 크게 받지 않고, 컴포넌트 단위로 설계되기 때문에 집중적인 개발이 가능하다. 렌더링을 예로 들 수 있다. 각 엔티티는 렌더를 위한 정보를 담는 컴포넌트를 갖고, 시스템은 이러한 엔티티들을 모아 프로세스 하는 것을 가능케한다.

 

단점


이렇게 장점만 높고 보니, 그렇다면 왜 널리 쓰이지 않는지 의문이 들 수 있다. ECS의 치명적인 단점 중 하나는 거의 모든 프로그래머가 OOP에 너무 익숙하다는 점이다. 개발의 설계부터 생각의 기준을 바꾸어야 하는 만큼, 자신만의 개발 전략을 갖고 있는 프로그래머라면 더더욱 이를 사용하기 까다로울 수밖에 없다. 또 하나는 설계할 "데이터"를 최대한 멀리 바라보고 구조를 짜야 한다는 점이다10. 개발이 오래될수록 프로그램에 관여하는 시스템이 필연적으로 증가할 수밖에 없는데, 이러한 시스템들이 만약 특정 컴포넌트를 공유하고 있다면 이를 수정하는 것은 대단히 어렵고 복잡한 작업이 될 것이다. 디버깅 또한 복잡도가 상승하는 데, 일반적으로 컴포넌트들의 기능은 한 번에 수행되지 않고, 여러 개의 컴포넌트들의 조합으로 수행되어 디버깅이 쉽지 않다.

또한, 대규모 작업이 이루어지는 환경에 특화되어 있다 보니 소규모 작업이나 기간이 짧은 프로젝트에는 적합하지 않다. 오히려 코드의 복잡도를 증가시키고, 개발 기간이 늘어날 수 있다. 예를 들어, 단순히 2명의 사람이 의자에 앉게 하는 작업을 하고 싶어서 3개의 엔티티를 만들고, 렌더 컴포넌트(인스턴스를 화면에 표시하도록 렌더 하는), 이동 컴포넌트, 행동 컴포넌트, 애니메이션 컴포넌트 ... 그리고 각각의 컴포넌트를 담당하는 시스템까지! 객체 지향 설계에서는 간단히 설계될 작업이 DOD에서는 오히려 복잡도가 상승하고 비효율적이 된다. 그렇다면 어떤 프로그램, 환경에서 ECS는 최대의 효율을 보여줄까?

응용


 

t-machine의 저자, Adam Martin은 ECS의 개념을 MMOG(Massively Multiplayer Online Game) 서버 미들웨어를 구축할 때 처음으로 사용하기 시작했다. 이후 이제껏 경험하지 못했던 대규모 멀티플레이어 게임인 Operation Flashpoint 2에 참여하고, ECS가 네트워크, 그리고 클라이언트 프로그래머 간의 최소한의 간섭으로 원활히 동작하게 하는 가장 효과적인 설계 방법이었다고 설명한다. 추가로, 20명이 넘는 클라이언트 프로그래머에게 "네트워크 친화적인 개발을 하라"라고 이야기하는 것보다, 네트워크 프로그래머가 직접 게임의 메인 시스템의 최소한의 간섭으로, 대규모 멀티플레이어 서버에서 요구하는 복잡한 기능들을 구현하는 것이 가능했다고 기술하는 데, 이와 같이 대규모의 멀티플레이어 게임, MMOG에서 ECS는 강한 강점을 보인다. 그리고 그의 경험에 의거하면 팀의 규모가 클수록, 그리고 작업이 세분화될수록 좋은 설계 전략이 될 수 있음을 증명한다6.

 

OF2 클라이언트 팀과 네트워크 팀, 두 팀 모두 행복한 결과를 가져왔던 이유는 모든 시스템이 독립적으로 돌아가는 것과, 데이터와 기능이 전부 분리되어 데이터의 설계 초기부터 의도하지 않았더라도 네트워크 친화적인 설계가 되기 때문이다. 클라이언트 팀은 당시 PS3의 하드웨어를 극복하기 위해 최대한의 메모리 효율을 뽑고, SOP(Stream-oriented programming, 스트림 지향 코딩11, 각주5)가 필수적이었다. 그들에게 있어 ECS는 최선의 선택지였을 것이다.

20년이 넘는 세월 동안 조금씩 그 개념이 확장되어 발전된 ECS, 유니티의 DOTS를 통해 차세대 설계 전략으로 주목받을 수 있을까? 최근 유니티는 대규모 업데이트를 통해 ECS를 업그레이드하였다. 과연 기존의 OOP 패러다임을 벗어나 DOP를 선두하는 게임 엔진이 될 수 있을지 많은 흥미가 생긴다. 게이머들이 원하는 더 넓은 게임 속 세상을 쉽게 구현할 수 있도록 더 발전된 ECS가 되기를.


각주

  1. 엔티티를 설명할 때 어려운 점이, 기존 OOP 와는 전혀 다른 설계 방식이라 비교 군과 적절한 단어 표현을 찾기 어렵다. Martin 은 월드 내 instance들을 엔티티라 지칭한다.
  2. cpp에서 헤더와 소스파일을 생각하면 이해가 될 수 있다. 단, 헤더에 인라인 구현이 포함될 수 있는 것과 달리, 이런 구현은 ECS에서 규칙을 위반한 것으로 간주한다9.
  3. 엔티티는 객체를 의미하지 않는다. 대체되는 의미로는 스마트 포인터로 들 수 있는데, 엔티티 자체로는 아무런 기능과 데이터, 정말로 아무것도 아니기 때문이다. 마찬가지로 데이터로 불리는 Component(컴포넌트) 또한 그 자체로는 아무런 기능과 의미를 갖지 않는다. 엔티티와 그에 상속된 컴포넌트들을 연산하는 시스템의 존재가 있어야 비로소 의미를 부여받는다.
  4. 일반적으로 메서드는 객체 단위로 만들어지지 않고, 클래스 단위로 만들어진다. .NET의 JIT 컴파일러의 경우, 만들어진 메서드를 복사하여 네이티브 코드를 만들어 실행한다. 생성된 코드는 메모리에서 지워지거나, 지워진 후 다시 생성될 수 있는데, 이러한 작업은 런타임 중 여러 차례 이루어질 수 있다.
  5. Martin 이 이야기하는 PS3 등, 게임 콘솔들은 한정된 메모리로 게임을 실행시켜야 하기 때문에 메모리에 항시 로드하지 않는 파일 스트림 지향이 필수라고 이야기한다. 최근 콘솔들이라고 과거와 크게 달라진 것은 없는데, 콘솔에 애플리케이션을 출시 전에 미리 해당 앱이 사용할 메모리를 선언하는 식이다. 윈도우와 같은 PC들은 동적으로 메모리를 할당받는 반면, 콘솔은 선언한 메모리만 할당받고, 그 이상은 할당받지 못한다. Xbox Series, PS5, Switch 모두 위와 같다.

참조

  1. https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/index.html
  2. https://unity.com/dots
  3. https://devblogs.microsoft.com/dotnet/hardware-intrinsics-in-net-core/
  4. https://www.computer.org/csdl/magazine/co/2022/05/09771161/1DeEYnefsoU
  5. https://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/
  6. https://t-machine.org/index.php/2007/11/11/entity-systems-are-the-future-of-mmog-development-part-2/
  7. https://stackoverflow.com/questions/1298122/where-are-methods-stored-in-memory
  8. https://stackoverflow.com/questions/27751624/are-instance-methods-duplicated-in-memory-for-each-object
  9. https://en.wikipedia.org/wiki/Entity_component_system#cite_note-3
  10. https://stackoverflow.com/questions/58596897/what-are-the-disadvantages-of-the-ecs-entity-component-system-architectural-pa
  11. https://en.wikipedia.org/wiki/Stream_processing

© 2024 - 2024 Vvidr - All Rights Reserved.

'설계 > 기술' 카테고리의 다른 글

열거형 쿼리 (Enum query)  (0) 2024.05.23
배열을 최적화하는 방법  (0) 2024.03.13
Jetbrains AI Assistant를 활용한 프로그램  (0) 2024.02.12