내일배움캠프

내일배움캠프: 이벤트 버스 패턴 (Event Bus Pattern)

pracumj 2024. 11. 2. 18:20

이벤트 버스 패턴이란?

이벤트 버스발행자(Publisher)와 구독자(Subscriber) 사이에서 중간 매개체(hub) 역할을 하는 패턴이다. 

 

예를 들어, 이벤트 버스기사를 발행하고 이를 구독자에게 전달하는 신문사라고 생각하면 된다.

신문사가 없다면 발행자가 직접 기사를 작성해 구독자들에게 전달해야 하지만, 신문사(이벤트 버스)를 통해 기사를 발행할 수 있게 된다. 즉, 구독자는 발행자가 누구인지 알 필요가 없고, 발행자도 구독자의 정보를 알 필요가 없어진다.

 

다만, 이벤트 버스를 통해 신문의 발행과 구독이 이뤄지기 때문에 약간의 비용이 발생할 수 있다. 일종의 수수료라고 생각하면 된다.  또한, 신문사를 통해 모든 발행과 구독이 이뤄지고 있다. 즉, 모든 기사(이벤트)의 중심이 신문사(이벤트버스)가 되기 때문에 만약 신문사에 문제가 생긴다면 발행자와 구독자 모두 영향을 받을 수 있다. 이는 이벤트 버스의 책임이 커질 수 있다는 단점으로 작용하게 된다. 

 

이벤트 버스 패턴을 사용해보자

유니티 이벤트(UnityEvent) 

이벤트 버스 패턴을 사용하기전 유니티이벤트에 대해서도 알고 가면 좋다. UnityEvent는 유니티에서 제공하는 기능으로 델리게이트를 쉽게 다룰 수 있게 해준다.   이벤트를 호출하고 리스너를추가/해제 하는 과정을 더 편리하게 할 수 있다.  Inspector 창에서 리스너를 추가하고 관리할 수 있어서 코드를 작성하지 않고도 이벤트를 연결 할 수 있다.  다만, UnityEvent의 경우에는 Unity의 직렬화 시스템에 의존하기 때문에, 씬 이동이나 프리팹 로드 시 연결된 리스너가 손실될 수 있으므로 주의가 필요하다. 

 

더보기

 

  1. UnityEvent는 성능 측면에서 C# Event보다 훨씬 느리고 메모리 효율이 떨어진다.
  2. UnityEvent는 리스너가 2개 이상일 때 메모리 할당에서 약간의 장점이 있지만, 가비지 발생과 속도에서 불리
  3. 결론적으로, 성능이 중요한 상황에서는 UnityEvent 대신 C# Event를 사용하는 것이 더 바람직
  4. 이 성능 차이는 대부분의 프로젝트에서 미미하므로 큰 규모가 아닌 이상 체감할 정도는 아님

 

 

UnityEvent는 성능 면에서 Action보다 좋지 않으므로, 성능이 중요한 상황에서는  Action을 사용하는게 더 좋다. 이 성능 차이는 대부분의 프로젝트에서 미미하므로 큰 규모가 아닌 이상 체감할 정도는 아니다. UnityEvent의 장점은 Inspector 창에서 이벤트 등록이 가능하다는 점이다. 따라서  Inspector에서 이벤트 등록를 하고 싶은 경우에는 UnityEvent를 선택하는 것이 더 좋다. 성능이 크게 고려되지 않는 경우라면 Action과 UnityEvent 중 어떤 것을 사용해도 무방 하다. 

 Action을 사용한 이벤트 버스 채널  예시 코드

using System;
using System.Collections.Generic;
using UnityEngine;

public enum AchievementType
{
    AllStageClear,
    NoDeathClear,
    HundredDeaths
}

public class AchievementEventBus : MonoBehaviour
{
    private Dictionary<AchievementType, Action> eventDictionary = new Dictionary<AchievementType, Action>();

    public void Subscribe(AchievementType type, Action listener)
    {
        if (eventDictionary.TryGetValue(type, out Action thisEvent))
        {
            thisEvent += listener;
            eventDictionary[type] = thisEvent; 
        }
        
        else
        {
            thisEvent = listener;
            eventDictionary.Add(type, thisEvent); 
        }
    }

    public void Unsubscribe(AchievementType type, Action listener)
    {
    
        if (eventDictionary.TryGetValue(type, out Action thisEvent))
        {
            thisEvent -= listener;
            if (thisEvent == null)
                eventDictionary.Remove(type);
            else
                eventDictionary[type] = thisEvent; 
        }
    }

