Dictionary

Dictionary<TKey, TValue>는 .NET에서 제공하는 제네릭 컬렉션 클래스로, 키-값 쌍을 효율적으로 저장하고 관리할 수 있는 자료 구조입니다. HashTable을 기반으로 하여 키를 사용한 빠른 검색, 삽입, 삭제 작업을 지원합니다. System.Collections.Generic 네임스페이스에 포함되어 있으며, 키를 통해 값에 접근하는 방식으로 데이터를 관리합니다.

Dictionary<TKey, TValue> 개요

Dictionary란 무엇인가?

  • Dictionary는 키Key와 값Value 쌍으로 데이터를 저장하는 자료 구조입니다.
  • 해시 테이블Hash Table을 기반으로 하여, 키를 사용한 빠른 데이터 접근이 가능합니다.
  • 각 키는 고유해야 하며, 하나의 키는 하나의 값에 대응합니다.

Dictionary를 언제 사용해야 하는가?

  • 빠른 검색이 필요한 경우: 키를 사용하여 값에 빠르게 접근해야 할 때.
  • 키-값 매핑이 필요한 경우: 특정 키에 대한 값을 저장하고 조회해야 할 때.
  • 데이터의 순서가 중요하지 않은 경우: 요소의 순서가 중요하지 않고, 키를 통해 데이터에 접근하면 되는 경우.

Dictionary의 기본 사용법

Dictionary<TKey, TValue>를 사용하면 키와 값을 명시적으로 지정하여 타입 안전성을 보장받을 수 있습니다.

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        Dictionary<int, string> people = new Dictionary<int, string>();
        people.Add(1, "Alice");
        people.Add(2, "Bob");
        people.Add(3, "Charlie");
		Console.WriteLine($"FirstMember: {people[1]}");
        // 요소 출력
        foreach (KeyValuePair<int, string> person in people)
        {
            Console.WriteLine($"ID: {person.Key}, Name: {person.Value}");
        }
        // 출력:
        // FirstMember: Alice
        // ID: 1, Name: Alice
        // ID: 2, Name: Bob
        // ID: 3, Name: Charlie
    }
}

Dictionary<TKey, TValue>의 특징

  • 빠른 검색 성능: 해시 테이블을 사용하므로, 평균적으로 O(1)의 시간 복잡도로 요소를 검색, 삽입, 삭제할 수 있습니다.
  • 유일한 키: 모든 키는 고유해야 하며, 중복된 키를 사용할 수 없습니다.
  • 타입 안전성: 제네릭 타입을 사용하여 키와 값의 타입을 명시적으로 지정할 수 있습니다.

장점

  • 고성능: 대량의 데이터를 관리할 때도 빠른 성능을 제공합니다.
  • 유연성: 다양한 타입의 키와 값을 사용할 수 있습니다.
  • LINQ 지원: LINQ를 통해 데이터 쿼리와 조작이 가능합니다.

단점

  • 메모리 사용량: 해시 테이블을 유지하기 위해 추가 메모리를 사용합니다.
  • 순서 관리 불가: 입력한 순서를 유지해야 한다면 다른 컬렉션이 더 적합합니다.
  • 스레드 안전하지 않음: 기본적으로 스레드 안전성을 제공하지 않으므로, 다중 스레드 환경에서 주의가 필요합니다.

주요 메서드와 속성

생성자와 초기화

기본 생성자

빈 딕셔너리를 생성합니다.

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

초기 용량 지정

딕셔너리의 초기 용량을 설정하여 성능을 최적화할 수 있습니다.

Dictionary<int, string> people = new Dictionary<int, string>(capacity: 100);
  • 초기 용량 설정을 통한 최적화를 참조하세요.

컬렉션 초기화 사용

Dictionary<int, string> people = new Dictionary<int, string>
{
    { 1, "Alice" },
    { 2, "Bob" },
    { 3, "Charlie" }
};

요소 추가

Add

키-값 쌍을 딕셔너리에 추가합니다.

people.Add(4, "David");
  • 이미 존재하는 키를 추가하려고 하면 ArgumentException이 발생합니다.

