[내일배움캠프 본 캠프 52일차] 이벤트버스 구성

2025. 12. 10. 21:06본 캠프

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
{
    //구독자를 타입형태로 리스트로 관리
    private static readonly Dictionary<Type, IList> _subscribers = new();
 
    //IList에 리스트로 관리할 구독자들을 불러오기
    private static List<WeakReference<Action<T>>> GetList<T>()
    {
        var type = typeof(T);
 
        // 딕셔너리에 해당 이벤트 타입이 없다면 새 리스트를 생성하여 등록
        if (!_subscribers.TryGetValue(type, out var list))
        {
            list = new List<WeakReference<Action<T>>>(); 
            _subscribers[type] = list;
        }
 
        //IList(콜백 저장된 리스트)를 실제 이벤트 형태의 타입으로 캐스팅해서 반환
        return (List<WeakReference<Action<T>>>)list; 
    }
 
cs

 

이벤트버스라는 라우터를 활용하기 위해서는 어떻게 구성해야 할까?

구독자들을 목록으로 관리해야 할 거고 이벤트도 타입단위로 분리해서

해당 이벤트 타입에 맞는 구독자를 불러와야 하는 형태로 코드를 작성해야 한다.

 

IList는 C# System에서 제공하는 비제네릭 리스트 인터페이스다.

어떤 리스트든 담을 수 있는 인터페이스인 IList를 선언하고 실제로 넣을 때는 List<WeakReference<Action<T>>>를 넣는다.

꺼낼 때는 타입에 맞춰 캐스팅해서 사용하면 된다.

 

그렇다면 WeakReference는 뭘까?

 

Unity에서 로직 흐름상 가장 흔하게 일어나는 오류 패턴 중

-씬이 언로드 되거나/오브젝트가 Destroy 됐을 때

정적(static) 객체가 오브젝트를 여전히 참고하고 있어서 메모리에서 사라지지 않는 문제가 있다.

 

이걸 Strong reference라고 칭하는데

이벤트버스가 구독자를 붙잡고 놓지 않은 상황이라면 플레이신을 재진입하거나 오브젝트가 파괴될 때 오류가 생길 수 밖에 없다.

 

 

Weak reference는 참조는 하지만 객체의 생명주기를 책임지지 않는다.

Eventbus가 구독자를 갖고 있어도 구독자가 Destroy 되면 GC가 지울 수 있다.

GC가 객체를 지우면 WeakReference 내부 target이 null이 되면서 Eventbus는 이를 감지하고 자동으로 제거할 수 있게 된다.

 

Additive Scene형식으로 UI를 메인신과 분리해서 구현이 목표이기 때문에 이와같은 '약한'참조의 형태가 필수적이게 된다.

 

그렇기에 List<WeakReference>를 사용하게 되면 하나의 이벤트 타입에 구독자가 여러 명 존재하는 경우

ex: 스트레스 변경시 -> HUDManager에서는 이미지 fillbar 변경/ UIManager에서는 해당수치에 영향을 받는 UI 변경 등

 

여러 WeakReference를 저장해야 하기때문에 List가 된다.

 

그럼 여기서 의문이 생기는데 Dictionary<Type, List<WeakReference<Action<T>>>> 를 쓰면 되지않을까? 라는 생각이 들지만

 

T가 이벤트마다 다르기 때문에 제네릭 타입이 이벤트마다 달라서 T가 다른 List<T>들을 공통 타입으로 관리해야 한다.

그래서 상위 타입인 IList 인터페이스를 사용한다.

 

Dictionary는 구조상 하나의 타입이어야 하기 때문에 비제네릭 상위 타입인 IList로 선언 한뒤

-> 제네릭 으로 진입 해서 실제 콜백들을 캐스팅해서 쓰는 방식이 된다.

 

이벤트버스의 구조도

 


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
public static void Publish<T>(T eventData)
{
    var list = GetList<T>();
 
    var snapshot = list.ToArray(); // 리스트 변경 후 오류방어를 위한 복사본 생성
 
    foreach (var weak in snapshot)
    {
        // Destroy된 객체 Traget null -> 자동 제거 (신 변경)
        if (!weak.TryGetTarget(out var callback))
        {
            list.Remove(weak);
            continue;
        }
 
        // 예외처리(개별 구독자 오류로 전체 Publish가 중단되지 않도록 보호처리)
        try
        {
            callback.Invoke(eventData);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[EventBus] {typeof(T).Name} 처리 중 예외: {ex}");
        }
    }
}
cs

 

발행시에는 try-catch 구조를 통해 구독자 한쪽에서 오류가 나와도 아예 멈춰버리는 상황을 방지해야 한다.