    public void Publish(AchievementType type)
    {
        if (eventDictionary.TryGetValue(type, out Action thisEvent))
            thisEvent?.Invoke();
    }
}

UnitEvent를 사용한 이벤트 버스 채널  예시 코드

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public enum AchievementType
{
    AllStageClear,
    NoDeathClear,
    HundredDeaths
}

public class AchievementEventBus : MonoBehaviour
{
    private Dictionary<AchievementType, UnityEvent> eventDictionary = new Dictionary<AchievementType, UnityEvent>();

    public void Subscribe(AchievementType type, UnityAction listener)
    {
        if (eventDictionary.TryGetValue(type, out UnityEvent thisEvent))
        {
            thisEvent.AddListener(listener);
        }
        else
        {
            thisEvent = new UnityEvent();
            thisEvent.AddListener(listener);
            eventDictionary.Add(type, thisEvent);
        }
    }

    public void Unsubscribe(AchievementType type, UnityAction listener)
    {
        if (eventDictionary.TryGetValue(type, out UnityEvent thisEvent))
        {
            thisEvent.RemoveListener(listener);
            
            if (thisEvent.GetPersistentEventCount() == 0)
                eventDictionary.Remove(type);
        }
    }

    public void Publish(AchievementType type)
    {
        if (eventDictionary.TryGetValue(type, out UnityEvent thisEvent))
            thisEvent?.Invoke();
    }
}

 

Action과 UnityEvent는 사용 방식과 코드 스타일에 따라 선택하여 사용하면 된다. 이벤트 버스는 중심 역할을 하므로, 다른 객체들이 접근할 수 있도록 static 또는 싱글톤 패턴으로 구현해야 한다.

 

이벤트 버스 구독자와 발행자 예시 코드 

public abstract class Subscriber : MonoBehaviour
{
    protected abstract AchievementType EventType { get; }

    protected virtual void OnEnable()
    {
        AchievementEventBus.Instance.Subscribe(EventType, Activate);
    }

    protected virtual void OnDisable()
    {
        AchievementEventBus.Instance.Unsubscribe(EventType, Activate);
    }

    protected abstract void Activate();
}



public class AchievementEventPublisher : MonoBehaviour
{
     public void OnAllStagesCleared()
    {
         AchievementEventBus.Instance.Publish(AchievementType.AllStageClear);
    }

     public void OnHundredDeaths()
    {
        AchievementEventBus.Instance.Publish(AchievementType.HundredDeaths);
    }
    
     public void OnNoDeathClear()
    {
        AchievementEventBus.Instance.Publish(AchievementType.NoDeathClear);
    }
}

AchievementEventPublisher 클래스는 특정 클리어 조건이 충족되었을 때 해당 조건에 맞는 이벤트를 발행하는 메서드를 포함하고 있다. 외부의 조건 판별 클래스는 이러한 메서드를 호출하여 이벤트 버스를 통해 이벤트를 발행한다.

이벤트를 구독하려면 Subscriber라는 추상 클래스를 상속하여 구독자 클래스를 정의하고, 각 구독자가 받을 이벤트 타입과 Activate 메서드를 설정해주면 이벤트가 발행될 때 Activate 메서드가 호출되어 설정해준 작업을 수행하게 된다.

AchievementEventPublisher는 예시를 위해 제공된 클래스이다. 실제로는 업적 조건을 판단하는 클래스에서 바로 이벤트 버스를 통해 이벤트를 발행해줘도 무방하다. 마찬가지로 구독하는 경우에도 추상 클래스 Subscriber 같은 특정 틀이 정해져 있는 것은 아니다. 이벤트 버스를 통해 원하는 타입의 이벤트를 구독해주기만 하면 된다.

마무리

이벤트 버스 패턴의 핵심은 발행과 구독을 어떻게 하는지 보다는 발행과 구독을 관리하는 주체가 이벤트 버스라는 점이다. 이벤트 버스가 주체로 관리하게 되면 발행자와 구독자 간의 의존성이 줄어들며 유연한 구조를 갖게 된다. 예를 들어 새로운 이벤트를 처리하는 상황이 생겨도 기존 이벤트의 수정 없이 새로운 구독자를 등록함으로써 해결이 가능해진다.

 

다만, 이벤트 버스를 통해 발행과 구독이 이루어지기 때문에, 약간의 비용이 발생할 수 있으며 이벤트 버스 자체에 대한 의존성이 커질 수 있는 위험이 있다.