[Tech +]컴포넌트 기반 개발 #2/4 : 컴포넌트 끼리는 어떻게 통신할까?

안녕하세요. 알비언 MetaverseLAB의 David입니다. 또 뵙게 되었네요!

지난 콘텐츠(컴포넌트 기반 개발 #1/4)에 이은 두 번째 이야기입니다. 지난 번에는 CBD의 소개와 장단점에 대해 다뤄보았다면 오늘은 컴포넌트 간 통신하는 방법에 대해 이야기해 보고자 합니다.

궁금하시죠? 그럼 바로 시작할게요!


소통, 소통, 소통! 소통합시다~!

컴포넌트 끼리는 어떻게 통신할까?



시작에 앞서 지난 이야기를 간략하게 리뷰해보겠습니다.


지난 이야기

컴포넌트 기반 개발이라(이하 CBD)함은 부품(컴포넌트)를 만들어 이들의 조합으로 객체를 만들고, 소프트웨어를 변경하는 방법론입니다. 이를 통해 얻을 수 있는 우수성과 불편한 점으로는


우수성

  1. 부품을 늘림으로 다양한 객체를 만들 수 있다
  2. 비교적 에디터를 만드는 것이 수월하다
  3. 컴파일 할 일이 줄어든다


불편한 점

  1. 컴포넌트 설계 난이도가 높다.
  2. 컴포넌트를 이용하지 않는 경우보다 느리다
  3. 보다 많은 테스트가 요구된다



부품(컴포넌트)들간의 의사소통

컴포넌트들은 CBD의 폐쇄된 환경 안에서도 동작하기 때문에 매우 매우 독립적입니다. 그렇다고는 하지만 모든 일이 항상 뜻대로 흘러가지는 않죠. 분명 언젠가는 다른 컴포넌트들과 소통(통신)해야 하는 경우가 발생하는데 이 경우 어떻게 해야 할까요? 바로 이벤트를 통해 소통합니다. 

앞으로 소개해드릴 이벤트를 통한 소통을 이용하면 컴포넌트들이 다른 컴포넌트들과 소통할 때 낮은 의존성과 높은 결합도를 거의 해치지 않는 방법으로 소통할 수 있음을 알 수 있습니다.

[ 그림1. 모든 컴포넌트들이 EventContainer(이름은 다를 수 있음)에 자신을 이벤트 구독자로 등록 ]


[ 그림2. ComponentA에서 일이 완료되었다는 이벤트를 EventContainer에 전달.
이 때 몇 가지 매개변수를 함께 넘겨주는 것도 가능 ]


[ 그림3. 이벤트 컨테이너는 구독 등록한 모든 컴포넌트들에게 이벤트가 발생되었음을 알림 ]



[ 그림4. 컴포넌트들은 해당 이벤트에 맞는 작업을 하고 이후 그림2 ~ 4의 일을 반복하며 소프트웨어가 동작하게 됨 ]


이를 간단한 예제로 구현해 보겠습니다.

  • Object Manager
    • 프로그램에 생성되는 모든 객체를 관리하기 위한 클래스

  • Component
    • 프로그램에 존재하는 모든 컴포넌트들의 부모클래스
    • ConcreteComponentA, ConcreteComponentB, ConcreteComponentC 
      : 예를 들기 위해 간단한 기능만 구현되어 있음
    • 개념적으로는 Entity가 컴포넌트들 가지고 있어야 하지만, id만으로 entity를 가르켜도 문제 없음
      ex ) 귀 컴포넌트, 다리 컴포넌트, 털 컴포넌트

  • Entity :
    • 컴포넌트들이 자신이 어디에 소속되는지 설명하는 클래스, 사실 큰 기능이 없다면 정수형으로 대체 가능

  • IEventParam
    • 모든 이벤트 매개변수 클래스 혹은 구조체는 이 인터페이스를 상속받아야 함

  • EventType
    • 이프로그램에서 사용되는 이벤트들의 목록

  • DefaultEventParam
    • 이벤트 매개변수 IEventParam 인터페이스를 상속 받음


[ Source code ]

namespace ComponentWithEvent
{
    public class Component
    {
        public int EntityId { get; set; }

