아이템 목록을 만들 때 List부터 선언하는 것은 자연스럽습니다. 화면에 순서대로 보여주기도 쉽고 Inspector에서 편집하기도 편하니까요. 그런데 네트워크에서 받은 ID로 아이템을 자주 찾기 시작하면 매번 목록을 훑는 부분이 눈에 들어옵니다.
이때 전체를 Dictionary로 바꾸기 전에, 필요한 작업을 나눠 보는 편이 좋습니다. 표시 순서와 ID 조회는 서로 다른 요구입니다. 데이터가 적다면 단순한 List로 충분할 수도 있습니다.
빠르다는 말에 어떤 연산인지 붙인다
List는 인덱스 접근과 연속 순회에 편합니다. 끝에 추가하는 비용은 평균적으로 작지만 용량을 늘릴 때 복사가 생길 수 있습니다. 중간에서 삭제하면 뒤의 요소가 이동하므로 ‘삭제가 잦으면 List가 빠르다’고 일반화하면 안 됩니다.
Dictionary는 키 조회에 적합합니다. 평균적으로 상수 시간에 가까운 조회를 기대하지만 해시 계산과 키 비교, 메모리 비용은 존재합니다. 해시가 충돌했다고 서로 다른 키의 값이 잘못 반환되는 것은 아닙니다. 해시 뒤에 키의 동등성도 비교합니다.
| 필요한 작업 | 먼저 검토할 구조 | 함께 볼 조건 |
|---|---|---|
| 정해진 개수, 인덱스 접근 | 배열 | 배열 요소 자체는 변경 가능 |
| 순서 있는 목록, 끝에 추가 | List | 중간 삽입·삭제와 용량 증가 |
| ID로 자주 조회 | Dictionary | 중복 키 정책과 메모리 |
| 먼저 들어온 작업부터 처리 | Queue | 무한히 쌓이지 않도록 상한 |
| 마지막 작업부터 되돌리기 | Stack | Undo 가능한 정보의 보관 범위 |
정렬된 범위 조회와 공간 충돌 후보 검색도 다른 문제입니다. 일반 이진 탐색 트리, 균형 트리, 공간 분할 트리를 하나로 묶어 항상 O(log n)이라고 설명할 수는 없습니다.
표시 목록에서 조회 인덱스 만들기
다음은 일반 C# 클래스 예제입니다. Unity Inspector 직렬화 예제는 아니며, 외부에서 전달받은 순서 있는 목록을 ID로 조회하는 부분만 다룹니다.
using System;
using System.Collections.Generic;
public sealed class ItemDefinition
{
public string Id { get; }
public string Name { get; }
public ItemDefinition(string id, string name)
{
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Item ID is required.", nameof(id));
Id = id;
Name = name;
}
}
public sealed class ItemIndex
{
private readonly Dictionary<string, ItemDefinition> byId =
new Dictionary<string, ItemDefinition>(StringComparer.Ordinal);
public ItemIndex(IEnumerable<ItemDefinition> items)
{
if (items == null)
throw new ArgumentNullException(nameof(items));
foreach (ItemDefinition item in items)
{
if (item == null)
throw new ArgumentException("Null item.", nameof(items));
if (byId.ContainsKey(item.Id))
throw new ArgumentException("Duplicate item ID: " + item.Id, nameof(items));
byId.Add(item.Id, item);
}
}
public bool TryFind(string id, out ItemDefinition item)
{
if (id == null)
{
item = null;
return false;
}
return byId.TryGetValue(id, out item);
}
}
이 예제는 같은 ID가 두 번 들어오면 초기화 단계에서 오류로 처리합니다. 조용히 마지막 값으로 덮어쓰면 원본 데이터 오류를 늦게 발견할 수 있기 때문입니다. 대소문자를 구분하는 Ordinal 비교를 선택했으므로 potion과 Potion은 다른 키입니다. 게임의 ID 규칙에 맞춰 정해야 합니다.
인덱스를 만들고 나서도 할 일이 남는다
원본 목록에 항목을 추가해도 이 Dictionary에는 자동 반영되지 않습니다. 로딩 후 목록을 고정할지, 변경할 때 인덱스를 다시 만들지, 두 구조를 함께 갱신할지 정해야 합니다. ID를 불변 프로퍼티로 둔 것도 키와 객체의 ID가 어긋나는 상황을 줄이려는 선택입니다.
검증은 존재하는 ID, 없는 ID, 중복 ID, 빈 목록부터 합니다. 그다음 실제 데이터 규모에서 전체 순회와 ID 조회가 얼마나 자주 발생하는지 측정합니다. Dictionary를 만드는 초기 비용까지 포함해야 적은 데이터에서 불필요한 복잡성을 늘리지 않을 수 있습니다.
