27. WeakReference / ConditionalWeakTable과 GC 상호작용
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 문제 27 — WeakReference / ConditionalWeakTable과 GC 상호작용
//
// 아래는 게임 서버에서 "몬스터 AI 행동 트리 캐시"를 WeakReference로 구성한
// 코드다. 캐시는 몬스터 인스턴스를 강하게 붙들지 않아서, 몬스터가 다른 곳에서
// 더 이상 참조되지 않으면 GC가 자유롭게 회수할 수 있게 하려는 의도다.
//
// (A) monsterCache는 (약한 참조로) 몬스터 객체를 들고 있다.
// (B) strongRef 가 몬스터를 강하게 참조하는 동안과, 그 참조를 null로 만든 뒤의
// IsAlive/Target 값을 비교한다.
// (C) ConditionalWeakTable<Monster, BehaviorTreeData>로 같은 역할을 구현한
// 버전과 동작을 비교한다.
//
// 과제:
// 1. strongRef가 살아있는 동안 monsterCache.IsAlive 와 monsterCache.Target의
// 값을 예측하라.
// 2. strongRef = null; 로 만든 직후, GC.Collect()를 호출하기 "전"에
// monsterCache.IsAlive를 확인하면 어떤 값이 나올지 예측하라(즉시 false가
// 되는지, 아니면 GC가 실제로 회수하기 전까지는 여전히 true일 수 있는지).
// 3. GC.Collect() + GC.WaitForPendingFinalizers()를 호출한 "후"
// monsterCache.IsAlive와 monsterCache.Target을 확인하면 어떤 값이 나올지
// 예측하라.
// 4. WeakReference의 기본 생성자(trackResurrection=false)와
// new WeakReference(obj, true)(장기 WeakReference, track resurrection)의
// 차이를, 소멸자(Finalizer)가 있는 객체를 대상으로 할 때 언제 다르게
// 동작하는지 설명 관점에서 예측하라.
// 5. ConditionalWeakTable<Monster, BehaviorTreeData>를 사용한 캐시에서,
// Monster 키 객체가 GC에 의해 회수되면 그에 연결된 BehaviorTreeData
// 값도 함께 회수 대상이 되는지(키의 수명에 값의 수명이 종속되는지)를
// WeakReference 기반 캐시와 비교해 예측하라.
//
// 아래 코드는 콘솔 앱에서 컴파일·실행 가능하다 (dotnet run, Release 빌드 권장 —
// Debug 빌드/JIT 최적화 여부에 따라 지역 변수의 실제 수명이 늘어나
// GC 타이밍이 달라질 수 있음에 유의).
using System;
using System.Runtime.CompilerServices;
class Monster
{
public string Name;
public Monster(string name) { Name = name; }
~Monster()
{
Console.WriteLine($"[finalizer] Monster({Name}) 소멸자 호출됨");
}
}
class BehaviorTreeData
{
public string TreeName;
public BehaviorTreeData(string treeName) { TreeName = treeName; }
}
class MonsterCacheDemo
{
static ConditionalWeakTable<Monster, BehaviorTreeData> behaviorCache = new();
// 메서드로 분리해 지역 변수(strongRef)가 실제로 스코프를 벗어나게 함으로써
// JIT의 생존 구간(liveness) 분석에 따라 GC 대상이 되도록 유도한다.
static WeakReference CreateMonsterAndCache()
{
var monster = new Monster("Slime");
behaviorCache.Add(monster, new BehaviorTreeData("SlimeAI"));
var weakRef = new WeakReference(monster, trackResurrection: false);
return weakRef;
// monster 지역 변수는 여기서 스코프를 벗어난다.
// (호출자가 강한 참조를 별도로 유지하지 않는 한, 이 시점 이후로는
// weakRef만이 유일한 참조 경로가 될 수 있다.)
}
static void Main()
{
WeakReference monsterCache = CreateMonsterAndCache();
Console.WriteLine("--- 1. 강한 참조가 이미 없는 상태에서 GC 실행 전 ---");
Console.WriteLine($"monsterCache.IsAlive (GC 전) = {monsterCache.IsAlive}");
Console.WriteLine("--- 2. GC.Collect() + WaitForPendingFinalizers() 실행 ---");
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.WriteLine($"monsterCache.IsAlive (GC 후) = {monsterCache.IsAlive}");
Console.WriteLine($"monsterCache.Target (GC 후) = {(monsterCache.Target == null ? "null" : "not null")}");
Console.WriteLine("--- 3. ConditionalWeakTable 쪽 캐시 상태 확인 ---");
// Monster 인스턴스가 이미 회수되었다면, 그 키로는 더 이상 접근할 방법이
// 없다(참조 자체가 사라졌으므로). 대신 캐시에 몇 개의 항목이 남아있는지는
// 직접적으로 열거할 수 없다는 점 자체가 ConditionalWeakTable의 설계
// 특징이다(디버깅 목적 외에는 열거 API를 일반적으로 노출하지 않음).
Console.WriteLine("(Monster 키 객체가 회수되면 ConditionalWeakTable의 대응 엔트리도 GC 대상이 됨)");
}
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.