제네릭과 제네릭 컬렉션 개요

제네릭과 제네릭 컬렉션 개요

프로그래밍에서 제네릭은 코드의 재사용성과 안전성을 높이기 위해 매우 중요한 개념입니다. .NET에서 제네릭Generic은 특정 데이터 타입에 구애받지 않고 동작하는 일반화된 클래스나 메서드를 정의할 수 있게 해줍니다. 이를 통해 코드 중복을 줄이고 타입 안전성을 강화할 수 있습니다.

제네릭의 필요성

제네릭의 개념은 코드의 재사용성과 타입 안전성을 강화하기 위한 필요에서 발전되었습니다. .NET Framework 1.0에서는 비제네릭 컬렉션(ArrayList, Hashtable 등)을 사용했지만, 이러한 컬렉션은 데이터가 object 타입으로 저장되어 타입 안전성을 보장하지 못하고, 값 타입을 저장할 때 박싱과 언박싱이 발생하여 성능 저하가 있었습니다. 이러한 문제를 해결하기 위해 .NET Framework 2.0에서 제네릭이 도입되었으며, 이를 통해 데이터 타입을 명시적으로 지정할 수 있게 되어 성능과 안전성 모두가 향상되었습니다.

제네릭의 개념

제네릭은 타입 매개변수Type Parameter를 사용하여 특정 데이터 타입에 구애받지 않는 클래스나 메서드를 정의하는 것을 의미합니다. 타입 매개변수는 클래스나 메서드를 정의할 때 타입을 미리 지정하지 않고, 나중에 사용할 때 구체적인 타입을 지정할 수 있는 ‘미지정 타입’입니다. 이를 통해 동일한 로직을 다양한 데이터 타입에 적용할 수 있습니다. 타입 매개변수의 원형은 보통 T로 표기되며, TKey, TValue와 같이 의미를 명확히 하기 위해 다른 이름으로도 사용할 수 있습니다. 이 매개변수는 클래스나 메서드를 사용할 때 구체적인 타입으로 대체됩니다. 예를 들어, List는 타입 매개변수 T를 사용하여(List<T>) 다양한 데이터 타입을 저장할 수 있습니다.

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
List<string> words = new List<string>();
words.Add("hello");
words.Add("world");

위의 예제에서 List<int>는 정수를 저장하고, List<string>은 문자열을 저장하도록 정의되어 있습니다. 제네릭을 사용하면 타입에 구애받지 않고도 안전하게 데이터를 관리할 수 있는 유연한 방법을 제공합니다.

제네릭의 특성

타입 안전성 강화

제네릭을 사용하면 특정 타입을 명시적으로 지정할 수 있으므로, 컴파일 타임에 타입 검사를 수행하여 런타임 오류를 방지할 수 있습니다. 예를 들어, List<int>에 문자열을 추가하려고 하면 컴파일 오류가 발생합니다. 이는 잘못된 타입 사용을 사전에 방지하여 프로그램의 안정성을 높이는 데 기여합니다.

List<int> numbers = new List<int>();
// numbers.Add("hello"); // 컴파일 오류 발생

타입 제약 조건 설정

제네릭에서는 타입 매개변수에 제약 조건Constraints을 설정하여 사용할 수 있는 타입을 제한할 수 있습니다. 이를 통해 제네릭 클래스나 메서드 내에서 해당 타입의 특성을 안전하게 사용할 수 있습니다.

기본 제약 조건

  • where T : class - T는 참조 타입이어야 합니다.
  • where T : struct - T는 값 타입이어야 합니다.
  • where T : new() - T는 매개변수가 없는 기본 생성자가 있어야 합니다.
  • where T : BaseClass - TBaseClass이거나 그 파생 클래스여야 합니다.
  • where T : Interface - TInterface를 구현해야 합니다. 예를 들어
public class Repository<T> where T : class, new()
{
    public T CreateInstance()
    {
        return new T();
    }
}

위 코드에서는 T가 참조 타입이면서 매개변수가 없는 기본 생성자가 있어야 함을 제약하고 있습니다.

public T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

위 코드에서는 TIComparable<T> 인터페이스를 구현해야 한다는 제약 조건을 두고 있습니다. 따라서 IComparable<T>를 구현한 타입에 대해서만 동작하며, 두 값 중 큰 값을 반환합니다.

제네릭 메서드

제네릭은 클래스뿐만 아니라 메서드에서도 사용할 수 있습니다. 메서드에 제네릭을 사용하면 메서드 단위로 타입 매개변수를 지정할 수 있습니다.

