32. 커맨드 재전송 시 서버측 중복 실행 방지(dedup) 부재

난이도 상 해설 보기 →

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

결함 코드 · C#
// 시나리오 (서버-클라): 클라이언트는 서버에 커맨드(아이템 사용, 스킬 시전 등)를
// 보낸 뒤 서버로부터 ACK 를 기다린다. 네트워크 지연/유실로 일정 시간 안에
// ACK 를 받지 못하면 클라이언트는 같은 커맨드를 동일한 CommandId 로 재전송한다.
//
// 서버는 각 커맨드를 수신하는 즉시 실행하고 ACK 를 돌려준다. 문제는, 서버가
// 보낸 ACK 자체가 유실될 수도 있다는 점이다 — 이 경우 서버는 커맨드를 이미
// 정상적으로 실행했는데도 클라이언트는 ACK 를 받지 못해 재전송한다.
//
// 요구사항:
//   - 같은 CommandId 를 가진 커맨드가 재전송되더라도, 서버는 그 커맨드의
//     효과(아이템 소모, 스킬 쿨다운 소모, 데미지 적용 등)를 정확히 한 번만
//     적용해야 한다.
//   - 재전송에 대해서도 서버는 ACK 를 응답해야 한다(클라이언트가 재전송을
//     멈출 수 있도록).
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.

using System.Threading.Tasks;

public sealed class UseItemCommand
{
    public long   CommandId;
    public ulong  PlayerId;
    public int    ItemId;
}

public interface IInventory
{
    Task<bool> ConsumeItemAsync(ulong playerId, int itemId);
}

public interface IAckSender
{
    Task SendAckAsync(ulong playerId, long commandId);
}

public sealed class CommandHandler
{
    private readonly IInventory _inventory;
    private readonly IAckSender _ackSender;

    public CommandHandler(IInventory inventory, IAckSender ackSender)
    {
        _inventory = inventory;
        _ackSender = ackSender;
    }

    // 클라이언트로부터 커맨드를 수신할 때마다 호출된다.
    // 같은 CommandId 로 여러 번 호출될 수 있다(재전송).
    public async Task OnUseItemCommandAsync(UseItemCommand cmd)
    {
        // (A) 수신한 커맨드를 곧바로 실행한다 — 이전에 같은 CommandId 를
        //     처리한 적이 있는지는 확인하지 않는다.
        bool consumed = await _inventory.ConsumeItemAsync(cmd.PlayerId, cmd.ItemId);

        if (consumed)
        {
            // 아이템 사용 효과 적용 (데미지 계산, 버프 부여 등, 구현 생략)
            ApplyItemEffect(cmd.PlayerId, cmd.ItemId);
        }

        // (B) 실행 결과와 무관하게 ACK 는 항상 돌려준다.
        await _ackSender.SendAckAsync(cmd.PlayerId, cmd.CommandId);
    }

    private void ApplyItemEffect(ulong playerId, int itemId)
    {
        // 아이템 효과 적용 (구현 생략)
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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