ImmutableCollections & 불변성 패턴
불변 컬렉션(Immutable Collections)은 한 번 생성된 후 절대 변경되지 않는 컬렉션입니다. 상태 변경이 필요하면 새로운 인스턴스를 반환합니다. 이 특성은 멀티스레드 환경에서 잠금 없이 안전하게 공유할 수 있고, 함수형 프로그래밍 스타일을 가능하게 합니다.
NuGet: System.Collections.Immutable.NET 5 이상은 기본 포함불변 컬렉션 종류
섹션 제목: “불변 컬렉션 종류”| 타입 | 특징 | 내부 구조 |
|---|---|---|
ImmutableArray<T> | 가장 빠른 읽기, 값 타입 기반 | 배열 |
ImmutableList<T> | 구조 공유 지원, 삽입/삭제 O(log n) | AVL 트리 |
ImmutableDictionary<K,V> | 구조 공유 해시맵 | 해시 배열 맵 트리 |
ImmutableHashSet<T> | 중복 없는 집합 | 해시 배열 맵 트리 |
ImmutableStack<T> | 스택, push/pop O(1) | 연결 리스트 |
ImmutableQueue<T> | 큐, enqueue/dequeue O(1) 분할상환 | 두 개의 스택 |
ImmutableSortedDictionary<K,V> | 정렬 보장 | AVL 트리 |
기본 사용법
섹션 제목: “기본 사용법”using System.Collections.Immutable;
// 생성var list = ImmutableList<int>.Empty;var list2 = ImmutableList.Create(1, 2, 3);var list3 = new[] { 1, 2, 3 }.ToImmutableList();
// 추가 — 원본 불변, 새 인스턴스 반환var list4 = list2.Add(4); // [1,2,3,4]var list5 = list2.Insert(0, 0); // [0,1,2,3]var list6 = list2.Remove(2); // [1,3]
Console.WriteLine(list2.Count); // 3 — 변경 없음Console.WriteLine(list4.Count); // 4
// ImmutableDictionaryvar dict = ImmutableDictionary<string, int>.Empty;dict = dict.Add("a", 1).Add("b", 2);dict = dict.SetItem("a", 100); // 키 존재 시 덮어쓰기
// ImmutableArray (값 타입 — boxing 없음)ImmutableArray<int> arr = ImmutableArray.Create(1, 2, 3);var arr2 = arr.Add(4);구조 공유 (Structural Sharing)
섹션 제목: “구조 공유 (Structural Sharing)”ImmutableList는 AVL 트리 기반이라 수정 시 변경된 경로의 노드만 새로 할당하고 나머지는 공유합니다.
원본 트리: 수정 후: [2] [2'] / \ / \ [1] [3] [1] [3'] \ [4] ← 새 노드var original = ImmutableList.Create(1, 2, 3);var modified = original.Add(4);
// 내부 노드 대부분 공유 — GC 압력 최소화// original과 modified는 독립적이지만 메모리 효율적ImmutableArray는 배열 기반이라 매 수정마다 전체 복사가 발생합니다. 자주 수정이 일어나는 경우 ImmutableList가 낫고, 읽기 위주라면 ImmutableArray가 더 빠릅니다.
Builder 패턴 (대량 수정 최적화)
섹션 제목: “Builder 패턴 (대량 수정 최적화)”여러 항목을 한 번에 추가할 때 빌더를 사용하면 중간 인스턴스 생성을 줄일 수 있습니다.
// 비효율적 — 매 Add마다 새 인스턴스 생성var list = ImmutableList<int>.Empty;for (int i = 0; i < 1000; i++) list = list.Add(i); // 1000개의 임시 인스턴스
// 효율적 — Builder 사용var builder = ImmutableList.CreateBuilder<int>();for (int i = 0; i < 1000; i++) builder.Add(i); // 가변 리스트처럼 동작var immutableList = builder.ToImmutable(); // 단 한 번 변환
// Dictionary Buildervar dictBuilder = ImmutableDictionary.CreateBuilder<string, int>();dictBuilder["a"] = 1;dictBuilder["b"] = 2;var immutableDict = dictBuilder.ToImmutable();멀티스레드 안전성
섹션 제목: “멀티스레드 안전성”불변 컬렉션은 잠금 없이 여러 스레드에서 공유 가능합니다.
// 공유 상태 — 읽기는 완전히 안전private static ImmutableList<string> _cache = ImmutableList<string>.Empty;
// 쓰기 시 Interlocked.CompareExchange로 잠금 없는 업데이트public static void AddItem(string item) { ImmutableList<string> original, updated; do { original = _cache; updated = original.Add(item); } while (Interlocked.CompareExchange(ref _cache, updated, original) != original);}
public static IReadOnlyList<string> GetAll() => _cache; // 잠금 불필요이 패턴은 읽기가 압도적으로 많고, 쓰기가 드문 경우(캐시, 설정 등)에 매우 효과적입니다.
ImmutableArray vs ImmutableList 선택 기준
섹션 제목: “ImmutableArray vs ImmutableList 선택 기준”// ImmutableArray — 읽기 성능 최우선// - foreach: List<T>와 동일한 속도// - 인덱서: O(1)// - 수정: 전체 복사 O(n)
ImmutableArray<int> fastRead = ImmutableArray.Create(1, 2, 3);
// ImmutableList — 수정이 잦은 경우// - 삽입/삭제: O(log n)// - 인덱서: O(log n)// - 구조 공유로 메모리 효율
ImmutableList<int> flexibleMod = ImmutableList.Create(1, 2, 3);
// 성능 측정 결과 (대략적 기준)// 읽기(foreach) : ImmutableArray ≈ List<T> >> ImmutableList// 추가(단일) : ImmutableList > ImmutableArray// 대량 초기화 : Builder 패턴이 항상 우선FrozenCollection과의 비교 (C# 8+ / .NET 8)
섹션 제목: “FrozenCollection과의 비교 (C# 8+ / .NET 8)”using System.Collections.Frozen;
// FrozenDictionary — 초기화 후 절대 변경 안 하는 경우 최적var data = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 };FrozenDictionary<string, int> frozen = data.ToFrozenDictionary();
// 읽기 성능: FrozenDictionary >> ImmutableDictionary// 이유: 초기화 시 해시 최적화 수행// 단점: 수정 불가, 빌더 없음, 초기화 비용 높음
// 사용 시나리오 비교// ImmutableDictionary: 수정이 가끔 발생 + 구조 공유 필요// FrozenDictionary : 앱 시작 시 1회 초기화 후 읽기 전용실전 패턴: 불변 상태 관리
섹션 제목: “실전 패턴: 불변 상태 관리”Redux 스타일의 상태 관리에서 불변 컬렉션이 유용합니다.
public record AppState( ImmutableList<string> Items, ImmutableDictionary<string, bool> Flags);
public static AppState Reduce(AppState state, IAction action) => action switch { AddItemAction a => state with { Items = state.Items.Add(a.Item) }, RemoveItemAction r => state with { Items = state.Items.Remove(r.Item) }, SetFlagAction f => state with { Flags = state.Flags.SetItem(f.Key, f.Value) }, _ => state};record의 with 표현식과 불변 컬렉션을 조합하면 전체 상태를 불변으로 유지하면서 간결하게 업데이트할 수 있습니다.
핵심 요약
섹션 제목: “핵심 요약”- 읽기 전용 공유 데이터 →
ImmutableArray(가장 빠른 읽기) - 구조 공유가 필요한 수정 →
ImmutableList/ImmutableDictionary - 대량 초기 구성 →
Builder패턴 - 초기화 후 읽기 전용 →
FrozenDictionary(.NET 8) - 멀티스레드 업데이트 →
Interlocked.CompareExchange루프