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#
내 답안 · 자동 저장

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