인덱서를 사용한 추가

people[5] = "Eve";
  • 키가 존재하지 않으면 새로운 키-값 쌍이 추가됩니다.

요소 접근 및 수정

인덱서를 통한 접근

특정 키에 해당하는 값을 가져오거나 설정합니다.

string name = people[1];
people[2] = "Bob Updated";
  • 존재하지 않는 키에 접근하려고 하면 KeyNotFoundException이 발생합니다.

ContainsKey

딕셔너리에 특정 키가 존재하는지 확인합니다.

bool hasKey = people.ContainsKey(3);

TryGetValue

특정 키에 해당하는 값을 안전하게 가져옵니다.

if (people.TryGetValue(3, out string value))
{
    Console.WriteLine(value);
}
else
{
    Console.WriteLine("키가 존재하지 않습니다.");
}
  • 키가 존재하지 않을 경우에도 예외가 발생하지 않으므로 안전한 접근 방법입니다.

요소 제거

Remove

특정 키에 해당하는 키-값 쌍을 제거합니다.

people.Remove(2);
  • 제거하려는 키가 존재하지 않아도 예외가 발생하지 않습니다.

Clear

딕셔너리의 모든 요소를 제거합니다.

people.Clear();

기타 유용한 메서드와 속성

Keys 및 Values 활용

KeysValues 속성을 사용하여 딕셔너리의 모든 키 또는 값을 가져올 수 있습니다. 이를 활용해 특정 작업을 수행하거나 조건에 맞는 데이터를 추출할 수 있습니다.

Dictionary<int, string> people = new Dictionary<int, string>
{
    { 1, "Alice" },
    { 2, "Bob" },
    { 3, "Charlie" }
};
// 모든 키 출력
foreach (int key in people.Keys)
{
    Console.WriteLine($"Key: {key}");
}
// 모든 값 출력
foreach (string value in people.Values)
{
    Console.WriteLine($"Value: {value}");
}
// 특정 조건에 맞는 값 추출
var filteredValues = people.Values.Where(v => v.StartsWith("A"));
foreach (var value in filteredValues)
{
    Console.WriteLine($"Filtered Value: {value}");
}

KeysValues 컬렉션은 각각 키와 값에 대한 반복 작업을 쉽게 수행할 수 있게 해주며, 조건에 맞는 데이터 필터링 등의 작업에 유용합니다.

Count

딕셔너리에 포함된 요소의 개수를 반환합니다.

int count = people.Count;

일반적인 사용 사례

데이터 매핑 및 조회

Dictionary는 데이터 매핑을 위한 도구로 매우 유용합니다. 예를 들어, 학생 ID와 이름을 매핑하거나, 제품 코드와 가격을 매핑하는 데 사용할 수 있습니다.

Dictionary<string, decimal> productPrices = new Dictionary<string, decimal>
{
    { "ProductA", 10.99m },
    { "ProductB", 5.49m },
    { "ProductC", 20.00m }
};
if (productPrices.TryGetValue("ProductB", out decimal price))
{
    Console.WriteLine($"ProductB의 가격: {price}");
}

캐싱

Dictionary는 데이터에 대한 캐시를 구현하는 데 자주 사용됩니다. 예를 들어, 계산 결과를 저장하여 동일한 계산을 반복하지 않도록 하는 방식으로 성능을 개선할 수 있습니다.

Dictionary<int, int> fibonacciCache = new Dictionary<int, int>();
int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    if (fibonacciCache.TryGetValue(n, out int cachedValue))
        return cachedValue;
    int value = Fibonacci(n - 1) + Fibonacci(n - 2);
    fibonacciCache[n] = value;
    return value;
}

반복 호출이 빈번한 경우에도, 캐싱을 통해 비용을 줄일 수 있습니다.

private static readonly int _myStructSize = Marshal.SizeOf<MyStruct>();
public void ProcessData()
{
    for (int i = 0; i < dataList.Count; i++)
    {
        int size = _myStructSize; // 캐시된 값을 사용하여 성능 최적화
        // 데이터 처리 로직...
    }
}

Dictionary<TKey, TValue>활용

