33. 하트비트와 데이터 메시지의 시퀀스 공유로 인한 유실 오탐
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오 (서버-클라): 서버와 클라이언트는 하나의 송신 시퀀스 번호 카운터를
// 공유한다. 게임 데이터 메시지(스킬 시전, 이동 등)뿐 아니라 하트비트(생존
// 확인) 메시지도 이 카운터에서 번호를 받아 나간다. 즉 하트비트도 시퀀스
// 번호를 하나씩 "소모"한다.
//
// 수신측은 유실 탐지를 위해 수신한 시퀀스 번호가 이전에 받은 번호보다
// 정확히 1 커졌는지를 검사한다(갭이 있으면 유실로 간주해 재전송을 요청한다).
//
// 요구사항:
// - 하트비트가 시퀀스 번호를 소모하며 오갔다는 사실이, 데이터 메시지에
// 대한 유실 탐지 결과에 영향을 주어서는 안 된다(정상 상황에서 오탐이
// 발생해서는 안 된다).
// - 실제로 데이터 메시지가 유실된 경우에는 반드시 갭이 탐지되어야 한다
// (하트비트 때문에 진짜 유실이 가려져서는 안 된다).
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
using System.Threading;
public enum MessageKind { Data, Heartbeat }
public sealed class OutgoingMessage
{
public long SequenceNumber;
public MessageKind Kind;
public byte[] Payload;
}
// 송신측: 하트비트와 데이터 메시지가 하나의 카운터를 공유한다.
public sealed class Sender
{
private long _nextSeq = 1;
public OutgoingMessage BuildDataMessage(byte[] payload)
{
long seq = Interlocked.Increment(ref _nextSeq) - 1;
return new OutgoingMessage { SequenceNumber = seq, Kind = MessageKind.Data, Payload = payload };
}
public OutgoingMessage BuildHeartbeat()
{
// (A) 하트비트도 같은 카운터에서 번호를 받아 시퀀스를 소모한다.
long seq = Interlocked.Increment(ref _nextSeq) - 1;
return new OutgoingMessage { SequenceNumber = seq, Kind = MessageKind.Heartbeat, Payload = null };
}
}
public interface IRetransmitRequester
{
void RequestRetransmit(long fromSeqExclusive, long toSeqInclusive);
}
// 수신측: 데이터 메시지의 시퀀스 번호만 보고 갭을 검사한다.
public sealed class DataMessageGapDetector
{
private long _lastSeenDataSeq = 0;
private readonly IRetransmitRequester _requester;
public DataMessageGapDetector(IRetransmitRequester requester)
{
_requester = requester;
}
// 데이터 메시지가 도착할 때마다 호출된다. 하트비트는 이 메서드를
// 거치지 않는다.
public void OnDataMessageReceived(OutgoingMessage msg)
{
// (B) 이전에 받은 데이터 메시지의 시퀀스 번호보다 정확히 1 커야
// 정상으로 판단한다. 하트비트가 그 사이에 번호를 가져간 경우는
// 고려하지 않는다.
long expected = _lastSeenDataSeq + 1;
if (msg.SequenceNumber != expected)
{
_requester.RequestRetransmit(_lastSeenDataSeq, msg.SequenceNumber);
}
_lastSeenDataSeq = msg.SequenceNumber;
}
} 결함 코드 · C++
// ============================================================================
// 시나리오 (서버-클라)
// ----------------------------------------------------------------------------
// 서버와 클라이언트는 하나의 송신 시퀀스 번호 카운터를 공유한다. 게임 데이터
// 메시지(스킬 시전, 이동 등)뿐 아니라 하트비트(생존 확인) 메시지도 이
// 카운터에서 번호를 받아 나간다 — 하트비트도 시퀀스 번호를 하나씩 소모한다.
//
// 수신측은 유실 탐지를 위해 수신한 시퀀스 번호가 이전에 받은 번호보다
// 정확히 1 커졌는지를 검사한다(갭이 있으면 유실로 간주해 재전송을
// 요청한다).
//
// 요구사항:
// - 하트비트가 시퀀스 번호를 소모하며 오갔다는 사실이, 데이터 메시지에
// 대한 유실 탐지 결과에 영향을 주어서는 안 된다(정상 상황에서 오탐이
// 발생해서는 안 된다).
// - 실제로 데이터 메시지가 유실된 경우에는 반드시 갭이 탐지되어야 한다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <cstdint>
#include <atomic>
#include <vector>
enum class MessageKind { Data, Heartbeat };
struct OutgoingMessage
{
int64_t sequenceNumber;
MessageKind kind;
std::vector<uint8_t> payload;
};
// 송신측: 하트비트와 데이터 메시지가 하나의 카운터를 공유한다.
class Sender
{
public:
OutgoingMessage BuildDataMessage(std::vector<uint8_t> payload)
{
int64_t seq = nextSeq_.fetch_add(1);
return OutgoingMessage{ seq, MessageKind::Data, std::move(payload) };
}
OutgoingMessage BuildHeartbeat()
{
// (A) 하트비트도 같은 카운터에서 번호를 받아 시퀀스를 소모한다.
int64_t seq = nextSeq_.fetch_add(1);
return OutgoingMessage{ seq, MessageKind::Heartbeat, {} };
}
private:
std::atomic<int64_t> nextSeq_{ 1 };
};
class IRetransmitRequester
{
public:
virtual ~IRetransmitRequester() = default;
virtual void RequestRetransmit(int64_t fromSeqExclusive, int64_t toSeqInclusive) = 0;
};
// 수신측: 데이터 메시지의 시퀀스 번호만 보고 갭을 검사한다.
class DataMessageGapDetector
{
public:
explicit DataMessageGapDetector(IRetransmitRequester& requester)
: requester_(requester)
{
}
// 데이터 메시지가 도착할 때마다 호출된다. 하트비트는 이 메서드를
// 거치지 않는다.
void OnDataMessageReceived(const OutgoingMessage& msg)
{
// (B) 이전에 받은 데이터 메시지의 시퀀스 번호보다 정확히 1 커야
// 정상으로 판단한다. 하트비트가 그 사이에 번호를 가져간 경우는
// 고려하지 않는다.
int64_t expected = lastSeenDataSeq_ + 1;
if (msg.sequenceNumber != expected)
{
requester_.RequestRetransmit(lastSeenDataSeq_, msg.sequenceNumber);
}
lastSeenDataSeq_ = msg.sequenceNumber;
}
private:
int64_t lastSeenDataSeq_ = 0;
IRetransmitRequester& requester_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.