Quick Flow
더티 플래그는 원본 값이 바뀔 때 cache를 바로 다시 만들지 않고 무효 상태만 기록했다가, 다음 조회에서 한 번 재계산하는 방식입니다. 원본, cache, invalidate 경로, rebuild owner가 분명할 때만 사용합니다.
public sealed class EquipmentStats
{
private readonly List<Item> items = new();
private bool totalsDirty = true;
private Stats cachedTotals;
public void Add(Item item) { items.Add(item); totalsDirty = true; }
public Stats Totals
{
get
{
if (totalsDirty) { cachedTotals = Calculate(items); totalsDirty = false; }
return cachedTotals;
}
}
}| 변화·조회 패턴 | 선택 | 반드시 정할 것 |
|---|---|---|
| 변경은 드물고 조회가 많음 | lazy dirty cache | 모든 mutation이 invalidate하는지 |
| 변경 때 즉시 UI도 갱신 | eager rebuild 또는 event | 한 frame 안의 observer 순서 |
| 계산이 매우 작음 | 매번 계산 | cache 복잡도를 만들지 않음 |
| 여러 thread·job이 읽음 | 동기화 정책부터 설계 | cache visibility와 snapshot |
원본과 파생 값을 나누기
inventory item, base stat, modifier는 source of truth이고 total attack, sorted UI list, navigation graph는 다시 만들 수 있는 derived state입니다. cache가 원본처럼 여러 곳에서 수정되면 invalidation 조건을 알 수 없게 됩니다. mutation API를 한 곳으로 모으거나 immutable snapshot을 발행해 dirty를 켜는 책임을 명확히 합니다.
Unity Transform 내부의 변경 추적을 직접 흉내 내기보다, gameplay의 own data에서 실제 측정된 반복 계산을 대상으로 시작합니다. Inspector의 ScriptableObject asset은 shared authoring data일 수 있으므로 actor별 runtime total을 asset에 저장해 다른 instance가 같은 cache를 공유하지 않게 합니다.
무효화와 재계산의 실패
원본 변경 뒤 dirty를 켜지 않으면 stale cache가, rebuild 뒤 false로 돌리지 않으면 매번 재계산이 생깁니다. 조회 중 rebuild가 event를 발행하고 listener가 다시 같은 cache를 읽는 re-entrant 경로도 정합니다. UI는 보여 줄 때만 cache를 읽는지, stat change event에서 즉시 refresh하는지 한 방식을 고릅니다.
측정 뒤 도입하기
더티 플래그는 cache invalidation이라는 새 state를 추가합니다. 계산이 싸거나 조회가 드물면 간단한 재계산이 더 정확하고 읽기 쉽습니다. profiler에서 같은 derived calculation이 실제 hot path인지, cache를 둔 뒤 CPU time·allocation·stale UI bug가 어떻게 변했는지 비교합니다.
dirty = true 한 줄이 pattern의 전부가 아닙니다. 원본을 바꾸는 모든 경로와 cache를 재생성하는 owner를 찾을 수 없으면, 성능 개선보다 오래된 값 bug를 먼저 만들 가능성이 큽니다.
참고 링크
1 sources