불변 컬렉션 개요

불변 컬렉션 개요

불변 컬렉션Immutable Collections은 컬렉션의 상태가 생성된 이후 변경되지 않는 데이터 구조입니다. 즉, 컬렉션에 요소를 추가하거나 제거하는 등의 변화를 가할 수 없으며, 기존 컬렉션을 변경하려면 항상 새로운 컬렉션이 생성됩니다. 이러한 특성은 데이터의 무결성을 유지하고 다중 스레드 환경에서 안전한 프로그래밍을 가능하게 해줍니다.

불변 컬렉션의 필요성

불변 컬렉션이 중요한 이유는 주로 다음과 같습니다:

  • 스레드 안전성: 불변 컬렉션은 변경할 수 없기 때문에 여러 스레드에서 동시에 접근해도 안전합니다. 이는 동시성 문제를 방지하고 복잡한 락 관리 없이 데이터를 공유할 수 있는 이점을 제공합니다.
  • 데이터 무결성 보장: 불변성은 데이터의 일관성을 유지하는 데 중요한 역할을 합니다. 데이터가 한 번 정의되면 이후 변경되지 않으므로, 원본 데이터가 변경되지 않는다는 보장이 필요할 때 불변 컬렉션을 사용하는 것이 유리합니다.
  • 복사 및 공유의 효율성: 불변 컬렉션은 복사 작업 시 실제 데이터가 아닌 참조를 공유하기 때문에, 복사 비용을 줄일 수 있습니다. 이로 인해 메모리 사용량이 절감되며, 많은 데이터를 여러 곳에서 사용해야 할 때 효율적입니다.

불변 컬렉션의 발전 과정

초기 .NET에서는 컬렉션이 기본적으로 가변 형태였기 때문에 다중 스레드 환경에서 데이터를 안전하게 사용하기 위해서는 복잡한 동기화 메커니즘이 필요했습니다. 이러한 문제를 해결하기 위해 .NET Framework 4.0부터 동시성 컬렉션Concurrent Collections이 도입되었습니다. ConcurrentDictionary, ConcurrentQueue, ConcurrentBag 등 다양한 동시성 컬렉션이 추가되면서, 락lock이나 락 프리lock-free 구조를 통해 다중 스레드 환경에서 데이터를 안전하게 관리할 수 있게 되었습니다. 이후, 함수형 프로그래밍의 인기가 높아지면서 데이터 불변성의 중요성이 부각되었고, 이를 바탕으로 불변 컬렉션Immutable Collections이 도입되었습니다. 불변 컬렉션은 다중 스레드 환경에서 데이터의 변경 없이 안정적으로 사용할 수 있어, 동기화 문제를 최소화하고 코드의 안정성을 높이는 데 기여했습니다. .NET Immutable Collections는 이러한 필요를 충족하며, 데이터의 변경이 발생할 때마다 새로운 컬렉션을 생성하여 참조 무결성을 보장하고, 데이터 공유와 유지보수를 더욱 간단하고 안전하게 해 줍니다. 불변 컬렉션은 동시성 컬렉션의 단점을 보완하는 대안으로서, 다중 스레드 환경에서의 데이터 안정성을 제공하는 중요한 역할을 하게 되었습니다.

불변 컬렉션의 종류

.NET에서 제공하는 주요 불변 컬렉션은 다음과 같습니다:

ImmutableList

  • ImmutableList<T>는 요소의 추가, 제거, 수정 작업이 필요한 경우 새로운 List를 반환합니다.
  • 리스트의 특성을 그대로 유지하면서 불변성을 보장합니다.

ImmutableDictionary

  • ImmutableDictionary<TKey, TValue>는 키-값 쌍을 저장하는 불변 컬렉션입니다.
  • 기존 딕셔너리에서 항목을 추가하거나 삭제하면 새로운 딕셔너리가 생성됩니다.

ImmutableHashSet

  • ImmutableHashSet<T>는 중복을 허용하지 않는 불변 컬렉션입니다.
  • 요소를 추가하거나 제거할 때마다 새로운 HashSet이 반환됩니다.

ImmutableArray

  • ImmutableArray<T>는 고정 크기의 배열을 불변으로 관리할 수 있는 구조입니다.
  • 배열의 장점인 빠른 접근 속도를 유지하면서도 불변성을 제공합니다.