ContainsKey 와 TryGetValue

코드의 작동 방식

if (dictionary.ContainsKey(1)) 
    Console.WriteLine(dictionary[1]);
  • 이 코드는 먼저 dictionary.ContainsKey(1)로 키가 존재하는지 확인한 후, 존재하면 dictionary[1]을 사용하여 값을 가져옵니다.
  • 이 경우 딕셔너리는 두 번 검색됩니다:
    • ContainsKey로 키가 존재하는지 확인할 때 한 번.
    • 인덱서를 통해 값을 가져올 때 한 번.
if (dictionary.TryGetValue(1, out string str))
    Console.WriteLine(str);
  • 이 코드는 TryGetValue를 사용하여 키가 존재하면 값을 반환하고, 키가 존재하지 않으면 기본값(null 또는 default(TValue))을 반환합니다.
  • 이 방식은 한 번의 검색만 수행되며, 키가 존재하면 str 변수에 값을 할당하고, 그 값을 사용합니다.

성능 차이

  • ContainsKey + 인덱서 접근 방식은 딕셔너리를 두 번 검색하므로, 최악의 경우 동일한 키에 대해 두 번의 해시 계산과 해시 버킷 접근이 필요합니다. 따라서 키를 찾고 그 값을 사용하는데 평균적으로 O(2)의 비용이 발생합니다.
  • TryGetValue방식은 키에 대해 딕셔너리를 한 번만 검색하므로, 한 번의 해시 계산과 접근만으로 값을 가져올 수 있습니다. 따라서 평균적으로 O(1)의 비용이 발생합니다.
  • 따라서 TryGetValue를 사용하는 것이 성능적으로 더 효율적입니다.

스레드 안전성

Dictionary는 기본적으로 스레드 안전하지 않으므로, 다중 스레드 환경에서 사용 시 동기화가 필요합니다.

private static Dictionary<int, string> sharedDictionary = new Dictionary<int, string>();
private static readonly object lockObject = new object();
static void AddToSharedDictionary(int key, string value)
{
    lock (lockObject)
    {
        sharedDictionary[key] = value;
    }
}

스레드 안전한 딕셔너리가 필요하다면 ConcurrentDictionary를 사용하는 것이 좋습니다.

LINQ와의 통합

Dictionary는 LINQ와 결합하여 데이터를 쉽게 필터링하고 검색할 수 있습니다.

var filtered = people.Where(p => p.Key > 1).Select(p => p.Value);
foreach (var name in filtered)
{
    Console.WriteLine(name);
    // 출력:
    // Bob
    // Charlie
}

Dictionary는 키-값 쌍으로 데이터를 관리하는 데 최적화된 컬렉션으로, 빠른 검색과 효율적인 데이터 매핑을 필요로 하는 상황에서 매우 유용합니다. 다만, 스레드 안전성과 메모리 사용량을 고려하여 적절한 상황에서 사용해야 합니다.

해시 함수와 충돌 처리

IEqualityComparer 사용

기본적으로 Dictionary는 키의 동등성 비교를 위해 Object.GetHashCode()Object.Equals()를 사용합니다. 그러나 커스텀 비교가 필요한 경우, IEqualityComparer<TKey> 인터페이스를 구현하여 딕셔너리의 키 비교 방식을 사용자 정의할 수 있습니다. 예를 들어, 대소문자를 구분하지 않는 문자열 키를 사용하는 딕셔너리를 만들고 싶다면 다음과 같이 할 수 있습니다:

class CaseInsensitiveComparer : IEqualityComparer<string>
{
    public bool Equals(string x, string y)
    {
        return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
    }
    public int GetHashCode(string obj)
    {
        return obj.ToLower().GetHashCode();
    }
}
Dictionary<string, string> caseInsensitiveDict = new Dictionary<string, string>(new CaseInsensitiveComparer());
caseInsensitiveDict.Add("Key1", "Value1");
Console.WriteLine(caseInsensitiveDict.ContainsKey("key1")); // True

이를 통해 사용자 정의 동등성 비교를 제공하고, 키의 비교 방식을 더욱 유연하게 설정할 수 있습니다.