1. 제너릭(Generic) 개념 이해하기
1.1 제너릭이란?
- **제너릭(Generic)**은 하나의 클래스나 메서드를 여러 자료형에 대해 재사용할 수 있도록 만드는 문법적 기능입니다.
- 예를 들어, 정수 리스트, 문자열 리스트, 사용자 정의 타입 리스트 등을 각각 따로 만드는 대신 List<T> 한 가지로 다양한 타입을 지원할 수 있게 됩니다.
1.2 왜 제너릭을 사용할까?
- 재사용성: 같은 로직을 여러 타입에 대해 중복 작성할 필요가 없으므로 생산성이 높아집니다.
- 타입 안전성: 컴파일 시점에 타입이 결정되기 때문에, 박싱/언박싱이나 object 캐스팅 오류 등을 미연에 방지합니다.
- 성능: 박싱/언박싱이 제거되어 성능 손실이 줄어들고, 실행 중에 런타임 타입 체크 부담이 감소합니다.
2. 제너릭 타입의 예시
2.1 내장 제너릭 컬렉션
C#에서 가장 흔하게 접할 수 있는 제너릭 타입은 **컬렉션(Collection)**입니다.
- List<T>, Dictionary<TKey, TValue>, Queue<T>, Stack<T> 등
예: List<T>를 이용해 정수 목록을 다루기
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
foreach (var num in numbers)
{
Console.WriteLine(num); // 10, 20
}
- List<int>는 ‘정수 전용 리스트’가 됩니다.
- numbers에 문자열이나 다른 타입을 잘못 추가하면 컴파일 에러가 발생하므로 안전합니다.
2.2 Dictionary<TKey, TValue> 예시
Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Alice"] = 90;
scores["Bob"] = 80;
Console.WriteLine(scores["Alice"]); // 90
- scores는 키를 문자열(string)로, 값을 정수(int)로 지정했습니다.
- 다른 타입을 섞어 넣으려 하면 컴파일 시점에 에러가 납니다.
3. 제너릭 클래스 직접 만들어보기
3.1 기본 제너릭 클래스 선언
public class MyGenericClass<T>
{
private T _value;
public MyGenericClass(T initialValue)
{
_value = initialValue;
}
public T GetValue()
{
return _value;
}
}
- MyGenericClass<T>: T는 타입 매개변수(Type Parameter)입니다.
- 클래스 내부의 모든 곳에서 T라는 추상적인 타입을 사용할 수 있습니다.
사용 예시
// int형 버전
MyGenericClass<int> intBox = new MyGenericClass<int>(100);
Console.WriteLine(intBox.GetValue()); // 100
// string형 버전
MyGenericClass<string> stringBox = new MyGenericClass<string>("Hello");
Console.WriteLine(stringBox.GetValue()); // Hello
3.2 제너릭 클래스 다중 타입 매개변수
제너릭 클래스는 타입 매개변수를 여러 개 가질 수도 있습니다:
public class Pair<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
}
// 사용 예시
Pair<string, int> pair = new Pair<string, int>("Age", 25);
Console.WriteLine($"{pair.First} = {pair.Second}"); // "Age = 25"
- Pair<T1, T2>는 첫 번째 타입과 두 번째 타입을 함께 묶는 구조를 제공합니다.
- Pair<string, int> / Pair<int, double> / Pair<Foo, Bar> 등 다양한 타입 조합으로 쓸 수 있습니다.
4. 제너릭 메서드
클래스 자체가 아닌 메서드에만 제너릭을 적용할 수도 있습니다.
public static T GetMax<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
public static void Main()
{
int maxInt = GetMax(10, 20); // GetMax<int>
string maxString = GetMax("A", "B"); // GetMax<string>
Console.WriteLine(maxInt); // 20
Console.WriteLine(maxString); // B
}
- GetMax<T> 메서드는 where T : IComparable<T>라는 **제한(Constraint)**을 통해 CompareTo 메서드가 있는 타입만 받도록 합니다.
- 이렇게 제너릭 메서드를 사용하면 매개변수 타입만 다르고 로직이 같은 함수를 여러 번 오버로드할 필요가 없습니다.
5. 제너릭 제약(Constraints)
5.1 왜 제약이 필요할까?
- 제너릭은 타입에 대한 구체적인 정보를 갖고 있지 않으므로, 제너릭 내부에서 ‘필요한 멤버(예: 메서드, 프로퍼티)’를 안전하게 호출할 수 있도록 제약을 지정합니다.
5.2 주요 제약 예시
- where T : struct : T는 값 타입만 가능
- where T : class : T는 참조 타입만 가능
- where T : new() : T는 매개변수가 없는 기본 생성자를 가져야 함
- where T : BaseClass : T는 특정 클래스(또는 추상 클래스)를 상속받아야 함
- where T : InterfaceName : T는 특정 인터페이스를 구현해야 함
예시: new() 제약
public class Factory<T> where T : new()
{
public T CreateInstance()
{
return new T(); // 기본 생성자가 있는 T만 가능
}
}
- where T : new()로 제약했기 때문에, T에 매개변수가 없는 기본 생성자가 반드시 존재해야 합니다.
- 그렇지 않다면 컴파일 시점에 오류가 발생합니다.
6. 제너릭을 사용할 때 주의할 점
- 타입 안정성: 제너릭을 올바른 타입으로 선언해야 합니다. 예: List<int>에 int만 추가
- Boxing/Unboxing: 제너릭을 쓰면 일반 object 대신 구체적인 타입을 다루므로 성능 이점이 큽니다.
- 제약 사용: 제너릭 내부에서 특정 멤버를 사용해야 한다면 적절한 제약(Constraints)을 사용하세요.
- 코드 가독성: 제너릭이 과도하게 복잡해지면 오히려 가독성이 떨어질 수 있습니다. 적절히 추상화하는 것이 중요합니다.
7. 종합 예제
아래는 제너릭 클래스와 제너릭 메서드, 제약을 함께 사용하는 예시입니다.
// 특정 타입이 IComparable<T>를 구현해야만 동작하는 정렬 기능을 가진 컬렉션
public class SortableList<T> where T : IComparable<T>
{
private List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
}
public void SortItems()
{
_items.Sort(); // T가 IComparable<T>여야 .Sort() 가능
}
public void PrintAll()
{
foreach (var item in _items)
{
Console.WriteLine(item);
}
}
}
// 사용 예시
public class Program
{
public static void Main()
{
// int는 IComparable<int>를 구현하므로 사용 가능
SortableList<int> intList = new SortableList<int>();
intList.Add(30);
intList.Add(10);
intList.Add(20);
intList.SortItems();
intList.PrintAll(); // 10, 20, 30
// string도 IComparable<string>을 구현
SortableList<string> stringList = new SortableList<string>();
stringList.Add("Banana");
stringList.Add("Apple");
stringList.Add("Cherry");
stringList.SortItems();
stringList.PrintAll(); // "Apple", "Banana", "Cherry"
}
}
마무리
- C#의 제너릭(Generic) 문법은 타입의 안전성과 재사용성을 극대화해주는 중요한 기법입니다.
- 컬렉션, 메서드, 클래스 등 다양한 범위에서 제너릭을 적용할 수 있으며, 제약(Constraints)을 통해 구체적인 메서드 호출, 생성자 호출 등을 제어할 수 있습니다.
- 실제 현업 코드(예: 데이터 처리, 알고리즘 라이브러리)에서는 제너릭을 적절히 활용해 효율적이고 유지보수 쉬운 구조를 만들 수 있습니다.
Key Points
- 재사용: 하나의 제너릭 클래스/메서드로 여러 타입 지원
- 안전성: 컴파일러가 타입 불일치를 잡아냄
- 성능: 불필요한 Boxing/Unboxing 제거
- 제약(Constraints): 필요한 멤버나 생성자 호출을 위해 특정 조건을 부여
제너릭을 익숙해지면 코드 품질이 한 단계 올라갈 것이니, 꼭 직접 작성해보고 테스트해보세요!
감사합니다.
.png)
댓글
댓글 쓰기