본문 바로가기

C# 콘솔에서 세이브/로드 시 참조가 깨지는 문제 (with Guid)

@코야딩구2025. 7. 18. 20:30

- 게임을 만들다 보면 한 객체를 여러 곳에서 참조하는 구조가 자주 등장한다.

- 특히 장착 아이템인벤토리가 같은 아이템 객체를 공유하는 구조에서는, 이 참조가 세이브/로드 과정에서 깨지는 문제가 발생할 수 있다.

- 이번 포스트에서는 이 문제를 어떻게 Guid를 기반으로 해결했는지 기록해보려고 한다.

💥 문제 상황: 저장 후 장착 아이템 참조가 풀려버린다?

- C# Text RPG 게임에서 Player는 다음 두 개의 아이템 저장소를 가지고 있다:

  • Inventory: 현재 보유 중인 아이템 리스트
  • EquippedItems: 실제로 착용 중인 장비 (무기, 방어구 등)

- 이때 EquippedItems는 Inventory에 존재하는 아이템을 참조하는 구조였다.
예를 들어 무기를 장착하면 해당 무기 객체는 인벤토리와 장비 슬롯에서 동일한 인스턴스로 존재했다.

- 그런데 JSON으로 저장한 후 다시 불러오면, 장비 슬롯이 참조하던 객체는 더 이상 Inventory에 있는 아이템이 아니었다.\

// 저장 전: 참조 공유 상태
Inventory[0] == Equipments[ItemType.Weapon]
// 저장 후 로드하면:
Inventory[0] != Equipments[ItemType.Weapon]  // 참조 깨짐

 

- JSON 역직렬화는 데이터를 "값"으로 복원하기 때문에, 객체 참조 관계가 사라진 것이다.

- 결과적으로 장비를 강화해도 인벤토리에는 반영되지 않고, 장비 해제 시 인벤토리와 따로 노는 현상이 발생했다.

✅ 해결 방법: 고유 ID(Guid)를 활용해 참조 복원하기

- 이 문제를 해결하기 위해, 아이템에 고유 식별자(Guid) 를 부여하고 장비창에는 참조 대신 ID만 저장하는 구조로 변경했다.
- 로드할 때는 인벤토리를 기준으로 ID → Item 매핑을 만든 뒤, 다시 참조를 연결해주는 방식이다.

- 저장 전: 장착 아이템의 참조 대신, ID만 따로 저장

📌 구조 요약

- Item 클래스에 Guid Id 필드를 추가

- Equipments는 여전히 Dictionary<ItemType, Item> 구조 유지

- 저장용으로 EquippedItemIds : Dictionary<ItemType, Guid> 를 따로 둠

- 저장 시: 장비창의 키와 Item.Id만 추출하여 EquippedItemIds에 저장

public void PrepareForSave()
{
    EquippedItemIds = Equipments.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Id);
}

- 로드 시: Inventory를 ID 기준으로 매핑, Equipments는 EquippedItemIds를 통해 다시 Inventory 아이템 참조

// 장비창 복원
var dict = Inventory.ToDictionary(item => item.Id, item => item);
// 기존 장비 창
Equipments = new Dictionary<Item.ItemType, Item>();

foreach (var kvp in EquippedItemIds)
{
	if (dict.TryGetValue(kvp.Value, out var item))
	{
		Equipments[kvp.Key] = item;
	}
}

- 이 방식은 단순하면서도 확장성이 좋고, 추후 데이터베이스나 서버 저장 방식으로 전환할 때도 유용하게 쓸 수 있다고 한다.

❗ 뜻밖의 문제: this는 여전히 나였다

 

- Guid 기반 장 구조를 도입하고 “이제 다 됐다”고 생각한 순간, 생각지도 못한 문제에 부딪혔다.
- 바로 LoadPlayer() 메서드 안에서 this와 player의 개념이 꼬여버린 것이다.

// LoadScene 클래스
Player? player = SaveManager.Load(stringName);
if (player == null) // 불러오지 못했다면
{
    Console.Write("해당 플레이어의 데이터를 찾을 수 없습니다.");
    Thread.Sleep(2000);
}
else // 불러왔다면
{
    Player.Instance.LoadPlayer(player); // 불러온 플레이어 세팅
}

- 위 코드는 JSON에서 복원한 player 객체를 싱글턴 Player.Instance에 반영하려는 의도였다.

- 그래서 Player 클래스에 LoadPlayer() 메서드를 만들어 다음과 같이 구현했다:

// Player 클래스
public void LoadPlayer(Player player)
{
    instance = player;
	// 장비창 복원
    var dict = instance.Inventory.ToDictionary(item => item.Id, item => item);
    instance.Equipments = new Dictionary<Item.ItemType, Item>();

    foreach (var kvp in instance.EquippedItemIds)
    {
        if (dict.TryGetValue(kvp.Value, out var item))
        {
            instance.Equipments[kvp.Key] = item;
        }
    }
	// 장착된 아이템에 따라 능력치 세팅
    instance.SetAbilityByEquipment();
}

🤯 내가 빠진 착각

- 처음엔 instance = player;라고 했으니, 이제 this도 player일 거라고 착각했다.
- 하지만 C#(C++)에서 this는 항상 현재 메서드를 호출한 인스턴스를 가리킨다.

- 즉, this.instance = player;는 내 내부 필드(instance) 를 player로 바꾼 것일 뿐, this 자체가 player가 된 게 아니다.

- 해결하고 나니 구조를 바꿔야 하지 않나 싶었지만, 교훈이 되도록 이대로 두기로 했다.

📝 마무리하며

- 포인터(참조) 개념은 익숙하다고 생각했는데, 여전히 부족함을 느낀 하루였다.

목차