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++
// ============================================================================
// 시나리오 (서버 내부)
// ----------------------------------------------------------------------------
// 서버 내부 이벤트 버스(Event Bus). 여러 모듈이 특정 이벤트 타입을 구독
// (Subscribe)하고, 이벤트가 발생하면 구독된 리스너들에게 순서대로 통지
// (Publish)한다. 리스너는 raw 포인터로 등록된다(소유권은 호출자가 가진다).
//
// 리스너는 처리 도중(콜백 안에서) 자기 자신 또는 다른 모듈을 구독 해제
// (Unsubscribe)할 수 있다. 어떤 모듈은 자신이 소멸(destruct)될 때 자신을
// 구독 해제하도록 되어 있다. 또한 다른 스레드(예: 모듈 종료/정리 스레드)가
// 임의의 시점에 Unsubscribe 를 호출하거나 리스너 객체 자체를 파괴할 수 있다.
//
// 요구사항:
// - Publish 도중에는 그 시점에 구독 중이던 리스너 전원이 정확히 한 번씩
// 통지를 받아야 한다.
// - 이미 구독 해제되었거나 파괴된 리스너에게는 통지가 가면 안 된다.
// - 리스너 콜백 도중 또는 다른 스레드에서 구독 해제가 일어나도 Publish
// 자체가 안전하게 끝까지 완료되어야 한다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
class IEventListener
{
public:
virtual void OnEvent(const std::string& eventName, void* payload) = 0;
virtual ~IEventListener() = default;
};
class EventBus
{
public:
void Subscribe(const std::string& eventName, IEventListener* listener)
{
listeners_[eventName].push_back(listener); // (A)
}
void Unsubscribe(const std::string& eventName, IEventListener* listener)
{
auto it = listeners_.find(eventName);
if (it == listeners_.end())
return;
auto& list = it->second;
list.erase(std::remove(list.begin(), list.end(), listener), list.end()); // (B)
}
void Publish(const std::string& eventName, void* payload)
{
auto it = listeners_.find(eventName);
if (it == listeners_.end())
return;
// 구독 중인 리스너 전원에게 순서대로 통지한다.
for (IEventListener* listener : it->second)
{
listener->OnEvent(eventName, payload);
}
}
private:
std::unordered_map<std::string, std::vector<IEventListener*>> listeners_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.