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++
// ============================================================================
// [코드리뷰 문제] C++ - 채팅/길드 같은 글로벌 서비스의 이벤트 순서 보장 (서버-서버)
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/분산 · 서버-서버):
// - 길드 채팅은 "글로벌 채팅 서비스" 가 담당한다. 길드원들은 서로 다른 존(zone) 서버에
// 흩어져 있고, 각 존 서버는 자기 존의 발화(메시지)를 글로벌 서비스로 포워딩한다.
// - 글로벌 서비스는 여러 존 서버에서 동시에 들어오는 메시지를 받아, 채널(길드)별로
// "모든 수신자가 동일한 순서로" 보도록 정렬한 뒤, 각 길드원이 접속한 존 서버로
// 팬아웃(broadcast)한다.
// - 메시지에는 발신 존 서버의 벽시계 타임스탬프(originTsMs)가 찍혀 온다.
// - Ingest 는 여러 수신 스레드에서 동시에 호출된다.
//
// 요구사항:
// - 한 채널의 메시지는 모든 수신자에게 "동일한 전역 순서" 로 전달돼야 한다.
// - 서버 간 시계 차이가 있어도 순서가 뒤집히면 안 된다.
// - 같은 메시지가 중복 전달되거나, 순서 구멍(gap)이 조용히 무시되면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 순서 역전·중복·구멍·손상이
// 발생하는지(동시 인터리빙·시계 차이 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
struct ChatEvent {
std::uint64_t channelId;
std::uint64_t senderId;
std::string text;
std::int64_t originTsMs; // 발신 존 서버의 벽시계(ms)
};
class GlobalChatService {
public:
// 여러 존 서버 수신 스레드가 동시에 호출
void Ingest(const ChatEvent& ev) {
// (A) 채널별 순서 번호 발급
std::uint64_t seq = seqByChannel_[ev.channelId]++;
// (B) 도착한 이벤트를 채널 버퍼에 넣고 "발신 타임스탬프" 기준으로 정렬
auto& buf = pending_[ev.channelId];
buf.push_back(Stamped{ev, seq});
std::sort(buf.begin(), buf.end(),
[](const Stamped& a, const Stamped& b) {
return a.ev.originTsMs < b.ev.originTsMs; // 시계 기준 정렬
});
Flush(ev.channelId);
}
private:
struct Stamped { ChatEvent ev; std::uint64_t seq; };
void Flush(std::uint64_t channelId) {
auto& buf = pending_[channelId];
// (C) 정렬된 순서대로 모든 길드원이 접속한 존 서버로 팬아웃
for (const auto& s : buf) {
for (std::uint64_t member : membersByChannel_[channelId])
SendToMember(member, s.ev, s.seq);
}
buf.clear(); // 보냈으니 버퍼 비움
}
void SendToMember(std::uint64_t /*memberId*/, const ChatEvent& /*ev*/,
std::uint64_t /*seq*/) {
// 해당 길드원이 접속한 존 서버로 전송(네트워크, 생략)
}
std::unordered_map<std::uint64_t, std::uint64_t> seqByChannel_; // 채널->다음 seq
std::unordered_map<std::uint64_t, std::vector<Stamped>> pending_; // 채널->대기 버퍼
std::unordered_map<std::uint64_t, std::vector<std::uint64_t>> membersByChannel_; // 채널->멤버
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.