35. 이벤트 버스 디스패치 중 구독 해제 경합

난이도 최상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// 시나리오 (서버 내부): 서버 내부 이벤트 버스(Event Bus). 여러 모듈이 특정
// 이벤트 타입을 구독(Subscribe)하고, 이벤트가 발생하면 구독된 리스너들에게
// 순서대로 통지(Publish)한다.
//
// 리스너는 처리 도중(콜백 안에서) 자기 자신 또는 다른 모듈을 구독 해제
// (Unsubscribe)할 수 있다. 예를 들어 "이 이벤트를 한 번만 처리하고 스스로
// 구독을 해제하는" 일회성 리스너 패턴이 흔하다. 또한 다른 스레드(예: 모듈
// 종료/정리 스레드)가 임의의 시점에 Unsubscribe 를 호출할 수도 있다.
//
// 요구사항:
//   - Publish 도중에는 그 시점에 구독 중이던 리스너 전원이 (구독 해제
//     여부와 무관하게) 정확히 한 번씩 통지를 받아야 한다.
//   - 리스너 콜백 도중 또는 다른 스레드에서 구독 해제가 일어나도 Publish
//     자체가 예외 없이 끝까지 완료되어야 한다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.

using System;
using System.Collections.Generic;

public interface IEventListener
{
    void OnEvent(string eventName, object payload);
}

public sealed class EventBus
{
    private readonly Dictionary<string, List<IEventListener>> _listeners = new();

    public void Subscribe(string eventName, IEventListener listener)
    {
        if (!_listeners.TryGetValue(eventName, out var list))
        {
            list = new List<IEventListener>();
            _listeners[eventName] = list;
        }
        list.Add(listener); // (A)
    }

    public void Unsubscribe(string eventName, IEventListener listener)
    {
        if (_listeners.TryGetValue(eventName, out var list))
        {
            list.Remove(listener); // (B)
        }
    }

    public void Publish(string eventName, object payload)
    {
        if (!_listeners.TryGetValue(eventName, out var list))
            return;

        // 구독 중인 리스너 전원에게 순서대로 통지한다.
        foreach (var listener in list)
        {
            listener.OnEvent(eventName, payload);
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.