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#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.