System.Collections.Immutable은 컬렉션을 불변(immutable)으로 유지하면서도 실용적인 성능을 제공하는 라이브러리입니다. 동시성 안전, 예측 가능성, 버그 감소에 큰 도움이 됩니다.
1. 왜 Immutable Collections인가
불변 컬렉션은 생성 후 상태가 바뀌지 않습니다. 변경이 필요하면 항상 새로운 인스턴스를 반환합니다. 이 특성은 다음을 보장합니다.
- 스레드 안전: 잠금 없이 공유해도 안전합니다.
- 예측 가능성: 사이드 이펙트가 줄어 디버깅이 쉬워집니다.
- 스냅샷 모델: 특정 시점의 데이터를 그대로 보존할 수 있습니다.
2. 설치와 기본 사용
NuGet 패키지 추가 후 using을 선언합니다.
// CLI: 프로젝트에 패키지 추가
// dotnet add package System.Collections.Immutable
using System.Collections.Immutable;
추가/삭제 등 모든 연산은 원본을 변경하지 않고 새 컬렉션을 반환합니다.
using System;
using System.Collections.Immutable;
var list1 = ImmutableList.Empty;
var list2 = list1.Add(1).Add(2);
Console.WriteLine(string.Join(",", list1)); // ""
Console.WriteLine(string.Join(",", list2)); // "1,2" (list1은 유지)
3. 주요 컬렉션 빠르게 보기
- ImmutableArray<T>: 값 타입처럼 가볍고 읽기/열거가 빠릅니다. 대량 읽기에 적합합니다.
- ImmutableList<T>: 중간 삽입/삭제, 다양한 편의 메서드 제공.
- ImmutableDictionary<TKey,TValue> / ImmutableHashSet<T>: 키/집합 기반 조회에 적합.
using System;
using System.Collections.Immutable;
// ImmutableArray
var arr = new[] { 1, 2, 3 }.ToImmutableArray();
var arr2 = arr.Add(4);
Console.WriteLine(arr[0]); // 1 (원본 유지)
Console.WriteLine(arr2[^1]); // 4
// ImmutableDictionary
var dict1 = ImmutableDictionary<string, int>.Empty
.Add("A", 1)
.SetItem("A", 2); // 키가 있으면 대체
var dict2 = dict1.Remove("A");
Console.WriteLine(dict1["A"]); // 2
Console.WriteLine(dict2.ContainsKey("A")); // False
// ImmutableHashSet (중복 자동 제거)
var set = ImmutableHashSet<string>.Empty.Add("a").Add("a");
Console.WriteLine(set.Count); // 1
4. 대량 변경은 Builder로
항목을 많이 추가/삭제해야 한다면 Builder를 사용해 할당을 줄입니다.
using System.Collections.Immutable;
var list = ImmutableList<int>.Empty;
var builder = list.ToBuilder();
for (int i = 0; i < 100_000; i++)
builder.Add(i);
list = builder.ToImmutable(); // 한 번에 불변 리스트로 고정
5. 멀티스레드에서 안전한 업데이트 패턴
불변 컬렉션은 읽기 공유가 안전합니다. 쓰기는 새 인스턴스를 만들어 원자적으로 교체합니다. Interlocked.CompareExchange를 이용해 경합 없이 갱신할 수 있습니다.
using System;
using System.Collections.Immutable;
using System.Threading;
public sealed class Cache
{
private ImmutableDictionary<string, int> _map = ImmutableDictionary<string, int>.Empty;
public int GetOrAdd(string key, Func<string, int> factory)
{
while (true)
{
var snapshot = _map; // 스냅샷은 항상 안전
if (snapshot.TryGetValue(key, out var value))
return value;
var computed = factory(key);
var updated = snapshot.Add(key, computed);
// snapshot이 여전히 현재이면 원자적으로 교체
var original = Interlocked.CompareExchange(ref _map, updated, snapshot);
if (ReferenceEquals(original, snapshot))
return computed; // 교체 성공
// 다른 스레드가 먼저 갱신했으면 재시도
}
}
}
참고: ImmutableInterlocked 유틸리티를 사용하면 사전용 TryAdd/TryRemove 같은 연산을 더 간단히 사용할 수 있습니다.
6. LINQ/변환 팁
LINQ 결과를 바로 불변으로 고정하면 이후 안전하게 공유할 수 있습니다.
using System.Linq;
using System.Collections.Immutable;
var evens = Enumerable.Range(1, 10)
.Where(x => x % 2 == 0)
.ToImmutableArray();
// 키가 고유해야 합니다.
var peopleById = people.ToImmutableDictionary(p => p.Id, p => p);
// 변경 후 다시 고정하는 패턴
var tmp = peopleById.ToBuilder();
tmp["42"] = tmp["42"] with { Name = "New Name" };
peopleById = tmp.ToImmutable();
7. 성능 팁과 언제 쓰면 좋은가
- 읽기 위주/스냅샷/구성 데이터: 강력 추천입니다.
- 멀티스레드 공유 캐시: 잠금 없이 안전하게 교체할 수 있어 적합합니다.
- 빈번한 개별 추가/삭제가 매우 많은 핫 루프: 일반 List/Dictionary로 작업 후 ToImmutableArray/ToImmutableDictionary로 고정하는 것이 더 낫습니다.
- 값 비교가 필요하면 SequenceEqual/SetEquals 등을 사용하세요(인스턴스 참조 비교는 값 동등성을 보장하지 않습니다).
요약: Immutable Collections는 코드 안전성과 가독성을 높이면서 동시성 문제를 줄여줍니다. 읽기 공유와 스냅샷이 중요한 곳부터 단계적으로 도입해 보시길 권합니다.
'C#' 카테고리의 다른 글
| C# 중첩 스위치 패턴과 복잡한 조건 매칭 구현 (0) | 2026.05.12 |
|---|---|
| C# 값 튜플(ValueTuple) vs Tuple 성능 비교 (0) | 2026.05.12 |
| C# Expression Tree로 동적 코드 생성하기 (0) | 2026.05.11 |
| C# AssemblyLoadContext로 플러그인 아키텍처 만들기 (0) | 2026.05.08 |
| C# 애플리케이션에서 메모리 풀(Object Pool) 구현하기 (0) | 2026.05.08 |