아이템 삭제 시스템 - 분산된 스택을 하나의 규칙으로 수렴시키는 구조
1. 시스템 요구 사항
아이템 추가 시스템이 안정적으로 동작하더라도, 인벤토리 시스템은 여전히 불완전한 상태였다.
실제 게임 플레이에서는 아이템을 획득하는 것보다 소비하거나 제거하는 경우가 훨씬 더 빈번하게 발생한다.
포션 사용, 제작 재료 소모, 퀘스트 제출, 상점 판매 등 다양한 상황에서 인벤토리의 특정 아이템을 지정한 개수만큼 감소시켜야 했다.
문제는 스택형 인벤토리 구조에서는 하나의 아이템이 여러 슬롯에 나뉘어 존재할 수 있다는 점이었다.
이 상태에서 아이템 X를 5개 제거하라는 요청이 들어오면, 어느 슬롯에서 몇 개를 제거할 것인지에 대한 규칙이 명확하지 않으면 데이터가 쉽게 어긋난다.
일부 슬롯에서만 줄어들고 나머지는 그대로 남거나, 플레이어가 인지하지 못한 슬롯에서 아이템이 사라지는 상황은 인벤토리 신뢰도를 크게 떨어뜨린다.
또한 상위 시스템에서는 몇 개를 제거할지만 알고 싶을 뿐, 그 아이템이 인벤토리 내부에서 몇 개의 슬롯에 어떻게 분산되어 있는지는 알 필요가 없었다.
삭제 로직이 UI나 개별 슬롯 단위로 흩어질 경우, 동일한 규칙을 여러 곳에서 중복 구현해야 하고, 규칙 변경 시 유지보수 비용이 급격히 증가할 위험이 있었다.
따라서 아이템 삭제 시스템에서는 분산된 스택을 하나의 연속된 수량으로 취급하고, 인벤토리 전체를 대상으로 일관된 규칙에 따라 수량을 감소시키는 구조가 필요했다.
이 과정 역시 아이템 추가 시스템과 마찬가지로, 중간 상태 없이 예측 가능한 결과만을 남기는 방식으로 설계되어야 했다.
2. 설계 목표
- 인벤토리 전체를 기준으로 특정 아이템을 n개만큼 감소시킬 수 있을 것
- 여러 슬롯에 분산된 동일 아이템 스택을 자연스럽게 합산해 처리할 것
- 상위 시스템은 슬롯 구조를 알 필요 없이 아이템과 수량만 전달할 수 있을 것
- 슬롯 데이터가 남거나 음수가 되는 비정상 상태를 구조적으로 방지할 것
3. 흐름도
[아이템 제거 요청]
↓
[인벤토리 전체 슬롯 순회]
↓
[지정 아이템 발견]
↓
[현재 슬롯 수량과 남은 제거 수량 비교]
↓
[슬롯 비우기 또는 부분 감소]
↓
[남은 제거 수량 갱신]
↓
[제거 수량이 0이 될 때까지 반복]
이 흐름에서 중요한 점은, 특정 슬롯을 목표로 제거하지 않고 인벤토리 전체를 하나의 연속된 수량 풀로 취급한다는 점이다.
이를 통해 아이템이 어떤 슬롯에 얼마나 분산되어 있는지와 무관하게, 항상 동일한 제거 결과를 보장할 수 있다.
4. 구현
4.1. 인벤토리 전체 기준 아이템 제거 로직
public void RemoveItem(InventoryItemData data, int count)
{
if (data == null || count <= 0) return;
int remaining = count;
// 뒤에서부터 제거 (최근 들어온 것부터 빠지는 느낌)
for (int i = slots.Count - 1; i >= 0 && remaining > 0; i--)
{
var slot = slots[i];
if (slot.IsEmpty) continue;
if (slot.data != data) continue;
if (slot.count > remaining)
{
slot.count -= remaining;
return;
}
else if (slot.count == remaining)
{
slot.Clear();
return;
}
else
{
remaining -= slot.count;
slot.Clear();
}
}
}
RemoveItem 함수는 인벤토리 전체에서 특정 아이템을 지정된 수량만큼 감소시키는 책임을 가진 함수다.
이 함수 역시 아이템 추가 시스템과 마찬가지로, 슬롯 단위가 아닌 인벤토리 전체 관점에서 동작하도록 설계되었다.
이 함수는 요청 수량보다 실제 보유 수량이 적은 경우에도 예외를 발생시키지 않으며, 가능한 범위 내에서만 제거를 수행한다.
이는 상위 시스템에서 사전 검증이 이루어진다는 전제를 바탕으로 한 설계 선택이다.
인벤토리 내부에서는 어떻게 제거할 것인가에만 집중하고, 제거 가능한가에 대한 판단은 상위 계층에서 책임지도록 역할을 분리했다.
함수의 진입부에서는 데이터와 수량에 대한 기본적인 방어 검사를 수행한다.
제거할 아이템이 없거나 제거 수량이 0 이하인 경우에는, 불필요한 연산과 예외 가능성을 사전에 차단하기 위해 즉시 반환하도록 구성했다.
이 로직의 핵심은 remaining 변수다.
remaining은 아직 제거해야 할 수량을 의미하며, 인벤토리 슬롯을 순회하면서 점진적으로 감소한다.
슬롯 순회는 리스트의 뒤에서부터 수행되는데, 이는 플레이어 체감상 나중에 들어온 아이템부터 먼저 빠지는 자연스러운 흐름을 제공하기 위한 선택이다.
유일한 방법은 아니지만, 예측 가능성과 직관성을 기준으로 판단했다.
이 순회 방향은 정책 변경에 따라 쉽게 수정 가능하며, 획득 시점 메타데이터나 정렬 기준이 추가될 경우 다른 제거 전략으로 확장할 수 있다.
각 슬롯을 검사할 때는 해당 슬롯이 비어 있지 않고, 제거 대상 아이템과 동일한 아이템을 담고 있는 경우에만 처리 대상이 된다.
이후 현재 슬롯의 수량과 remaining 값을 비교해 세 가지 경우로 나누어 처리한다.
슬롯의 수량이 remaining보다 많은 경우에는, 해당 슬롯에서 필요한 만큼만 감소시키고 즉시 함수를 종료한다.
이 즉시 종료 구조를 통해, 제거 요청이 완전히 충족되었으면 불필요한 슬롯 순회를 방지하고 제거 요청이 충족된 이후에는 데이터 접근을 최소화하도록 했다.
슬롯의 수량이 remaining과 정확히 같은 경우에는 슬롯을 비우고 종료한다.
반대로 슬롯의 수량이 remaining보다 적은 경우에는, 슬롯을 완전히 비운 뒤 remaining에서 그 수량만큼을 차감하고 다음 슬롯으로 이동한다.
이 구조를 통해 여러 슬롯에 나뉘어 있는 동일 아이템도 하나의 연속된 수량처럼 자연스럽게 제거되며, 제거 이후 어떤 슬롯에도 음수 수량이나 잘못된 데이터가 남지 않도록 보장된다.
또한 슬롯 단위 로직이 외부로 노출되지 않기 때문에, 인벤토리 내부 구조 변경 시에도 상위 시스템에 영향을 주지 않는다.
5. 개발 의도
이 아이템 삭제 시스템에서 가장 중요하게 생각한 점은, 상위 시스템이 인벤토리 내부 구조를 전혀 알 필요가 없도록 만드는 것이었다.
퀘스트 시스템이나 제작 시스템, 상점 시스템은 이 아이템을 몇 개 소모한다는 사실만 알면 충분하며, 그 아이템이 인벤토리 내부에서 몇 개의 슬롯에 어떻게 분산되어 있는지는 인벤토리 시스템의 책임이어야 한다.
또한 삭제 과정에서 슬롯 단위로 로직이 분산되지 않도록, 모든 규칙을 InventoryManager 한 곳에 집중시켰다.
이 구조를 통해 아이템 삭제 규칙이 변경되더라도, UI나 개별 슬롯 로직을 수정할 필요 없이 데이터 레이어만 수정하면 되도록 설계했다.
결과적으로 이 아이템 삭제 시스템은 단순히 수량을 줄이는 기능이 아니라, 스택형 인벤토리 구조에서 데이터 일관성을 유지하기 위한 핵심 처리 단계로 작동한다.
이는 이후 스택 병합, 분할, 자동 정렬 같은 기능으로 확장될 때도 동일한 철학을 유지할 수 있는 기반이 되었다.
