18. 채팅/길드 같은 글로벌 서비스의 이벤트 순서 보장 (서버-서버, C#)

난이도 최상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 채팅/길드 같은 글로벌 서비스의 이벤트 순서 보장 (서버-서버)
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/분산 · 서버-서버):
//   - 길드 채팅은 "글로벌 채팅 서비스" 가 담당한다. 길드원들은 서로 다른 존(zone) 서버에
//     흩어져 있고, 각 존 서버는 자기 존의 발화를 글로벌 서비스로 포워딩한다.
//   - 글로벌 서비스는 여러 존 서버에서 동시에 들어오는 메시지를 받아, 채널(길드)별로
//     "모든 수신자가 동일한 순서로" 보도록 정렬한 뒤, 각 길드원이 접속한 존 서버로
//     팬아웃한다.
//   - 메시지에는 발신 존 서버의 벽시계 타임스탬프(OriginTsMs)가 찍혀 온다.
//   - Ingest 는 여러 수신 스레드에서 동시에 호출된다.
//
// 요구사항:
//   - 한 채널의 메시지는 모든 수신자에게 "동일한 전역 순서" 로 전달돼야 한다.
//   - 서버 간 시계 차이가 있어도 순서가 뒤집히면 안 된다.
//   - 같은 메시지가 중복 전달되거나 순서 구멍(gap)이 조용히 무시되면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 순서 역전·중복·구멍·손상이
//   발생하는지(동시 인터리빙·시계 차이 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System.Collections.Generic;
using System.Linq;

public class ChatEvent
{
    public ulong  ChannelId;
    public ulong  SenderId;
    public string Text;
    public long   OriginTsMs;   // 발신 존 서버의 벽시계(ms)
}

public class GlobalChatService
{
    private struct Stamped { public ChatEvent Ev; public ulong Seq; }

    private readonly Dictionary<ulong, ulong> _seqByChannel = new();          // 채널->다음 seq
    private readonly Dictionary<ulong, List<Stamped>> _pending = new();        // 채널->대기 버퍼
    private readonly Dictionary<ulong, List<ulong>> _membersByChannel = new(); // 채널->멤버

    // 여러 존 서버 수신 스레드가 동시에 호출
    public void Ingest(ChatEvent ev)
    {
        // (A) 채널별 순서 번호 발급
        ulong seq = _seqByChannel.TryGetValue(ev.ChannelId, out var s) ? s : 0;
        _seqByChannel[ev.ChannelId] = seq + 1;

        // (B) 도착한 이벤트를 채널 버퍼에 넣고 "발신 타임스탬프" 기준으로 정렬
        if (!_pending.TryGetValue(ev.ChannelId, out var buf))
        { buf = new List<Stamped>(); _pending[ev.ChannelId] = buf; }
        buf.Add(new Stamped { Ev = ev, Seq = seq });
        buf.Sort((a, b) => a.Ev.OriginTsMs.CompareTo(b.Ev.OriginTsMs)); // 시계 기준 정렬

        Flush(ev.ChannelId);
    }

    private void Flush(ulong channelId)
    {
        var buf = _pending[channelId];
        // (C) 정렬된 순서대로 모든 길드원이 접속한 존 서버로 팬아웃
        foreach (var st in buf)
            foreach (var member in _membersByChannel.GetValueOrDefault(channelId, new List<ulong>()))
                SendToMember(member, st.Ev, st.Seq);
        buf.Clear();   // 보냈으니 버퍼 비움
    }

    private void SendToMember(ulong memberId, ChatEvent ev, ulong seq)
    {
        // 해당 길드원이 접속한 존 서버로 전송(네트워크, 생략)
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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