ImmutableList
ImmutableList는 .NET에서 제공하는 불변 컬렉션Immutable Collections 중하나로, 변경 불가능한 List를 관리합니다. 불변 리스트는 생성된 이후에 상태가 변경되지 않으며, 데이터의 추가, 삭제, 수정이 필요할 때마다 새로운 리스트를 생성합니다. 이러한 특성은 데이터 무결성을 보장하고, 멀티스레드 환경에서의 동시성 문제를 방지하는 데 큰 장점이 있습니다.
ImmutableList의 특징
- 불변 컬렉션 참조
기본 사용법
using System;
using System.Collections.Immutable;
ImmutableList<int> numbers = ImmutableList.Create(1, 2, 3);
Console.WriteLine(string.Join(", ", numbers));
// 출력: 1, 2, 3
ImmutableList<int> newNumbers = numbers.Add(4);
Console.WriteLine(string.Join(", ", newNumbers));
// 출력: 1, 2, 3, 4
// 기존 리스트와 새로운 리스트가 다른지 확인
Console.WriteLine(object.ReferenceEquals(numbers, newNumbers));
// 출력: False
ImmutableList.Create(1, 2, 3)
을 사용하여 리스트를 생성합니다.numbers.Add(4)
를 호출하면 새로운 리스트newNumbers
가 생성됩니다.numbers
는 변경되지 않고 그대로 유지됩니다.
주요 메서드와 활용
생성 및 초기화
ImmutableList는 직접 생성자를 사용하지 않고, 정적 메서드를 통해 생성합니다.
ImmutableList<int> emptyList = ImmutableList<int>.Empty;
ImmutableList<int> numbers = ImmutableList.Create(1, 2, 3);
// 빌더 패턴을 통한 생성
var builder = ImmutableList.CreateBuilder<int>();
builder.Add(1);
builder.Add(2);
builder.Add(3);
ImmutableList<int> builtList = builder.ToImmutable();
요소 추가 및 제거
- Add: 요소를 추가하여 새로운 리스트를 반환합니다.
var newNumbers = numbers.Add(4);
- AddRange: 여러 요소를 한꺼번에 추가합니다.
var newNumbers = numbers.AddRange(new[] { 4, 5, 6 });
- Remove: 특정 요소를 제거합니다.
var newNumbers = numbers.Remove(2);
- RemoveRange: 여러 요소를 제거합니다.
var newNumbers = numbers.RemoveRange(new[] { 2, 4 });
요소 접근 및 수정
- SetItem: 특정 인덱스의 요소를 변경합니다.
var newNumbers = numbers.SetItem(1, 5);
- Replace: 특정 값을 다른 값으로 대체합니다.
var newNumbers = numbers.Replace(2, 5);
- IndexOf: 요소의 인덱스를 반환합니다. 찾지 못한 경우 -1을 반환합니다.
int index = numbers.IndexOf(2);
- Clear: 모든 요소를 제거하고 빈 리스트를 반환합니다.
var clearedList = numbers.Clear();
- BinarySearch: 정렬된 리스트에서 특정 요소의 인덱스를 이진 탐색 방식으로 찾습니다.
ImmutableList<int> numbers = ImmutableList.Create(1, 2, 3, 4, 5);
int index = numbers.BinarySearch(3);
// index는 2입니다.
- ToBuilder: 리스트를 변경 가능한
ImmutableList.Builder
로 변환하여 여러 번의 변경 작업을 효율적으로 수행한 후, 다시 불변 리스트로 변환합니다.
var builder = numbers.ToBuilder();
builder.Add(4);
builder.Add(5);
var updatedNumbers = builder.ToImmutable();
- 빌더를 사용하면 변경 가능한 컨텍스트에서 여러 작업을 수행한 후, 최종적으로 불변 리스트를 생성합니다.
데이터 스냅샷과 복원
불변 리스트는 상태를 변경하지 않으므로, 특정 시점의 데이터를 스냅샷처럼 저장하고 복원할 수 있습니다. 이는 버전 관리나 트랜잭션, Undo/Redo 기능을 구현할 때 유용합니다.
var history = new Stack<ImmutableList<int>>();
var list = ImmutableList<int>.Empty;
history.Push(list);
list = list.Add(1);
history.Push(list);
list = list.Add(2);
// 이전 상태로 복원
list = history.Pop();
- 작업을 수행할 때마다 리스트의 상태를 저장합니다.
- 필요할 때 스택에서 이전 상태를 꺼내어 복원합니다.
불변 리스트와 UI 연동
불변 리스트는 데이터 변경 시마다 새로운 인스턴스를 생성하므로, UI와의 데이터 바인딩에 유리합니다.
- 변경 추적 용이: 리스트가 변경될 때마다 새로운 객체가 생성되므로, 변경을 쉽게 감지할 수 있습니다.
- 데이터 일관성 유지: 불변성이 보장되므로, UI에 표시되는 데이터의 일관성이 유지됩니다.
public class ViewModel : INotifyPropertyChanged
{
private ImmutableList<int> _numbers;
public ImmutableList<int> Numbers
{
get => _numbers;
private set
{
_numbers = value;
OnPropertyChanged(nameof(Numbers));
}
}
public ViewModel()
{
Numbers = ImmutableList<int>.Empty.AddRange(new[] { 1, 2, 3, 4, 5 });
}
public void UpdateList()
{
Numbers = Numbers.Add(6);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
INotifyPropertyChanged
를 구현하여 데이터 변경 시 UI가 자동으로 업데이트되도록 합니다.- MVVM 패턴에서 데이터 바인딩에 활용할 수 있습니다.
고급 활용과 최적화
메모리 풀링과 불변 리스트
불변 리스트는 변경될 때마다 새로운 인스턴스를 생성하기 때문에, 특히 대규모 데이터를 관리하거나 빈번한 변경이 발생할 경우 메모리 사용량이 증가할 수 있습니다. 이러한 문제를 해결하기 위해 메모리 풀링Pooling 기법을 사용할 수 있습니다.
메모리 풀링을 활용한 최적화 방법
- 리스트 패턴 풀링: 자주 사용되는 특정 리스트 패턴(예: 빈 리스트, 초기값이 동일한 리스트)을 미리 풀에 저장해 두고, 필요할 때마다 해당 리스트를 재사용합니다. 이렇게 하면 불필요한 리스트 생성과 메모리 할당을 줄일 수 있습니다.
- 중복 리스트 방지: 동일한 구조의 리스트가 자주 생성되는 경우, 새로운 리스트를 생성하는 대신 기존 풀에 저장된 리스트를 재사용하여 메모리 낭비를 줄일 수 있습니다.
- 풀 크기 관리: 풀의 크기를 적절히 관리하여 메모리 사용량을 조절하고, 메모리 최적화를 극대화합니다.
예제: 리스트 풀링 구현
using System.Collections.Concurrent;
using System.Collections.Immutable;
public class ImmutableListPool<T>
{
private readonly ConcurrentBag<ImmutableList<T>> _pool = new ConcurrentBag<ImmutableList<T>>();
public ImmutableList<T> Get()
{
if (_pool.TryTake(out var list))
{
return list;
}
return ImmutableList<T>.Empty;
}
public void Return(ImmutableList<T> list)
{
_pool.Add(list);
}
}
// 사용 예제
var pool = new ImmutableListPool<int>();
var list = pool.Get();
list = list.Add(1).Add(2);
// 리스트 사용 후 풀에 반환
pool.Return(list);
- 리스트를 풀에서 가져와 사용하고, 사용이 끝난 후 반환하여 재사용합니다.
- 이 방법을 통해 불필요한 메모리 할당을 줄이고, 메모리 사용을 최적화할 수 있습니다.
대규모 리스트에서의 효율적인 페이징
대규모 데이터 집합에서 ImmutableList를 사용할 때는 전체 리스트를 메모리에 로드하는 대신, 필요한 부분만 관리하여 메모리 사용을 최적화할 수 있습니다. 페이징Paging 기법을 적용하면 필요한 데이터만 불변 리스트로 로드하고, 나머지는 필요할 때만 불러올 수 있습니다.
페이징을 통한 메모리 관리
- 페이지 단위 데이터 관리: 대용량 데이터를 다룰 때 필요한 범위(페이지)의 데이터만 리스트로 생성하여, 메모리 사용을 줄입니다.
- 지연 로딩Lazy Loading: 페이지에 해당하는 데이터만 지연 로딩하여 메모리 사용량을 줄이고, 성능을 최적화할 수 있습니다.
- 부분 로딩: 현재 페이지와 인접한 페이지 데이터만 미리 로드하여 사용자 경험을 향상시킬 수 있습니다.
예제: 페이징 구현
using System;
using System.Collections.Immutable;
public class PaginatedImmutableList<T>
{
private readonly ImmutableList<T> _data;
private readonly int _pageSize;
public PaginatedImmutableList(ImmutableList<T> data, int pageSize)
{
_data = data;
_pageSize = pageSize;
}
public ImmutableList<T> GetPage(int pageNumber)
{
int start = pageNumber * _pageSize;
int count = Math.Min(_pageSize, _data.Count - start);
return _data.GetRange(start, count);
}
}
// 사용 예제
var data = ImmutableList<int>.Empty.AddRange(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
var paginatedList = new PaginatedImmutableList<int>(data, 3);
var page1 = paginatedList.GetPage(0); // [1, 2, 3]
var page2 = paginatedList.GetPage(1); // [4, 5, 6]
- 전체 데이터를 메모리에 로드하지 않고, 필요한 페이지의 데이터만 불변 리스트로 반환합니다.
- 메모리 사용을 최소화하면서도 필요한 데이터에 빠르게 접근할 수 있습니다.