Observer 4. Event Bus

Observer 4. Event Bus

코드들은 설명을 위한 예제 코드입니다. 오류가 있을 수 있습니다.

이벤트 버스(Event Bus)는 Publisher-Subscriber 패턴의 확장된 형태로, 시스템 전반에서 PublisherSubscriber가 서로 직접적인 관계 없이 메시지나 이벤트를 주고받을 수 있도록 돕는 패턴입니다. 이벤트 버스는 시스템 내 다양한 이벤트를 중앙화된 버스에서 관리하고, 구독자들에게 효율적으로 전달하는 역할을 합니다. 특히, 대규모 시스템에서 모듈 간 통신을 쉽게 하고, 독립적인 컴포넌트들이 이벤트 기반으로 협력할 수 있도록 합니다.

Publisher-Subscriber 패턴과 이벤트 버스의 관계

Publisher-Subscriber 패턴에서는 발행자가 이벤트를 발행하면, 그 이벤트를 구독한 모든 구독자에게 이벤트가 전달됩니다. 이 구조는 발행자와 구독자 사이의 느슨한 결합을 보장하며, 독립적인 컴포넌트들이 직접적인 의존성 없이 상호작용할 수 있습니다.

이벤트 버스는 이 패턴을 확장하여 다양한 이벤트를 중앙 집중화된 구조로 관리하고, 이벤트가 발생할 때마다 구독자에게 전달하는 역할을 수행합니다. 이를 통해 시스템 전반에서 이벤트를 일관성 있게 관리하고 처리할 수 있습니다.

코드 예시

아래는 간단한 이벤트 버스 구현 예시입니다.

IEventArgs 정의

1
public interface IEventArgs {}

이 인터페이스는 이벤트 데이터가 상속받을 기본 인터페이스입니다. 각 이벤트 타입에 맞는 데이터를 이 인터페이스를 상속하여 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
public struct SWJEventArgs : IEventArgs
{
    public string message;

    public SWJEventArgs(string message)
    {
        this.message = message;
    }
}

SWJEventArgsIEventArgs를 상속받아 이벤트 데이터를 전달합니다. 예를 들어, 특정 메시지를 이벤트로 전달하고 싶을 때 이 구조체를 사용할 수 있습니다.

EventBinding 클래스

event를 이벤트 데이터 타입별로 binding 합니다.