        public virtual void OnEvent(EventType eventType, IEventParam param)
        {
            //code here what todo
        }
    }
}
namespace ComponentWithEvent
{
    public class ConcreteComponentA : Component
    {
        public override void OnEvent(EventType eventType, IEventParam param)
        {
            base.OnEvent(eventType, param);
            switch (eventType)
            {
                case EventType.Event1:
                    Console.WriteLine($"componentName : ConcreteComponentA, entityId : {EntityId} , eventType : {eventType}");
                    break;
                default:
                    break;
            }
        }
    }
}
namespace ComponentWithEvent
{
    public class ConcreteComponentB : Component
    {
        public override void OnEvent(EventType eventType, IEventParam param)
        {
            base.OnEvent(eventType, param);
            switch (eventType)
            {
                case EventType.Event2:
                    Console.WriteLine($"componentName : ConcreteComponentB, entityId : {EntityId} , eventType : {eventType}");
                    break;
                default:
                    //nothing todo
                    break;
            }
        }
    }
}
namespace ComponentWithEvent
{
    public class ConcreteComponentC : Component
    {
        public override void OnEvent(EventType eventType, IEventParam param)
        {
            base.OnEvent(eventType, param);
            switch (eventType)
            {
                case EventType.Event3:
                    var convertedParam = (DefaultEventParam)param;
          //이러한 예시로 이벤트가 무시될 수 있습니다.
                    if (convertedParam.target != this.EntityId)
                        return;

                    Console.WriteLine($"componentName : ConcreteComponentC, entityId : {EntityId} , eventType : {eventType}"); break;
                default:
                    break;
            }
        }
    }
}
namespace ComponentWithEvent
{
    public static class ObjectManager
    {
        static HashSet<Entity> = new HashSet<Entity>();
        static List components<Entity> = new List<Entity>();
        static int objectCount = int.MinValue;
        public static Entity GenerateEntity()
        {
            var newEntity = new Entity(objectCount++);
            entities.Add(newEntity);
            return newEntity;
        }

        public static void AddComponent<T>(Entity entity)
            where T : Component, new()
        {
            T newComponent = new T();
            newComponent.EntityId = entity.Id;
            components.Add(newComponent);
        }

        public static void Emit(EventType eventType, IEventParam param)
        {
            for (int i = 0; i < components.Count; i++)
            {
                components[i].OnEvent(eventType, param);
            }
        }
    }
}
namespace ComponentWithEvent
{
    public interface IEventParam
    {
    }
}
namespace ComponentWithEvent
{
    public struct DefaultEventParam : IEventParam
    {
        public int target;

        public DefaultEventParam(int target)
        {
            this.target = target;
        }
    }
}
namespace ComponentWithEvent
{
    public class Entity
    {
    public int Id { get; private set } 
        public Entity(int id)
        {
            this.Id = id;
        }
    }
}


[ Main Program ]

using ComponentWithEvent;
 
//엔티티 A를 생성
 Entity entityA = ObjectManager.GenerateEntity();
 //컴포넌트 A와 컴포넌트 B가 엔티티 A를 가르키도록하면서 생성
 ObjectManager.AddComponent<ConcreteComponentA>(entityA);
 ObjectManager.AddComponent<ConcreteComponentB>(entityA);
 
//엔티티 B를 생성
 Entity entityB = ObjectManager.GenerateEntity();
 //컴포넌트 C를 하나 더 생성하고 B를 가르키도록 생성
 ObjectManager.AddComponent<ConcreteComponentC>(entityB);
 
Entity entityC = ObjectManager.GenerateEntity();
 ObjectManager.AddComponent<ConcreteComponentA>(entityC);
 ObjectManager.AddComponent<ConcreteComponentC>(entityC);
 
DefaultEventParam eventParam = new DefaultEventParam(entityB.Id);
 //이벤트를 만들어냄
 ObjectManager.Emit(EventType.Event1, eventParam);
 
Console.WriteLine("============================================");
 
eventParam = new DefaultEventParam(entityB.Id);
 ObjectManager.Emit(EventType.Event2, eventParam);
 
Console.WriteLine("============================================");
 
eventParam = new DefaultEventParam(entityC.Id);
 ObjectManager.Emit(EventType.Event3, eventParam);


[ Result ]

componentName : ConcreteComponentA, entityId : -2147483648 , eventType : Event1
componentName : ConcreteComponentA, entityId : -2147483646 , eventType : Event1
============================================
componentName : ConcreteComponentB, entityId : -2147483648 , eventType : Event2
============================================
componentName : ConcreteComponentC, entityId : -2147483646 , eventType : Event3


오늘은 컴포넌트들이 어떻게 소통하는지에 대해 다뤄보았는데요, 이벤트를 통해 각 컴포넌트들의 OnEvent를 호출하기 때문에 컴포넌트 기반 개발을 사용하지 않은 경우보다 대체로 느립니다. 성능을 올리기 위해서 이벤트의 필터링을 다양한 방법으로 할 수 있습니다.

다음 콘텐츠에서는 컴포넌트 기반 개발을 Unity상에서 어떻게 사용하는지 알아보겠습니다. 이번 글 역시 조금이나마 유익하셨길 바랍니다. :)



Ref.

[1] https://ko.wikipedia.org/wiki/컴포넌트_기반_소프트웨어_공학

[2] https://docs.unity3d.com/kr/2018.4/Manual/Components.html

[3] https://docs.unrealengine.com/4.26/en-US/Basics/Components/

[4] https://hrcak.srce.hr/file/69311

[5] https://www.youtube.com/watch?v=cxyG_REKD4Y

[6] http://gameprogrammingpatterns.com/component.html

[7] Game Programming Gems 5 - 정보문화사



Social Media

 official@mergerity.com