10. 메시지 큐 소비자의 오프셋 커밋 순서·멱등성·리밸런스 결함 (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// 시나리오: 게임 백엔드의 이벤트 파이프라인. 결제/보상 이벤트가 파티션된 메시지 큐
// (Kafka 류)로 흐르고, 소비자(consumer)가 파티션별로 오프셋을 따라가며 배치로 읽어
// 재화를 지급한다. 소비자는 컨슈머 그룹에 속해, 노드 증감/장애 시 파티션이
// 다른 소비자로 재할당(rebalance)될 수 있다.
//
// 요구사항:
//   - Poll: 현재 오프셋부터 한 배치를 읽어 각 이벤트를 처리하고, 진행한 오프셋을 커밋한다.
//   - GrantReward: 이벤트대로 플레이어에게 재화를 지급한다.
//   - 크래시/재기동/재할당이 일어나도 "정확히 한 번 지급된 것과 동등"해야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 크래시·재시도·재할당 타이밍에서 지급이 중복되거나
//       유실될 수 있다. 어떤 상황에서 그런지 밝히고 고쳐라.

using System.Collections.Generic;

public struct RewardEvent
{
    public long   Offset;
    public long   PlayerId;
    public long   Amount;
    public string EventId;      // 프로듀서가 붙인 고유 ID
}

public interface IMessageQueue
{
    List<RewardEvent> Fetch(int partition, long offset, int maxCount);
    void CommitOffset(int partition, long offset);
}

public sealed class RewardConsumer
{
    private readonly IMessageQueue _mq;
    private readonly int _partition;
    private long _offset;
    private readonly Dictionary<long, long> _wallet;   // playerId -> balance

    public RewardConsumer(IMessageQueue mq, int partition, long startOffset, Dictionary<long, long> wallet)
    { _mq = mq; _partition = partition; _offset = startOffset; _wallet = wallet; }

    public void Poll()
    {
        var batch = _mq.Fetch(_partition, _offset, maxCount: 100);
        foreach (var e in batch)
        {
            GrantReward(e);                          // (B)
        }
        if (batch.Count > 0)
        {
            _offset = batch[batch.Count - 1].Offset + 1;
            _mq.CommitOffset(_partition, _offset);   // (A)
        }
    }

    private void GrantReward(RewardEvent e)
    {
        _wallet.TryGetValue(e.PlayerId, out long bal);
        _wallet[e.PlayerId] = bal + e.Amount;        // 잔액에 지급액을 더한다
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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