public void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

위 메서드는 두 변수의 값을 교환하는 기능을 하며, 어떤 타입이든 사용할 수 있습니다.

박싱과 언박싱 문제 해결

비제네릭 컬렉션에서 값 타입을 저장할 때 발생하는 박싱Boxing과 언박싱Unboxing은 불필요한 메모리 할당과 해제를 유발하여 성능 저하를 일으킵니다. 그러나 제네릭을 사용하면 이러한 박싱과 언박싱이 필요 없으므로 메모리 사용 효율이 높아지고, 성능이 향상됩니다.

비제네릭 사용 시

ArrayList list = new ArrayList();
list.Add(42); // 박싱 발생
int value = (int)list[0]; // 언박싱 발생

제네릭 사용 시

List<int> list = new List<int>();
list.Add(42); // 박싱 없음
int value = list[0]; // 언박싱 없음

코드 재사용성

제네릭을 사용하면 같은 로직을 다양한 타입에 적용할 수 있어 코드 중복을 줄이고 코드 재사용성을 높일 수 있습니다. 예를 들어, 같은 기능을 수행하는 메서드를 여러 데이터 타입에 대해 작성할 필요 없이, 제네릭을 통해 타입에 상관없이 동일한 로직을 적용할 수 있습니다.

public class Stack<T>
{
    private T[] elements;
    private int position = 0;
    public Stack(int size)
    {
        elements = new T[size];
    }
    public void Push(T item)
    {
        elements[position++] = item;
    }
    public T Pop()
    {
        return elements[--position];
    }
}

위의 Stack<T> 클래스는 어떤 타입의 데이터든 스택으로 관리할 수 있습니다.

제네릭 컬렉션의 종류와 특징

.NET에서 자주 사용되는 제네릭 컬렉션은 다음과 같습니다.

  • List : 가변 크기의 배열과 같은 기능을 제공하며, 삽입, 삭제, 검색이 매우 용이합니다.
  • Dictionary : 키-값 쌍으로 데이터를 관리하는 해시 테이블 기반의 컬렉션으로, 빠른 검색과 추가가 가능하여 빠른 데이터 매핑에 적합합니다.
  • Queue : FIFOFirst In First Out 방식으로 데이터를 관리하는 컬렉션으로, 데이터를 순차적으로 처리하는 데 유용합니다.
  • Stack : LIFOLast In First Out 방식으로 데이터를 관리하는 컬렉션으로, 후입선출 방식의 데이터 관리에 사용됩니다.
  • HashSet : 중복을 허용하지 않는 데이터의 집합으로, 고유한 데이터 관리에 적합합니다.

제네릭 컬렉션 사용 시 유의사항

스레드 안전성

제네릭 컬렉션은 스레드 안전하지 않으므로 다중 스레드 환경에서 동기화가 필요합니다. List<T>의 경우를 예로 들면 다음과 같습니다.

private static List<int> sharedList = new List<int>();
private static readonly object lockObject = new object();
static void AddToSharedList(int value)
{
    lock (lockObject)
    {
        sharedList.Add(value);
    }
}

Dictionary<TKey, TValue> 의 경우는 다음과 같습니다.

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;
    }
}

그러나 .NET에서는 다중 스레드 환경에서 안전하게 사용할 수 있는 스레드 안전한 컬렉션을 제공하고 있습니다.

스레드 안전한 컬렉션

컬렉션 초기 용량 설정

제네릭 컬렉션의 초기 용량 설정 을 참조하세요

공변성과 반공변성

제네릭은 인터페이스와 델리게이트에서 공변성Covariance과 반공변성Contravariance을 지원합니다. 이는 제네릭 타입 매개변수가 상속 관계를 따를 때 캐스팅을 허용하는 기능입니다.

공변성

  • 정의: 제네릭 인터페이스나 델리게이트에서 아웃풋 위치의 타입 매개변수가 변환될 때 사용됩니다.
  • 예시: IEnumerable<out T>는 공변성을 지원하여 IEnumerable<string>IEnumerable<object>로 캐스팅할 수 있습니다.
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 허용됨

반공변성

  • 정의: 제네릭 인터페이스나 델리게이트에서 인풋 위치의 타입 매개변수가 변환될 때 사용됩니다.
  • 예시: IComparer<in T>는 반공변성을 지원하여 IComparer<object>IComparer<string>으로 캐스팅할 수 있습니다.
IComparer<object> objectComparer = ...;
IComparer<string> stringComparer = objectComparer; // 허용됨