오늘은 C++ 텍스트 콘솔 RPG 프로젝트에서
아이템, 인벤토리, 캐릭터, 효과 처리 구조를 전반적으로 정리하고 리팩터링했다.
단순 구현이 아니라, SOLID 원칙을 최대한 지키는 구조를 목표로 설계를 다시 잡았다.
1️⃣ Item 설계 리팩터링 – Effect 중심 구조
기존에는:
회복 / 버프 / 장비 아이템이 서로 다른 방식으로 효과를 가질 가능성이 있었고
장비 전용 StatBonus 같은 구조가 따로 존재해 확장 시 복잡해질 여지가 있었다.
개선 방향
모든 아이템 효과를 Effect 하나로 통합
Item은 효과 데이터를 제공만 하고, 실제 적용은 다른 책임으로 분리
핵심 코드
enum class EffectType {
HealFlat,
AddStatFlat,
InvincibleHits
};
enum class StatType {
Attack,
MaxHealth
};
struct Effect {
EffectType type;
StatType stat; // AddStatFlat일 때만 의미
int value; // 회복량 / 증감량 / 무적 횟수
int duration; // 지속 턴 (0이면 즉시)
};
✔ 아이템 종류가 늘어나도
→ Effect 조합만 추가하면 확장 가능
2️⃣ EffectManager 도입 – 효과 처리 책임 분리
문제점
Inventory 내부에서 캐릭터 스탯을 직접 수정하면
SRP 위반
Effect가 늘어날수록 Inventory가 비대해질 위험
해결
EffectManager 클래스를 별도로 분리
Effect 해석과 캐릭터 스탯 반영을 전담
핵심 코드 요약
class EffectManager {
public:
static void apply(Character& c, const Effect& e, int sign);
static void applyAll(Character& c,
const std::vector<Effect>& effects,
int sign);
};
// 적용 / 원복 공용 처리
EffectManager::applyAll(character, item->getEffects(), +1);
EffectManager::applyAll(character, item->getEffects(), -1);
sign = +1 → 장착/사용
sign = -1 → 해제/원복
✔ 장착/해제 로직을 대칭 구조로 처리
✔ Effect 추가 시 Inventory 수정 최소화
3️⃣ Inventory 역할 정리 및 헤더/CPP 분리
Inventory는 다음 책임만 갖도록 정리했다.
Inventory 책임
아이템 보관
소비 아이템 사용
장비 아이템 장착 / 해제
Shop과의 구매 / 판매 중개
Character는 포인터로 참조만 받고 소유하지 않는다.
Inventory.h (선언만)
class Item;
class Character;
class Inventory {
public:
Inventory();
~Inventory();
void add(Item* item);
void useItem(Character* c, int index);
void equipItem(Character* c, int index, EquipSlot slot);
void unequipItem(Character* c, EquipSlot slot);
private:
std::vector<Item*> items_;
std::vector<Item*> equipped_;
};
Inventory.cpp (구현)
void Inventory::useItem(Character* c, int index) {
Item* item = items_[index];
EffectManager::applyAll(*c, item->getEffects(), +1);
delete item;
items_.erase(items_.begin() + index);
}
void Inventory::equipItem(Character* c, int index, EquipSlot slot) {
int slotIdx = (slot == EquipSlot::Weapon) ? 0 : 1;
if (equipped_[slotIdx]) {
EffectManager::applyAll(*c, equipped_[slotIdx]->getEffects(), -1);
items_.push_back(equipped_[slotIdx]);
}
equipped_[slotIdx] = items_[index];
items_.erase(items_.begin() + index);
EffectManager::applyAll(*c, equipped_[slotIdx]->getEffects(), +1);
}
✔ 역할이 명확해지고
✔ 컴파일 의존성과 협업 충돌 가능성 감소
4️⃣ Character 책임 재정의
Character는:
스탯 보관
게임 규칙을 포함한 “행동 API”만 담당
핵심 API 요약
void heal(int amount);
void addAttack(int delta);
void addMaxHealth(int delta);
void addGold(int delta);
void takeDamage(int dmg);
void tickEffects(); // 턴 종료 시 호출
✔ 직접 setter 사용 ❌
✔ 상태 변경 규칙을 Character 내부에 캡슐화
5️⃣ InvincibleHits / duration 처리 방향
InvincibleHits
기본 설계: 아이템 사용 즉시 활성화
duration > 0
timedEffects_에 등록
턴 종료 시 tickEffects()에서 감소 및 만료 처리
struct TimedEffect {
Effect effect;
int remaining;
};
“다음 턴부터 적용” 정책이 필요하면
→ pending 상태를 두는 방식으로 확장 가능하도록 설계만 잡아둠
6️⃣ Shop 연동 규칙 고려 사항 (반영 X)
메모리 소유권 문제를 명확히 정리
Shop은 아이템 프로토타입만 보관
구매 시:
Item* buyItem(int index) { return items[index]->clone(); }
Inventory가 소유권 관리(delete 책임)
판매 시:
아이템 delete
금액만 반환
✔ 댕글링 포인터 / 중복 delete 방지
7️⃣ SOLID 관점 정리
이번 리팩터링 결과:
SRP
Character / Inventory / EffectManager 책임 분리
OCP
Effect 추가로 기능 확장 가능
LSP
Item 파생 클래스 치환 가능
ISP
Item 인터페이스 최소화
DIP
Effect 처리 로직 분리로 의존성 완화