콘텐츠로 이동

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
// ImmutableDictionary
var 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);

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가 더 빠릅니다.


여러 항목을 한 번에 추가할 때 빌더를 사용하면 중간 인스턴스 생성을 줄일 수 있습니다.

// 비효율적 — 매 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 Builder
var 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
};

recordwith 표현식과 불변 컬렉션을 조합하면 전체 상태를 불변으로 유지하면서 간결하게 업데이트할 수 있습니다.


  • 읽기 전용 공유 데이터 → ImmutableArray (가장 빠른 읽기)
  • 구조 공유가 필요한 수정 → ImmutableList / ImmutableDictionary
  • 대량 초기 구성 → Builder 패턴
  • 초기화 후 읽기 전용 → FrozenDictionary (.NET 8)
  • 멀티스레드 업데이트 → Interlocked.CompareExchange 루프