c# 컬렉션(Collection)의 성능 및 메모리 관리

2024. 9. 29. 19:58c#

컬렉션의 성능 및 메모리 관리

C#에서 컬렉션(Collection)은 데이터를 효율적으로 저장하고 관리할 수 있는 유용한 도구입니다.
그러나 각각의 컬렉션은 성능과 메모리 사용 측면에서 차이가 있기 때문에, 적절한 컬렉션을 선택하는 것이 성능 최적화의 중요한 요소가 됩니다. 이 포스팅에서는 C#에서 제공하는 다양한 컬렉션들의 성능과 메모리 관리에 대해 알아보겠습니다.


1. 컬렉션의 종류

C#에서 제공하는 주요 컬렉션은 크게 List, ArrayList, Dictionary, HashSet, Queue, Stack 등이 있습니다.
이들 컬렉션은 각기 다른 데이터 구조와 성능 특성을 가지고 있습니다.

List

  • 장점: 인덱스를 통한 빠른 접근이 가능하고, 크기가 동적으로 조정됩니다.
  • 단점: 중간에 데이터를 삽입하거나 삭제할 경우 성능이 저하될 수 있습니다.

ArrayList

  • 장점: 다양한 타입의 데이터를 저장할 수 있습니다.
  • 단점: 비권장 자료구조입니다. 제네릭을 지원하지 않기 때문에, 박싱 및 언박싱으로 인해 성능이 저하될 수 있습니다.

Dictionary

  • 장점: 키-값 쌍을 사용하여 빠른 조회가 가능합니다.
  • 단점: 삽입 및 삭제 시 오버헤드가 발생할 수 있습니다.

HashSet

  • 장점: 중복 없는 요소를 저장하고, 빠른 검색을 제공합니다.
  • 단점: 해시 충돌이 발생할 경우 성능 저하가 발생할 수 있습니다.

Queue 및 Stack

  • 장점: Queue는 선입선출(FIFO), Stack은 후입선출(LIFO)의 구조를 지원하여 특정 시나리오에 적합합니다.
  • 단점: 요소 삽입 및 제거가 특정 방향으로만 이루어지므로, 다양한 데이터 구조에서의 효율성은 떨어질 수 있습니다.

2. 성능 비교

컬렉션의 성능은 데이터의 삽입, 삭제, 검색, 접근과 같은 연산에 따라 달라집니다.
여기서는 각 컬렉션의 성능을 살펴보겠습니다.

List의 성능

List<T>는 배열 기반으로 구현되어 있으며, 인덱스를 사용하여 데이터에 접근할 때 O(1)의 성능을 보입니다. 그러나 중간에 요소를 삽입하거나 삭제할 경우, 데이터를 이동해야 하므로 O(n)의 성능을 가집니다.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 데이터 삽입: O(n)
numbers.Insert(2, 10);  // 인덱스 2에 삽입

// 데이터 접근: O(1)
int num = numbers[2];

Dictionary의 성능

Dictionary<TKey, TValue>는 해시 테이블 기반이기 때문에 대부분의 경우 삽입, 삭제, 조회 연산이 O(1)의 성능을 보입니다. 그러나 해시 충돌이 발생하면 성능이 O(n)으로 떨어질 수 있습니다.

Dictionary<string, int> ages = new Dictionary<string, int>();

// 데이터 삽입: O(1)
ages.Add("Alice", 25);

// 데이터 조회: O(1)
int age = ages["Alice"];

HashSet의 성능

HashSet<T> 역시 해시 테이블을 사용하므로 삽입, 삭제, 조회 연산이 O(1)입니다. 다만, 중복된 값을 허용하지 않기 때문에 데이터 중복 검사가 필요할 때 유리합니다.

HashSet<int> uniqueNumbers = new HashSet<int>();

// 데이터 삽입: O(1)
uniqueNumbers.Add(5);

// 데이터 중복 검사
bool exists = uniqueNumbers.Contains(5);  // O(1)

3. 메모리 관리

컬렉션은 성능뿐만 아니라 메모리 사용도 중요하게 고려해야 합니다. 특히, 동적 메모리 할당이 자주 발생하는 컬렉션에서는 메모리 관리가 성능에 큰 영향을 미칩니다.

1) 메모리 증가

List<T>와 같은 동적 배열은 데이터가 추가될 때 용량이 부족하면 배열의 크기를 두 배로 확장합니다. 이 과정에서 새로운 배열을 할당하고 기존 데이터를 복사해야 하므로, 많은 요소가 추가되는 상황에서는 메모리 오버헤드가 발생할 수 있습니다.

List<int> numbers = new List<int>();

// 1000개의 요소 추가
for (int i = 0; i < 1000; i++)
{
    numbers.Add(i);
}

2) 박싱 및 언박싱

ArrayList와 같은 비제네릭 컬렉션에서는 박싱(Boxing) 및 **언박싱(Unboxing)**이 빈번하게 발생할 수 있습니다. 이는 참조형과 값형 데이터를 컬렉션에 저장할 때, 값을 힙에 복사하고 포인터로 참조하기 때문에 메모리 사용량이 증가하고 성능이 저하됩니다.

ArrayList list = new ArrayList();
list.Add(10);  // Boxing 발생

int value = (int)list[0];  // Unboxing 발생

3) 메모리 누수 방지

컬렉션을 사용하다 보면 불필요한 데이터가 여전히 메모리에 남아 있는 메모리 누수 문제가 발생할 수 있습니다. 이를 방지하기 위해 Clear() 메서드를 사용하여 컬렉션의 데이터를 명시적으로 삭제하는 것이 좋습니다.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 메모리 해제를 위해 컬렉션 비우기
numbers.Clear();

 


4. 적절한 컬렉션 선택

컬렉션을 선택할 때는 성능과 메모리 사용량을 동시에 고려해야 합니다. 다음은 상황에 맞는 컬렉션 선택 가이드입니다.

  • 빠른 조회가 필요한 경우: Dictionary<TKey, TValue>나 HashSet<T>와 같은 해시 기반 컬렉션을 사용하는 것이 좋습니다.
  • 중복 없는 데이터가 필요한 경우: HashSet<T>는 데이터 중복을 허용하지 않기 때문에 유리합니다.
  • 데이터를 자주 삽입/삭제하는 경우: LinkedList<T>와 같은 링크드 리스트 기반 구조가 효율적일 수 있습니다.
  • 순차적 데이터 처리: Queue<T>와 Stack<T>는 순차적으로 데이터를 처리하는 작업에서 성능이 좋습니다.

 

컬렉션을 사용할 때는 성능과 메모리 사용에 대한 이해가 필수적입니다.
각 컬렉션의 특성과 상황에 맞는 사용법을 익히면, 메모리 관리와 성능 최적화를 동시에 달성할 수 있습니다.
이 글에서 소개한 내용은 다양한 컬렉션 선택과 관리에 있어 중요한 기준을 제공하므로, 프로젝트에 맞는 컬렉션을 선택하는 데 도움이 될 것입니다.