C# Generics & Constraints 심층 분석
제네릭이 필요한 이유
섹션 제목: “제네릭이 필요한 이유”제네릭(Generics) 이전에는 object 기반 컬렉션을 사용했습니다. 이는 박싱/언박싱 비용과 런타임 타입 캐스트 오류를 유발했습니다.
// 제네릭 이전: object 기반 (비효율, 타입 안전 없음)ArrayList list = new ArrayList();list.Add(42); // 박싱 발생int val = (int)list[0]; // 언박싱 + 캐스트
// 제네릭: 타입 안전, 박싱 없음List<int> typed = new List<int>();typed.Add(42);int val2 = typed[0];제네릭 클래스와 메서드
섹션 제목: “제네릭 클래스와 메서드”// 제네릭 클래스public class Repository<T> where T : class{ private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item); public T? Find(Func<T, bool> predicate) => _items.FirstOrDefault(predicate); public IReadOnlyList<T> GetAll() => _items.AsReadOnly();}
// 제네릭 메서드public static T Max<T>(T a, T b) where T : IComparable<T>{ return a.CompareTo(b) >= 0 ? a : b;}
Console.WriteLine(Max(3, 7)); // 7Console.WriteLine(Max("apple", "orange")); // orange타입 제약 조건 (Constraints)
섹션 제목: “타입 제약 조건 (Constraints)”where 키워드로 타입 매개변수에 제약을 걸어 컴파일 타임 안전성을 높입니다.
// where T : struct — 값 타입만 허용public struct NullableValue<T> where T : struct{ private T _value; public bool HasValue { get; private set; } public T Value => HasValue ? _value : throw new InvalidOperationException();}
// where T : class — 참조 타입만 허용public class Cache<T> where T : class{ // T는 null 가능}
// where T : new() — 매개변수 없는 생성자 필수public static T CreateInstance<T>() where T : new(){ return new T();}
// where T : SomeBaseClass — 특정 기반 클래스 요구public void Process<T>(T entity) where T : EntityBase, IValidatable{ entity.Validate(); // IValidatable 멤버 호출 가능 entity.Id; // EntityBase 멤버 접근 가능}
// 다중 제약 조건public static T DeepCopy<T>(T source) where T : class, ICloneable, new(){ return (T)source.Clone();}제약 조건 종류 요약
섹션 제목: “제약 조건 종류 요약”| 제약 | 의미 |
|---|---|
where T : struct | 값 타입 (int, struct 등) |
where T : class | 참조 타입 (class, interface, delegate) |
where T : notnull | null 불가 (C# 8+) |
where T : unmanaged | 비관리 타입 (포인터 연산 가능) |
where T : new() | 기본 생성자 보유 |
where T : BaseClass | 특정 기반 클래스 상속 |
where T : IInterface | 인터페이스 구현 |
where T : U | T는 U이거나 U의 파생 타입 |
공변성 (Covariance) — out
섹션 제목: “공변성 (Covariance) — out”out 키워드는 타입 매개변수를 반환 위치에만 사용하도록 제한하며, 더 파생된 타입을 더 기반 타입으로 암묵적 변환을 허용합니다.
// IEnumerable<T>는 out T로 선언됨: IEnumerable<out T>IEnumerable<string> strings = new List<string> { "hello" };
// string은 object의 파생 타입이므로 공변성으로 허용IEnumerable<object> objects = strings; // OK
// 직접 선언interface IProducer<out T>{ T Produce(); // 반환 위치: OK // void Consume(T item); // 매개변수 위치: 컴파일 오류}
class StringProducer : IProducer<string>{ public string Produce() => "hello";}
IProducer<string> strProd = new StringProducer();IProducer<object> objProd = strProd; // 공변성: OK반변성 (Contravariance) — in
섹션 제목: “반변성 (Contravariance) — in”in 키워드는 타입 매개변수를 입력 위치에만 사용하도록 제한하며, 더 기반 타입을 더 파생 타입으로 암묵적 변환을 허용합니다.
// Action<T>는 in T로 선언됨: Action<in T>Action<object> objAction = obj => Console.WriteLine(obj);
// object를 받는 액션은 string도 처리 가능 → 반변성Action<string> strAction = objAction; // OK
// 직접 선언interface IConsumer<in T>{ void Consume(T item); // 입력 위치: OK // T Produce(); // 반환 위치: 컴파일 오류}
class ObjectConsumer : IConsumer<object>{ public void Consume(object item) => Console.WriteLine(item);}
IConsumer<object> objCons = new ObjectConsumer();IConsumer<string> strCons = objCons; // 반변성: OKstrCons.Consume("hello");제네릭과 런타임 (CLR)
섹션 제목: “제네릭과 런타임 (CLR)”값 타입 특화
섹션 제목: “값 타입 특화”CLR은 값 타입(int, double, struct) 제네릭에 대해 별도의 JIT 컴파일 코드를 생성합니다. 박싱 없이 실행됩니다.
// List<int>와 List<double>은 별도의 JIT 코드 생성// 박싱 없음 → 고성능var ints = new List<int>();var doubles = new List<double>();참조 타입 공유
섹션 제목: “참조 타입 공유”참조 타입 제네릭은 하나의 JIT 코드를 공유합니다. List<string>과 List<object>는 같은 JIT 코드를 사용합니다.
제네릭 특화 패턴
섹션 제목: “제네릭 특화 패턴”제네릭 타입 검사로 타입별 최적화를 구현할 수 있습니다.
public static class Serializer{ public static string Serialize<T>(T value) { // 타입별 특화 처리 if (value is int i) return i.ToString(); if (value is string s) return $"\"{s}\""; if (value is IEnumerable<object> enumerable) return $"[{string.Join(",", enumerable.Select(Serialize))}]";
return value?.ToString() ?? "null"; }}static abstract 인터페이스 멤버 (C# 11+)
섹션 제목: “static abstract 인터페이스 멤버 (C# 11+)”// 수치 연산을 제네릭으로 표현public interface IAddable<T> where T : IAddable<T>{ static abstract T operator +(T left, T right); static abstract T Zero { get; }}
public static T Sum<T>(IEnumerable<T> source) where T : IAddable<T>{ var result = T.Zero; foreach (var item in source) result = result + item; return result;}제네릭 딕셔너리 패턴
섹션 제목: “제네릭 딕셔너리 패턴”타입을 키로 사용하는 타입 안전 컨테이너를 구현합니다.
public class TypedContainer{ private readonly Dictionary<Type, object> _store = new();
public void Set<T>(T value) => _store[typeof(T)] = value!;
public T Get<T>() { if (_store.TryGetValue(typeof(T), out var value)) return (T)value; throw new KeyNotFoundException($"{typeof(T).Name} not found"); }
public bool TryGet<T>(out T? value) { if (_store.TryGetValue(typeof(T), out var obj)) { value = (T)obj; return true; } value = default; return false; }}
// 사용var container = new TypedContainer();container.Set<string>("hello");container.Set<int>(42);
Console.WriteLine(container.Get<string>()); // helloConsole.WriteLine(container.Get<int>()); // 42제네릭 메서드 추론
섹션 제목: “제네릭 메서드 추론”컴파일러는 인수 타입에서 타입 매개변수를 자동으로 추론합니다.
public static Pair<T1, T2> MakePair<T1, T2>(T1 first, T2 second) => new Pair<T1, T2>(first, second);
// 타입 명시 불필요var pair = MakePair("hello", 42); // Pair<string, int>| 개념 | 핵심 |
|---|---|
| 제약 조건 | where로 컴파일 타임 타입 안전성 보장 |
| 공변성(out) | 파생 타입 → 기반 타입 암묵적 변환 |
| 반변성(in) | 기반 타입 → 파생 타입 암묵적 변환 |
| 값 타입 특화 | CLR이 타입별 JIT 코드 생성, 박싱 없음 |
| 참조 타입 공유 | 모든 참조 타입이 동일한 JIT 코드 공유 |
제네릭의 제약 조건과 분산 규칙을 이해하면 재사용 가능하고 타입 안전한 라이브러리를 설계할 수 있습니다.