제네릭과 제네릭 컬렉션 개요
프로그래밍에서 제네릭은 코드의 재사용성과 안전성을 높이기 위해 매우 중요한 개념입니다. .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
-T
는BaseClass
이거나 그 파생 클래스여야 합니다.where T : Interface
-T
는Interface
를 구현해야 합니다. 예를 들어
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;
}
위 코드에서는 T
가 IComparable<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에서는 다중 스레드 환경에서 안전하게 사용할 수 있는 스레드 안전한 컬렉션을 제공하고 있습니다.
스레드 안전한 컬렉션
- ConcurrentDictionary
- ConcurrentQueue
- ConcurrentStack
- ConcurrentBag 이러한 컬렉션은 다중 스레드 환경에서 동기화 없이도 안전하게 사용할 수 있도록 설계되었습니다.
컬렉션 초기 용량 설정
제네릭 컬렉션의 초기 용량 설정 을 참조하세요
공변성과 반공변성
제네릭은 인터페이스와 델리게이트에서 공변성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; // 허용됨