ImmutableQueue

  • ImmutableQueue<T>는 FIFO 구조를 가진 불변 큐입니다.
  • 요소를 추가하거나 제거하면 새로운 가 생성됩니다.

ImmutableStack

  • ImmutableStack<T>는 LIFO 구조를 가진 불변 스택입니다.
  • 요소를 추가하거나 제거할 때마다 새로운 스택을 반환합니다.

불변 컬렉션의 장단점

장점

  • 안전한 동시성: 불변 컬렉션은 변경할 수 없기 때문에 여러 스레드가 동시에 접근해도 데이터의 일관성이 보장됩니다. 이는 락이 필요 없음을 의미하며, 동시성 문제를 줄여줍니다.
  • 쉽고 명확한 코드: 데이터 변경을 허용하지 않기 때문에 코드에서 데이터 흐름을 쉽게 추적할 수 있습니다. 불변성 덕분에 예기치 않은 변경을 방지할 수 있어 유지보수가 용이해집니다.
  • 함수형 프로그래밍에 적합: 불변 컬렉션은 함수형 프로그래밍 패러다임에서 자주 사용됩니다. 데이터의 상태를 변경하지 않고 새로운 값을 반환하는 함수형 프로그래밍에서 불변 컬렉션은 매우 유용합니다.

단점

  • 성능 오버헤드: 불변 컬렉션은 변경 작업 시마다 새로운 컬렉션을 생성하므로, 가변 컬렉션에 비해 메모리 사용량이 증가할 수 있으며 성능에도 영향을 미칠 수 있습니다.
  • 복잡한 변경 작업: 작은 변경에도 새로운 컬렉션이 생성되므로, 변경 작업이 빈번하게 발생하는 경우 사용하기에 적합하지 않을 수 있습니다.

구조적 공유를 통한 메모리 절약

구조적 공유structural sharing는 불변 컬렉션이 새로운 인스턴스를 생성할 때 변경된 부분만 복사하고 나머지 부분은 기존 컬렉션의 데이터를 공유하는 기법입니다. 이를 통해 불변 컬렉션이 자주 생성되더라도 메모리 사용량을 최소화할 수 있습니다. 예를 들어, 새로운 요소가 추가될 때 전체 컬렉션을 복사하는 대신, 루트 노드와 일부 변경된 경로만 새로 생성하고 나머지 요소는 기존 인스턴스를 공유하도록 합니다.

장점

  • 메모리 사용량 감소: 변경이 일어난 부분만 복사하므로, 데이터가 많아도 메모리 사용량을 줄일 수 있습니다.
  • 성능 최적화: 전체 컬렉션을 복사하지 않아 복사 비용이 감소합니다.

변경 시 새로운 컬렉션 반환

불변 컬렉션은 변경 작업 시마다 새로운 인스턴스를 생성하고 반환합니다. 예를 들어, 요소를 추가하는 Add 연산을 수행하면 기존 컬렉션을 수정하는 대신, 변경 사항이 반영된 새로운 불변 컬렉션을 반환합니다. 이 과정에서 새로운 컬렉션은 변경 사항이 포함된 인스턴스이고, 원본 컬렉션은 그대로 유지됩니다. 이 방식은 원본 컬렉션에 대한 안전한 참조를 제공하여, 다른 스레드가 해당 컬렉션에 접근하더라도 일관된 상태를 유지할 수 있습니다. 또한, 불변 컬렉션의 인스턴스를 반복적으로 사용하여 안전한 캐싱이나 동시성을 보장할 수 있습니다.

빌더 패턴을 활용한 삽입 및 삭제 관리

불변 리스트는 변경 시마다 새로운 인스턴스를 생성하므로, 반복적인 추가나 삭제 작업에서 메모리와 성능에 영향을 줄 수 있습니다.

잘못된 사용 예시

var list = ImmutableList<int>.Empty;
for (int i = 0; i < 100; i++)
{
    list = list.Add(i);
}
  • 매번 Add를 호출할 때마다 새로운 리스트가 생성되어 메모리 할당이 빈번하게 발생합니다.

효율적인 사용 예시

var builder = ImmutableList.CreateBuilder<int>();
for (int i = 0; i < 100; i++)
{
    builder.Add(i);
}
var newList = builder.ToImmutable();
  • 빌더를 사용하여 변경 작업을 수행하고, 최종적으로 한 번만 불변 리스트를 생성합니다.
  • 메모리 할당과 성능 측면에서 훨씬 효율적입니다.