42. Dictionary<TKey,TValue> 의 내부 구조와 열거 순서

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// [주제] Dictionary<TKey,TValue> 의 내부 구조(buckets/entries)와 열거 순서
//
// 게임서버에서 "엔티티 ID -> 엔티티" 맵을 Dictionary 로 관리하고, 매 틱
// foreach 로 순회하며 갱신·브로드캐스트한다. 아래 코드는 삽입/삭제/재삽입을
// 섞은 뒤 열거 순서와 Count 를 출력한다. 실행 결과를 예측하고 내부 자료구조
// 수준에서 설명하라.
//
// [과제]
//  1) Phase1~Phase4 각 시점에서 foreach 가 어떤 순서로 키를 출력하는지 예측하고,
//     그 순서가 buckets[] / entries[] 중 무엇에 의해 결정되는지 설명하라.
//  2) Remove 로 비워진 슬롯이 이후 Add 에서 어떻게 재사용되는지
//     (freeList / freeCount) 서술하고, 왜 나중에 넣은 키가 "중간 자리"에
//     나타나는지 설명하라.
//  3) 용량을 넘겨 Resize 가 일어난 뒤 열거 순서는 어떻게 되는가?
//     "재해싱하면 순서가 섞인다"는 통념이 맞는지 근거를 대라.
//  4) Count 와 내부 _count(사용된 entries 슬롯 수)의 관계를 설명하라.
//     둘이 달라지는 시점은 언제인가?
//  5) Phase5 에서 열거 중 Add 와 열거 중 Remove 의 결과가 왜 다른가?
//     버전 토큰(_version)의 역할과 함께 설명하라.
//  6) TryGetValue 가 실패했을 때 out 변수에 무엇이 들어가는지,
//     값 형식일 때 이것이 왜 위험한지 설명하라.
//  7) 게임서버 맥락: "엔티티가 삽입 순서대로 순회될 것"을 가정한 틱 로직이
//     왜 위험한지, 순서가 필요하면 무엇을 써야 하는지 논하라.

using System;
using System.Collections.Generic;

public struct EntityState
{
    public int Hp;
    public int Level;
    public override string ToString() => $"(hp={Hp},lv={Level})";
}

public static class Program
{
    // 용량 7(소수)로 시작해 Resize 시점을 관찰 가능하게 만든다.
    private static readonly Dictionary<int, EntityState> Entities = new(7);

    private static void Add(int id, int hp)
        => Entities[id] = new EntityState { Hp = hp, Level = 1 };

    private static void Dump(string phase)
    {
        Console.Write($"{phase}: Count={Entities.Count} | ");
        foreach (var kv in Entities)
            Console.Write($"{kv.Key} ");
        Console.WriteLine();
    }

    public static void Main()
    {
        // ---- Phase1: 순차 삽입 ----
        Add(101, 10);
        Add(102, 20);
        Add(103, 30);
        Add(104, 40);
        Add(105, 50);
        Dump("Phase1");

        // ---- Phase2: 중간 두 개 제거 ----
        Entities.Remove(102);
        Entities.Remove(104);
        Dump("Phase2");

        // ---- Phase3: 두 개 재삽입 (새 ID) ----
        Add(106, 60);
        Add(107, 70);
        Dump("Phase3");

        // ---- Phase4: 용량을 넘겨 Resize 유발 ----
        Add(108, 80);
        Add(109, 90);
        Add(110, 100);   // 이 시점에 내부 배열이 커진다
        Dump("Phase4");

        // ---- Phase5: 열거 중 수정 ----
        try
        {
            foreach (var kv in Entities)
            {
                if (kv.Key == 103)
                    Entities[999] = new EntityState { Hp = 1, Level = 1 }; // 새 키 추가
            }
            Console.WriteLine("Phase5-Add: no exception");
        }
        catch (InvalidOperationException)
        {
            Console.WriteLine("Phase5-Add: InvalidOperationException");
        }

        try
        {
            foreach (var kv in Entities)
            {
                if (kv.Key == 103)
                    Entities.Remove(105);
            }
            Console.WriteLine("Phase5-Remove: no exception");
        }
        catch (InvalidOperationException)
        {
            Console.WriteLine("Phase5-Remove: InvalidOperationException");
        }

        // ---- Phase6: 조회 실패 시 out 값 ----
        if (!Entities.TryGetValue(4242, out EntityState missing))
            Console.WriteLine($"Phase6: missing={missing}");

        // ---- Phase7: "첫 번째 엔티티"를 순서에 의존해 고르는 코드 ----
        foreach (var kv in Entities)
        {
            Console.WriteLine($"Phase7: first={kv.Key}");
            break;
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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