
Observer 4. Event Bus
코드들은 설명을 위한 예제 코드입니다. 오류가 있을 수 있습니다.
이벤트 버스(Event Bus)는 Publisher-Subscriber 패턴의 확장된 형태로, 시스템 전반에서 Publisher와 Subscriber가 서로 직접적인 관계 없이 메시지나 이벤트를 주고받을 수 있도록 돕는 패턴입니다. 이벤트 버스는 시스템 내 다양한 이벤트를 중앙화된 버스에서 관리하고, 구독자들에게 효율적으로 전달하는 역할을 합니다. 특히, 대규모 시스템에서 모듈 간 통신을 쉽게 하고, 독립적인 컴포넌트들이 이벤트 기반으로 협력할 수 있도록 합니다.
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;
}
}
SWJEventArgs
는 IEventArgs
를 상속받아 이벤트 데이터를 전달합니다. 예를 들어, 특정 메시지를 이벤트로 전달하고 싶을 때 이 구조체를 사용할 수 있습니다.
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;
}
}