오늘의 할 일 |
프로젝트 회고 할 일 |
|
|
TRPG 프로젝트 분석
시스템 아키텍처: 확장성에 초점을 둔 감자가 생각났어요

1. State Pattern 기반 흐름 제어
게임을 만들다 보면 유저의 행동에 따라 화면과 로직이 바뀌는 걸 확인할 수 있다. 만약 이 모든 과정을 하나의 코드로 관리한다면 어떤 문제가 발생할까?
if-else문의 무분별한 사용으로 인해 코드는 금세 스파게티처럼 꼬이고, 버그 하나를 고치려다가 다른 기능까지 망가뜨리는 대참사가 발생할 수 있다.
감자가 생각났어요에서는 이러한 문제를 상태 패턴을 이용하여 해결했다.
상태 패턴을 스마트폰에 비유해 쉽게 이해하기 위해 GameManager와 IGameState를 스마트폰 기기와 개별 앱에 대응시켜 생각해보자.
GameManager: 스마트폰 기기
IGameState: 개별 앱
스마트폰 기기는 기본 틀일 뿐 기기 스스로 카카오톡 메시지를 보내거나 유튜브 영상을 재생하는 등의 행위를 하지 않는다. 이러한 행위는 개별 앱에서 진행하게 되는데, 이를 프로젝트에 그대로 적용하여 GameManager는 직접 무언가를 하지 않고, 실제 행동은 IGameState가 하도록 했다.
GameManager와 IGameState 코드:
// GameManager.cpp의 일부
/*
* @brief: 현재 게임 상태 설정
* @param newState: 새로운 게임 상태
*/
void GameManager::SetCurrentState(IGameState* newState)
{
// 1. 기존 상태가 존재한다면 파괴하기 전에 Exit를 먼저 호출
if (CURRENT_STATE != nullptr)
{
CURRENT_STATE->Exit();
delete CURRENT_STATE;
}
// 2. 새로운 상태로 교체
CURRENT_STATE = newState;
// 3. 새 상태가 정상적으로 할당되었다면 Enter 호출
if (CURRENT_STATE != nullptr)
{
CURRENT_STATE->Enter();
}
}
/*
* @brief: 현재 게임 상태 가져오기
* @return: 현재 게임 상태
*/
IGameState* GameManager::GetCurrentState() {
return CURRENT_STATE;
}
#pragma once
#include <string>
class IGameState {
public:
virtual ~IGameState() = default;
/*
* @brief: 상태에 진입할 때 딱 한 번 호출되는 함수
*/
virtual void Enter();
/*
* @brief: 매 프레임마다 한 번 실행되는 함수
* @param ch: 전환할 상태 인덱스(ENUM으로 관리)
* @param lastCommand: 사용자가 입력한 마지막 명령어 문자열
*/
virtual void Update(int ch, std::string& lastCommand) = 0;
/*
* @brief: 상태를 빠져나갈 때 딱 한 번 호출되는 함수
*/
virtual void Exit();
};
상태 패턴을 적용했을 때의 장점은 다음과 같다.
- 유지보수와 협업의 편리함
- 각 기능들이 독립적인 클래스로 나누어져 있기 때문에 팀원들이 각자 맡은 부분을 충돌 없이 동시에 개발할 수 있었다.
- 기존 코드를 훼손하지 않으면서 새로운 기능 추가
- 상태 패턴을 이용하여 개방-폐쇄 원칙에 따른 설계가 가능했다.
2. Listen Server 네트워크 모델
멀티플레이어 게임을 만들 때 서버 구조는 크게 세 가지로 나뉜다.
- 데디케이티드 서버(Dedicated Server): 게임 로직 연산과 동기화만을 전담하는 별도의 서버 컴퓨터를 두는 구조
-
리슨 서버(Listen Server): 플레이어 중 한 명(호스트)의 기기에서 클라이언트와 서버 역할을 동시에 수행하는 구조
-
P2P(Peer-to-Peer): 중앙 서버 없이 모든 클라이언트가 서로 직접 연결되어 통신하는 구조
우리는 세 가지 방법 중 가볍고 빠른 구축이 가능한 리슨 서버를 채택하여 별도의 서버 컴퓨터 없이 방장이 서버 역할을 하는 방향으로 개발을 진행했다.
플레이어가 게임을 켜고 '방 만들기'를 누르는 순간, 프로그램 내부의 isServer라는 플래그가 올라간다.
- 방장(isServer == ture): 클라이언트의 역할을 수행하면서, 동시에 다른 클라이언트들의 네트워크 연결을 수락하고 행동 결과를 계산하여 공지하는 서버 역할을 수행한다.
- 일반 참가자(isServer == false): 서버 로직은 무시한다.
즉, 방장은 클라이언트로서의 역할도 수행하지만, isServer == true라는 플래그를 달고 서버로서의 역할도 동시에 수행하게 된 것이다.
이러한 방식은 비용이 발생하지 않고 개발 속도가 빠르다는 장점이 있지만, 방장이 게임을 끄거나 인터넷 연결이 끊어지면 그 방의 모든 사람이 게임에서 튕긴다는 문제가 발생한다. 또한, 방장의 경우 네트워크 지연성이 0이기 때문에 일반 참가자보다 유리한 조건에서 게임을 플레이할 수 있다.
이 외에도 개발을 진행하면서 발생한 방장이 자신에게 패킷을 보내지 않기 때문에 아이템 거래 시 방장의 인벤토리가 갱신되지 않는 문제가 발생했는데, 이 부분에 대한 자세한 내용은 트러블 슈팅에서 작성하고자 한다.
아이템 거래소 관련 함수로 보는 isServer 플래그 - NetworkManager.cpp 코드 일부:
/*
* @brief: 아이템 거래 요청 함수(방장-직접 처리/일반 참가자-서버로 패킷 전송)
* @param pkt: 전송할 거래 요청 패킷
*/
void NetworkManager::SendTradeRequest(const Pkt_TradeRequest& pkt)
{
if (Client::isServer)
{
// 내가 방장인데 내가 신청하는 경우: 스스로에게 바로 처리
TradeManager::GetInstance().Server_HandleRequest(pkt.info);
}
else if (clientSocket != INVALID_SOCKET)
{
// 게스트인 경우: 서버로 전송
send(clientSocket, reinterpret_cast<const char*>(&pkt), pkt.header.size, 0);
}
}
/*
* @brief: 받은 거래 요청에 대한 응답을 전송하는 함수(방장-로컬에서 최종 판정/일반 참가자-서버로 결과 전송)
* @param pkt: 전송할 거래 응답 패킷
*/
void NetworkManager::SendTradeResponse(const Pkt_TradeResponse& pkt)
{
if (Client::isServer)
{
TradeManager::GetInstance().Server_HandleResponse(pkt.tradeId, pkt.response);
}
else if (clientSocket != INVALID_SOCKET)
{
send(clientSocket, reinterpret_cast<const char*>(&pkt), pkt.header.size, 0);
}
}
/*
* @brief: 거래가 성사되었거나 상태가 변경되었을 때, 방장이 모든 유저에게 변경된 거래 정보를 동기화시키는 함수
* @param pkt: 브로드캐스트할 거래 동기화 패킷
*/
void NetworkManager::BroadcastTradeSync(const Pkt_TradeSync& pkt)
{
if (!Client::isServer) return; // 방장만 브로드캐스트 가능
// 모든 클라이언트에게 쏘기
std::lock_guard<std::mutex> lock(clientsMutex);
for (SOCKET clientSock : connectedClients)
{
send(clientSock, reinterpret_cast<const char*>(&pkt), pkt.header.size, 0);
}
// 방장 본인의 화면도 즉시 동기화
TradeManager::GetInstance().SyncTrade(pkt.info);
}
3. Manager Service
한 놈은 한 가지 일만 잘하게 만드는 것을 뜻하는 단일 책임 원칙(SRP)을 지키기 위해 State에서는 UI(화면 출력)과 입력을 진행하였고, 비지니스 로직이나 데이터를 건드리는 부분은 Manager를 통해 구현되게 하였다.
State: 사용자의 입력을 받는
- ...State로 끝나는 파일
- 역할: 콘솔 화면에 글자를 출력하고, 사용자가 누른 키보드 값을 읽는다.
- 행동: 사용자의 입력에 따라 GameManager 함수를 호출한다.
Manager: 핵심 로직을 처리하는
- ....Manager로 끝나는 파일
- 역할: 비지니스 로직 처리
- 특징: 전체에서 하나만 존재하는 싱글톤 패턴을 따르며, 어떤 State에서든 언제든지 호출하여 사용할 수 있다.
이렇게 코드를 분리하면 다음과 같은 장점이 있다.
- 코드 가독성이 좋다.
- 재사용이 쉽다.
- 버그 수정이 쉽다.
이 부분은 State Pattern 파트에서 코드를 이미 작성했기 때문에 코드 예시는 생략하려 한다~!!
4. 데이터 중심 모델
게임에서 가장 중요한 것 중 하나는 데이터의 상태이다.
감자가 생각났어요에서 데이터는 결국 공통된 데이터 모델(Player, Inventory...)을 수정된다.
공통된 데이터 모델 사용의 장점은 다음과 같다.
- 데이터 일관성
- 데이터 저장과 불러오기의 편리함
- 네트워크 동기화의 단순화
이 부분에 대해서는 간단히 장점만 정리하고 마무리하려 한다.
TRPG 프로젝트 분석 결과 및 회고
우리 프로젝트는 핀볼을 통해 팀원 중 한 분의 코드를 베이스로 진행하였다.
분석하면서 생각한 거지만,, 내 코드가 뽑히지 않아서 정말 다행이다.(처참했을 거 같다..)
다른 분의 코드 베이스에서 작업을 진행한다는 건, 그 분의 코드를 이해하고 응용할 수 있는 능력이 필요한 일인데 프로젝트 진행 기간이 짧았어서 완벽히 이해가 된 상태로 기능을 구현한 건 아니었다.
이 때문에 아쉬움이 많이 남았었는데, 프로젝트 분석 회고를 진행하며 전체적인 구조와 코드 설계 방식에 대해 이해할 수 있었고 OPP의 중요성을 다시 한 번 느꼈다.
TextRPG였기 때문에 새로운 걸 구현해보는 경험을 쌓기는 쉽지 않을 거라고 생각했는데, 이렇게까지 배울 점이 많은 프로젝트를 하게될 줄 몰랐고 역시 어떤 프로젝트던 도움이 안되는 프로젝트는 없는 거 같다.
상태 패턴을 활용하여 컨텐츠 간 결합도를 낮추고, 리슨 서버 모델을 구현하여 멀티플레이 환경에서 데이터 동기화 문제 해결한 경험을 바탕으로 앞으로도 OPP에 맞는 개발을 해봐야지!
Git Repository: https://github.com/NBC-TRPG6/TRPG6
'내배캠 > CH2 팀프로젝트(감자가생각났어요)' 카테고리의 다른 글
| [TIL: 2026.06.04] TRPG 프로젝트 트러블 슈팅(3) (0) | 2026.06.03 |
|---|---|
| [TIL: 2026.06.03] TRPG 프로젝트 진행 시 담당했던 파트 코드 분석 회고(2) (0) | 2026.06.03 |
| [TIL: 2026.05.29] TRPG 팀프로젝트 종료 회고록 (0) | 2026.06.01 |
| [TIL: 2026.05.28] C++ TRPG 프로젝트: 서버 검증 기반 1:1 아이템 거래 시스템 구현 (0) | 2026.06.01 |
| [TIL: 2026.05.22] C++ TRPG 인벤토리 구조 리팩토링 및 렌더러 기반 상점 UI 구축 트러블슈팅 (0) | 2026.06.01 |