콘텐츠로 이동

C# FrozenDictionary & FrozenSet — 읽기 전용 고성능 컬렉션

FrozenDictionary<TKey, TValue>FrozenSet<T>은 .NET 8에서 도입된 불변 고성능 컬렉션입니다. 생성 시 내부 구조를 최적화해 이후 조회 속도를 Dictionary보다 빠르게 만들지만, 생성 후 수정은 불가합니다.


using System.Collections.Frozen;
// FrozenDictionary 생성
var config = new Dictionary<string, string>
{
["host"] = "localhost",
["port"] = "5432",
["db"] = "mydb"
}.ToFrozenDictionary();
// 읽기 전용 — Add/Remove 없음
string host = config["host"];
bool hasPort = config.ContainsKey("port");
// FrozenSet 생성
var allowedRoles = new[] { "admin", "editor", "viewer" }
.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
bool isAllowed = allowedRoles.Contains("Admin"); // true

연산DictionaryFrozenDictionary
생성빠름느림 (최적화 포함)
조회 (TryGetValue)기준최대 ~2× 빠름
메모리기준비슷하거나 적음
수정가능불가

내부적으로 키 분포를 분석해 완전 해시 함수(perfect hashing)를 생성하거나, 작은 컬렉션은 선형 탐색을 사용합니다.


3.1 애플리케이션 시작 시 한 번 생성

섹션 제목: “3.1 애플리케이션 시작 시 한 번 생성”
public static class CountryLookup
{
private static readonly FrozenDictionary<string, Country> _map =
LoadCountries().ToFrozenDictionary(c => c.Code);
public static Country? Find(string code)
=> _map.TryGetValue(code, out var c) ? c : null;
}
public class PermissionChecker(IEnumerable<string> permissions)
{
private readonly FrozenSet<string> _permissions =
permissions.ToFrozenSet(StringComparer.Ordinal);
public bool Has(string permission) => _permissions.Contains(permission);
}
var methodHandlers = new Dictionary<string, Func<HttpContext, Task>>
{
["GET"] = HandleGet,
["POST"] = HandlePost,
["DELETE"] = HandleDelete,
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

// 대소문자 무시 FrozenDictionary
var headers = rawHeaders.ToFrozenDictionary(
StringComparer.OrdinalIgnoreCase);
// 숫자 키 FrozenDictionary
var statusMessages = new Dictionary<int, string>
{
[200] = "OK",
[404] = "Not Found",
[500] = "Internal Server Error",
}.ToFrozenDictionary();

항목ImmutableDictionaryFrozenDictionary
불변성
With 연산✓ (새 인스턴스 반환)
조회 성능Dictionary보다 느림Dictionary보다 빠름
목적불변 수정 가능 컬렉션읽기 전용 최고 성능

  • 애플리케이션 시작 시 데이터를 로드하고 이후 읽기만 하는 경우
  • 핫 경로(hot path)에서 딕셔너리 조회 성능이 병목인 경우
  • 설정 값, 코드 테이블, 권한 목록 등 정적 데이터

// FrozenDictionary는 읽기 전용이므로 추가 동기화 없이 멀티스레드 안전
private static readonly FrozenDictionary<string, Config> _configs =
LoadFromFile().ToFrozenDictionary(c => c.Key);
// 여러 스레드에서 동시에 읽어도 락 불필요
Parallel.ForEach(requests, req =>
{
if (_configs.TryGetValue(req.Key, out var cfg))
Process(req, cfg);
});

| Method | N | Mean | Allocated |
|------------------------|------|----------:|----------:|
| Dictionary_TryGetValue | 1000 | 12.34 ns | - |
| Frozen_TryGetValue | 1000 | 6.87 ns | - |
| Immutable_TryGetValue | 1000 | 38.21 ns | - |
FrozenDictionary: Dictionary 대비 ~44% 빠름
ImmutableDictionary: Dictionary 대비 ~3× 느림 (구조적 공유 비용)

내부적으로 소형 컬렉션(~10개 이하)은 선형 탐색, 대형 컬렉션은 완전 해시(perfect hashing) 전략을 자동 선택합니다.


// struct 키는 boxing 없이 비교 → 추가 성능 이득
var idMap = items
.ToFrozenDictionary(item => item.Id); // Id가 int/Guid 등 struct
// Guid 키 예시
FrozenDictionary<Guid, User> userCache = users
.ToFrozenDictionary(u => u.Id);

// Program.cs — 라우트 핸들러 테이블을 Frozen으로
builder.Services.AddSingleton(sp =>
{
return new Dictionary<string, RequestDelegate>
{
["/health"] = HealthHandler,
["/metrics"] = MetricsHandler,
["/ready"] = ReadyHandler,
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
});
// 미들웨어에서 사용 (매 요청마다 빠른 조회)
app.Use(async (ctx, next) =>
{
var handlers = ctx.RequestServices
.GetRequiredService<FrozenDictionary<string, RequestDelegate>>();
if (handlers.TryGetValue(ctx.Request.Path, out var handler))
{
await handler(ctx);
return;
}
await next(ctx);
});

FrozenDictionaryFrozenSet은 “한 번 빌드, 많이 읽기” 패턴에 최적화된 컬렉션입니다. 정적 데이터 조회가 빈번한 경우 Dictionary를 그대로 두지 말고 ToFrozenDictionary()로 교체하면 의미 있는 성능 향상을 얻을 수 있습니다. 읽기 전용 특성 덕분에 별도 락 없이 멀티스레드 환경에서 안전하게 공유할 수 있습니다.