오늘은 MonsterRepository를 리팩토링하던 중 몬스터 상태가 전투마다 초기화되지 않는 문제를 마주했다.
원래는 new로 새 List를 만들어 매번 전투에 넘기는 방식이었는데, 던전마다 몬스터 목록을 매칭하기 위해 Dictionary<int, List<List<Monster>>>로 바꾸면서 의도치 않게 몬스터 인스턴스가 메모리상에서 공유되는 구조가 되었다.
리팩토링 전에는 아래와 같은 방식으로 매번 새로운 리스트를 생성했다.
때문에 랜덤 스폰 시에 Monster를 가지고 와도 항상 새로운 Monster를 가지고 올 수 있다.
List<Monster> Monsters => new()
{
new Monster("슬라임", 1, new Stats(10, 2, 1)),
new Monster("고블린", 2, new Stats(20, 3, 2))
};
하지만 던전 단위로 몬스터를 관리하려다 보니, 아래처럼 딕셔너리로 몬스터 목록을 저장하게 되었다.
DungeonMonsters = new Dictionary<int, List<List<Monster>>>
{
{ 1, new() { MonstersNo1, SpecialMonstersNo1 } },
{ 2, new() { MonstersNo2, SpecialMonstersNo2 } }
};
이제 MonstersNo1 안의 Monster 객체들은 딕셔너리에 한 번 들어가면 프로그램이 끝날 때까지 메모리에 남는다.
결국 한 전투에서 IsDead = true가 되면, 그 인스턴스는 다른 전투에서도 그대로 죽은 상태로 유지된다.
C#에서 class는 참조형(reference type) 이기 때문에 리스트에 들어간 Monster는 “값의 복사본”이 아니라 “주소”만 전달된다.
즉, Monsters.Add(monsters[0]); 라고 하면, 새로운 몬스터를 만드는 게 아니라 이미 존재하던 객체의 메모리 주소를 가리키는 참조만 추가하는 셈이다.
그래서 어떤 전투에서 HP가 줄거나 죽어도, 그 정보가 그대로 다른 전투까지 이어진다.
이는 객체의 생명주기를 명확히 끊지 못한 구조다.
이 문제를 해결하기 위해, Monster 클래스에 Clone() 메서드를 추가해서 전투마다 새로운 객체 인스턴스를 생성하도록 했다.
public Monster Clone()
{
return new Monster(Name, Level, new Stats(Stats.Atk, Stats.Def, Stats.Hp));
}
이러면 전투 시작 시에는 Repository의 몬스터를 그대로 가져오지 않고, 복제본을 생성해서 사용할 수 있다.
var original = monsters[0][random.Next(monsters[0].Count)];
Monsters.Add(original.Clone());
이제 매번 new를 통해 새로운 메모리 공간에 독립적인 Monster 객체가 만들어지므로 전투마다 완전히 초기화된 상태로 시작된다.
| 시점 | 객체 상태 | 메모리 위치 | 관리 주체 |
|---|---|---|---|
| Repository 초기화 시 | 템플릿 데이터 생성 | 힙(Heap) | MonsterRepository |
| 전투 시작 시 | 복제된 Monster 인스턴스 생성 | 힙(Heap) | BattleManager |
| 전투 종료 시 | 참조 해제 (Monsters.Clear()) | GC(가비지 컬렉터)가 수거 | - |
이제 Repository는 순수하게 몬스터 정보(템플릿)만 저장하고, 전투마다 생성되는 몬스터들은 잠깐 메모리에 존재하다가 참조가 사라지면 가비지 컬렉터에 의해 정리된다.
이번 문제는 단순한 참조 실수가 아니라, 객체를 한 번만 만들고 계속 돌려쓰는 구조에서 비롯된 오류였다.
Repository는 말 그대로 데이터를 보관하는 곳이어야 하고, 실제 전투에서 쓰는 몬스터는 매번 new로 새로 만들어야 한다.
알고 있는 개념이었지만, 막상 코드에 직접 적용하려고 할 때는 그 내용을 자연스럽게 녹여내기까지 충분한 연습이 필요하다 는 걸 다시금 느낀다.