1
2
3
4
5
public interface IEventBinding<T> where T : IEventArgs
{
    Action EventNoArgs { get; }
    Action<T> EventWithArgs { get; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EventBinding<T> : IEventBinding<T> where T : IEventArgs
{
    private Action eventNoArgs;
    private Action<T> eventWithArgs;

    // Explicit interface implementation
    Action IEventBinding<T>.EventNoArgs => eventNoArgs;
    Action<T> IEventBinding<T>.EventWithArgs => eventWithArgs;

    public EventBinding(Action eventNoArgs) => this.eventNoArgs = eventNoArgs;
    public EventBinding(Action<T> eventWithArgs) => this.eventWithArgs = eventWithArgs;

    public void Add(Action<T> eventWithArgs) => this.eventWithArgs += eventWithArgs;
    public void Remove(Action<T> eventWithArgs) => this.eventWithArgs -= eventWithArgs;

    public void Add(Action eventNoArgs) => this.eventNoArgs += eventNoArgs;
    public void Remove(Action eventNoArgs) => this.eventNoArgs -= eventNoArgs;
}

EventBus 클래스

EventBus는 중앙에서 모든 등록된 이벤트를 관리하고, 발생시키는 역할을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class EventBus<T> where T : IEventArgs
{
    static readonly HashSet<IEventBinding<T>> bindings = new HashSet<IEventBinding<T>>();

    public static void Register(IEventBinding<T> binding) => bindings.Add(binding);
    public static void UnRegister(IEventBinding<T> binding) => bindings.Remove(binding);

    public static void Invoke(T e)
    {
        foreach (var binding in bindings)
        {
            binding.EventWithArgs?.Invoke(e);
            binding.EventNoArgs?.Invoke();
        }
    }

    static void Clear()
    {
        bindings.Clear();
    }
}

Unity 에서 구현

이제 Unity에서 EventBus를 사용해봅시다. 초기화에는 다양한 방법이 있습니다. 이번엔 Unity 의 RuntimeInitializeOnLoadMethod를 통해 이벤트 버스를 초기화 해보겠습니다.

Unity RuntimeInitializeOnLoadMethod로 이벤트 버스 초기화

이 코드는 Unity 에디터나 런타임에서 이벤트 버스의 초기화상태 관리를 처리합니다. 각 씬이 시작될 때 이벤트 버스를 초기화하고, 에디터 모드에서는 플레이 모드 종료 시 모든 이벤트 버스를 초기화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public static class EventBusUtil
{
    public static IReadOnlyList<Type> EventTypes {get; set;}
    public static IReadOnlyList<Type> EventBusTypes {get; set;}

#if UNITY_EDITOR
    public static PlayModeStateChange PlayModeState {get; set;}

    [InitializeOnLoadMethod]
    public static void InitialiseEditor()
    {
        EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
    }

    static void OnPlayModeStateChanged(PlayModeStateChange state)
    {
        PlayModeState = state;
        if(state == PlayModeStateChange.ExitingPlayMode)
            ClearAllBuses();
    }
#endif

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void Initialise()
    {
        EventTypes = PredefinedAssemblyUtil.GetTypes(typeof(IEventArgs));
        EventBusTypes = InitialiseAllBuses();
    }

    static List<Type> InitialiseAllBuses()
    {
        List<Type> busTypes = new List<Type>();

        var typeDef = typeof(EventBus<>);
        foreach(var eventType in EventTypes)
        {
            var busType = typeDef.MakeGenericType(eventType);
            busTypes.Add(busType);
        }
        return busTypes;
    }

    public static void ClearAllBuses()
    {
        foreach(var busType in EventBusTypes)
        {
            var clearMethod = busType.GetMethod("Clear", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
            clearMethod.Invoke(null, null);
        }
    }
}

PredefinedAssemblyUtil

특정 인터페이스를 구현한 타입들을 어셈블리에서 검색하고 반환하는 유틸리티입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public static class PredefinedAssemblyUtil
{
    enum AssemblyType
    {
        AssemblyCSharp,
        AssemblyCSharpEditor,
        AssemblyCSharpFirstPass,
        AssemblyCSharpEditorFirstPass,
    }

    static AssemblyType? GetAssemblyType(string assemblyName)
    {
        return assemblyName switch
        {
            "Assembly-CSharp" => AssemblyType.AssemblyCSharp,
            "Assembly-CSharp-Editor" => AssemblyType.AssemblyCSharpEditor,
            "Assembly-CSharp-firstpass" => AssemblyType.AssemblyCSharpFirstPass,
            "Assembly-CSharp-Editor-firstpass" => AssemblyType.AssemblyCSharpEditorFirstPass,
            _ => null,
        };
    }

    static void AddTypesFromAssembly(Type[] assemblyTypes, Type interfaceType, List<Type> results)
    {
        if(assemblyTypes == null) return;

        for(int i = 0; i < assemblyTypes.Length; i++)
        {
            Type type = assemblyTypes[i];
            if(type != interfaceType && interfaceType.IsAssignableFrom(type))
            {
                results.Add(type);
            }
        }
    }

    public static List<Type> GetTypes(Type interfaceType)
    {
        Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

        Dictionary<AssemblyType, Type[]> assemblyTypes = new Dictionary<AssemblyType, Type[]>();
        List<Type> types = new List<Type>();

        for(int i = 0; i < assemblies.Length; i++)
        {
            Assembly assembly = assemblies[i];
            AssemblyType? assemblyType = GetAssemblyType(assembly.GetName().Name);

            if(assemblyType.HasValue)
            {
                assemblyTypes.Add(assemblyType.Value, assembly.GetTypes());
            }
        }

        assemblyTypes.TryGetValue(AssemblyType.AssemblyCSharp, out var assemblyCSharpTypes);
        AddTypesFromAssembly(assemblyCSharpTypes, interfaceType, types);

        assemblyTypes.TryGetValue(AssemblyType.AssemblyCSharpEditor, out var assemblyCSharpEditorTypes);
        AddTypesFromAssembly(assemblyCSharpEditorTypes, interfaceType, types);

        return types;
    }
}

SWeetJelly
SWeetJelly Programmer, Game Designer
comments powered by Disqus