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++
// ============================================================================
// 시나리오 (서버-클라)
// ----------------------------------------------------------------------------
// 클라이언트는 서버에 커맨드(아이템 사용, 스킬 시전 등)를 보낸 뒤 ACK 를
// 기다린다. 일정 시간 안에 ACK 를 받지 못하면 클라이언트는 같은 커맨드를
// 동일한 CommandId 로 재전송한다.
//
// 서버는 각 커맨드를 수신하는 즉시 실행하고 ACK 를 돌려준다. 서버가 보낸
// ACK 자체가 네트워크에서 유실될 수도 있다 — 이 경우 서버는 커맨드를 이미
// 정상 실행했는데도 클라이언트는 ACK 를 받지 못해 재전송한다.
//
// 요구사항:
// - 같은 CommandId 를 가진 커맨드가 재전송되더라도, 서버는 그 효과를
// 정확히 한 번만 적용해야 한다.
// - 재전송에 대해서도 서버는 ACK 를 응답해야 한다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <cstdint>
#include <string>
struct UseItemCommand
{
int64_t commandId;
uint64_t playerId;
int itemId;
};
class Inventory
{
public:
bool ConsumeItem(uint64_t playerId, int itemId)
{
// 아이템 소모 처리 (구현 생략, 성공 시 true)
return true;
}
};
class AckSender
{
public:
void SendAck(uint64_t playerId, int64_t commandId)
{
// ACK 패킷 전송 (구현 생략)
}
};
class CommandHandler
{
public:
CommandHandler(Inventory* inventory, AckSender* ackSender)
: inventory_(inventory), ackSender_(ackSender) {}
// 네트워크 수신 스레드에서, 커맨드를 수신할 때마다 호출된다.
// 같은 commandId 로 여러 번 호출될 수 있다(재전송).
void OnUseItemCommand(const UseItemCommand& cmd)
{
// (A) 수신한 커맨드를 곧바로 실행한다 — 이전에 같은 commandId 를
// 처리한 적이 있는지는 확인하지 않는다.
bool consumed = inventory_->ConsumeItem(cmd.playerId, cmd.itemId);
if (consumed)
{
ApplyItemEffect(cmd.playerId, cmd.itemId);
}
// (B) 실행 결과와 무관하게 ACK 는 항상 돌려준다.
ackSender_->SendAck(cmd.playerId, cmd.commandId);
}
private:
void ApplyItemEffect(uint64_t playerId, int itemId)
{
// 아이템 효과 적용 (구현 생략)
}
Inventory* inventory_;
AckSender* ackSender_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.