
학습 개요
TRPG 프로젝트에서 플레이어 간의 상호작용을 극대화하기 위해 아이템 및 재화 거래 시스템을 개발했다. 거래 시스템을 개발할 때 단순히 데이터를 주고받는 것을 넘어, 네트워크 환경에서 발생할 수 있는 아이템 복사 버그와 데이터 동기화 문제를 해결하는 데 초점을 맞췄다.
시스템 아키텍처 및 데이터 모델
네트워크 상에서 거래되는 모든 정보는 단일 구조체로 패키징되어 이동해야 한다. 본 프로젝트에서는 컴파일러에 의한 메모리 패딩(Padding)으로 패킷 크기가 어긋나는 것을 막기 위해 #pragma pack(push, 1)을 사용했다.
📌 TradeInfo 구조체 (Packet.h)
거래의 '누가, 누구에게, 무엇을 주고받는지'에 대한 명세서
#pragma pack(push, 1)
struct TradeInfo
{
uint32_t tradeId; // 방장(서버)이 부여하는 거래 고유 번호
char sender[32]; // 거래를 신청한 사람 이름
char receiver[32]; // 거래를 받을 사람 이름
// [주는 아이템 정보]
char itemGiveName[32];
int itemGiveType; // 0: HP_POTION, 1: ATTACK_BUFF, 2: MONSTER_PART
int itemGiveValue;
int itemGivePrice;
int itemGiveCount; // 몇 개 줄 것인지
// [받고 싶은 아이템 정보]
char itemReceiveName[32];
int itemReceiveType; // 0: HP_POTION, 1: ATTACK_BUFF, 2: MONSTER_PART
int itemReceiveValue;
int itemReceivePrice;
int itemReceiveCount; // 몇 개 받을 것인지
// 거래 상태 (0: 대기 중, 1: 수락됨, 2: 거절됨, 3: 취소됨)
uint8_t status;
};
#pragma pack(pop)
핵심 통신 프로토콜: 3-Way Handshake 방식
아이템 거래는 민감한 데이터 이동이므로, 클라이언트 간 직접 통신이 아닌 서버(방장) 중심의 검증 통신으로 설계했다.
통신 흐름은 크게 3단계로 이루어진다.
Step 1. 거래 신청 (Client -> Server)
- 패킷: PKT_C2S_TRADE_REQUEST
- 동작: 신청자가 UI에 / 구분자로 신청서([받을사람]/[줄아이템]/[받을아이템])를 작성하면, 로컬 인벤토리 검증을 거친 후 서버로 패킷을 전송
// ItemTradeRequestState.cpp 일부
// 유효성 검사
// (1) 줄 아이템이 내 인벤토리에 있는지 확인
auto* giveSlot = player->GetInventory().GetItemSlot(giveItemName);
if (!giveSlot || giveSlot->count <= 0)
{
Renderer::DisplayUITimed(UIPart::CenterLeft, 10, "해당 아이템을 보유하고 있지 않습니다!", 2.0f);
return;
}
...
// 3. 모든 검사 통과 시 패킷 생성 및 전송
Pkt_TradeRequest pkt;
// 신청자 정보
strcpy_s(pkt.info.sender, Client::playerName.c_str());
strcpy_s(pkt.info.receiver, receiverName.c_str());
... (아이템 정보 복사) ...
// 패킷 전송
NetworkManager::GetInstance().SendTradeRequest(pkt);
Step 2. 서버의 등록 및 전파 (Server -> All)
- 서버는 요청을 받으면 Server_HandleRequest를 호출
- 가장 중요한 점은 서버가 직접 고유한 tradeId를 부여한다는 것
// TradeManager.cpp
// [서버 전용] 방장이 클라이언트의 거래 신청을 받았을 때
void TradeManager::Server_HandleRequest(const TradeInfo & info)
{
if (!Client::isServer) return;
TradeInfo newInfo = info;
newInfo.tradeId = nextTradeId++; // 서버가 고유 ID 부여
newInfo.status = 0; // Pending
{
std::lock_guard<std::mutex> lock(tradeMutex);
tradeList.push_back(newInfo);
}
// 모든 플레이어에게 "새 거래 생겼다"고 전파
Pkt_TradeSync syncPkt;
syncPkt.info = newInfo;
// NetworkManager를 통해 브로드캐스트 (이 함수는 나중에 구현)
NetworkManager::GetInstance().BroadcastTradeSync(syncPkt);
IPCManager::GetInstance().SendLog("[서버] 새로운 거래 신청 등록 (ID: " + std::to_string(newInfo.tradeId) + ")");
}
Step 3. 응답 및 최종 동기화 (Client -> Server -> All)
- 수신자가 제안을 보고 수락하면 PKT_C2S_TRADE_RESPONSE를 서버로 보낸다.
- 서버는 해당 tradeId의 거래 상태를 1 (수락됨)로 변경하고 다시 BroadcastTradeSync를 호출하여 전원에게 알린다.
Troubleshooting: 아이템 복사 방지를 위한 동기화 로직
이 시스템을 개발하며 가장 까다로웠던 부분이 "어느 시점에 아이템을 실제로 인벤토리에서 넣고 뺄 것인가?"였다. 단순히 SYNC 패킷을 받았을 때 아이템을 지급하면, 네트워크 문제로 패킷을 2번 받거나 UI가 리프레시될 때 아이템이 복사되는 최악의 버그가 발생한다.
이를 해결하기 위해 TradeManager::SyncTrade에서 상태 전이(State Transition) 기법을 활용했다.
📌 멱등성을 보장하는 SyncTrade 로직
// TradeManager.cpp
// [공용] 목록 동기화 (서버가 보낸 정보를 내 로컬 리스트에 반영)
void TradeManager::SyncTrade(const TradeInfo & info)
{
std::lock_guard<std::mutex> lock(tradeMutex);
// 이미 있는 거래면 업데이트, 없으면 추가
auto it = std::find_if(tradeList.begin(), tradeList.end(), [&](const TradeInfo& t)
{
return t.tradeId == info.tradeId;
});
if (it != tradeList.end())
{
if (it->status == 0 && info.status == 1)
{
ApplyRealItemTrade(info); // 실제 아이템 이동 로직 호출
}
* it = info;
}
else
{
tradeList.push_back(info);
// 만약 서버에서 처음부터 성공 상태로 보냈다면
if (info.status == 1) ApplyRealItemTrade(info);
}
}
- 해결 원리: 서버로부터 수락됨(status=1) 패킷을 여러 번 받더라도, 첫 번째 수신 시 로컬 리스트의 상태가 1로 변경되므로 두 번째부터는 if (it->status == 0 && info.status == 1) 조건을 절대 만족할 수 없다.
실제 아이템 교환 로직 (ApplyRealItemTrade)
실제 아이템 교환 시에는 자신이 신청자인지 수신자인지에 따라 로직이 반대로 적용되도록 분기 처리를 구현했다.
// TradeManager.cpp
void TradeManager::ApplyRealItemTrade(const TradeInfo & info)
{
Player * myPlayer = GameManager::GetInstance().GetPlayer();
if (!myPlayer) return;
// 내가 신청자(A)인 경우: 내 아이템(Give)을 주고 상대 아이템(Receive)을 받음
if (std::string(info.sender) == Client::playerName)
{
myPlayer->GetInventory().UseItem(nullptr, info.itemGiveName, info.itemGiveCount);
// 아이템 생성
Item* newItem = new Item(info.itemReceiveName, (ItemType)info.itemReceiveType, info.itemReceiveValue, info.itemReceivePrice);
myPlayer->GetInventory().AddItem(newItem, info.itemReceiveCount);
IPCManager::GetInstance().SendLog("[거래] '" + std::string(info.receiver) + "'님과의 거래 성사! 아이템이 교환되었습니다.");
}
// 내가 수락자(B)인 경우: 내 아이템(Receive)을 주고 상대 아이템(Give)을 받음
else if (std::string(info.receiver) == Client::playerName)
{
myPlayer->GetInventory().UseItem(nullptr, info.itemReceiveName, info.itemReceiveCount);
Item * newItem = new Item(info.itemGiveName, static_cast<ItemType>(info.itemGiveType), info.itemGiveValue, info.itemGivePrice);
myPlayer->GetInventory().AddItem(newItem, info.itemGiveCount);
IPCManager::GetInstance().SendLog("[거래] '" + std::string(info.sender) + "'님과의 거래 성사! 아이템이 교환되었습니다.");
}
}
Troubleshooting: 호스트(방장) 동기화 누락 이슈 해결
- 이슈 발생: P2P 호스트-클라이언트 구조이다 보니, 방장이 거래를 수락했을 때 클라이언트는 BroadcastTradeSync를 받아 정상적으로 아이템이 들어오지만, 정작 방장 본인의 인벤토리는 변하지 않는 문제가 있었다. 방장은 브로드캐스트 패킷을 보내기만 하고 자신은 수신 처리를 타지 않았기 때문이다.
- 해결: 서버의 응답 처리 함수 마지막에 자기 자신의 SyncTrade를 직접 호출하도록 로직을 추가
// TradeManager.cpp
void TradeManager::Server_HandleResponse(uint32_t tradeId, uint8_t response)
{
// ... (상태 변경 및 로깅 로직) ...
Pkt_TradeSync syncPkt;
syncPkt.info = updatedInfo;
NetworkManager::GetInstance().BroadcastTradeSync(syncPkt);
SyncTrade(updatedInfo);
}
이를 통해 서버 역할을 수행하는 방장도 일반 플레이어와 동일한 사이클로 렌더링되도록 문제를 해결했다.
회고
이번 거래 시스템을 구현하면서 가장 크게 배운 점은 네트워크 환경에서는 클라이언트의 상태를 절대 신뢰하면 안 된다는 것이다.
- 클라이언트 UI에서 입력을 필터링하더라도(예: 인벤토리에 없는 아이템 신청 불가),
- 결국 최종 승인과 ID 발급은 중앙(서버/방장)에서 처리하고,
- 상태의 전이(State Transition) 과정에서만 실제 중요한 데이터 조작(아이템 추가/삭제)을 수행해야 무결성이 보장된다는 것을 실감했다.
향후에는 거래 완료 시 파일이나 DB에 로그를 기록하여 혹시 모를 부정 거래를 어드민이 추적할 수 있는 시스템을 추가해보고 싶다.
'내배캠 > CH2 팀프로젝트(감자가생각났어요)' 카테고리의 다른 글
| [TIL: 2026.06.04] TRPG 프로젝트 트러블 슈팅(3) (0) | 2026.06.03 |
|---|---|
| [TIL: 2026.06.03] TRPG 프로젝트 진행 시 담당했던 파트 코드 분석 회고(2) (0) | 2026.06.03 |
| [TIL: 2026.06.02] TRPG 프로젝트 분석 및 회고(1) (1) | 2026.06.02 |
| [TIL: 2026.05.29] TRPG 팀프로젝트 종료 회고록 (0) | 2026.06.01 |
| [TIL: 2026.05.22] C++ TRPG 인벤토리 구조 리팩토링 및 렌더러 기반 상점 UI 구축 트러블슈팅 (0) | 2026.06.01 |