13. 제네릭
제네릭은 '타입을 매개변수로 받는 코드' 를 작성하는 기법입니다. List<T> 같은 컬렉션이 어떻게 만들어졌는지, 직접 제네릭 클래스·메서드·제약을 정의하는 방법을 익힙니다.
이 강의에서 배우는 것
- 1제네릭 클래스와 제네릭 메서드를 정의·사용한다
- 2타입 매개변수에 **제약(`where`)** 을 거는 이유와 방법을 안다
- 3공변성(`out`) / 반변성(`in`) 의 개념을 한 줄로 이해한다
소개
`List<int>`, `Dictionary<string, int>` 처럼 `<T>` 가 붙은 타입은 모두 **제네릭(generic)** 입니다. 제네릭은 "어떤 타입에도 동작하지만, 사용 시에는 한 가지 타입으로 고정"되는 강력한 메커니즘입니다. 코드 재사용과 타입 안전성을 동시에 얻을 수 있어요.
핵심 개념
1) 제네릭 클래스
`<T>` 는 "아직 정하지 않은 타입"을 뜻하는 자리표시자(placeholder)입니다. 사용 시점에 결정됩니다.
public class Box<T>
{
public T? Value { get; set; }
}
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "안녕" };`object` 로 받는 대신 `T` 를 쓰면 **박싱/캐스팅이 필요 없고 컴파일 시점에 타입이 검사**됩니다.
2) 제네릭 메서드
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
int m = Max(3, 7); // T 가 int 로 추론됨
string s = Max("a", "z");대부분 컴파일러가 `T` 를 추론합니다. 명시할 땐 `Max<int>(3, 7)`.
3) 제약 (`where`)
| 제약 | 의미 |
|---|---|
| `where T : class` | 참조 타입만 (`string`, 클래스) |
| `where T : struct` | 값 타입만 (`int`, `bool`, 구조체) |
| `where T : new()` | 매개변수 없는 생성자 필요 |
| `where T : IComparable<T>` | 해당 인터페이스 구현 |
| `where T : SomeBase` | `SomeBase` 또는 그 자식 |
여러 개 결합: `where T : class, new()` (참조 타입 + 기본 생성자).
4) 공변성(`out`) / 반변성(`in`) 개요
간단히 말해 **"제네릭 타입 사이의 형변환을 허용"** 하는 옵션입니다.
- `IEnumerable<out T>`: `IEnumerable<Dog>` 를 `IEnumerable<Animal>` 로 사용 가능 (**공변**, 출력만 하는 경우).
- `Action<in T>`: `Action<Animal>` 을 `Action<Dog>` 로 사용 가능 (**반변**, 입력만 받는 경우).
지금은 "이런 게 있다" 정도만 기억하세요. 자세한 활용은 고급 주제입니다.
핵심 예제
예제 1 — `GenericClass` : `Box<T>` 정의·사용
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "안녕" };
Console.WriteLine($"intBox: {intBox.Value}");
Console.WriteLine($"strBox: {strBox.Value}");
public class Box<T>
{
public T? Value { get; set; }
public void Show() => Console.WriteLine($"Box 안의 값: {Value}");
}**실행 결과**
intBox: 42
strBox: 안녕**메모:** 한 번 정의한 `Box<T>` 로 모든 타입을 담을 수 있어 코드 중복이 사라집니다. `T?` 의 `?` 는 `Value` 가 초기에 `null` 일 수 있음을 표시.
예제 2 — `GenericMethod` : `Max<T>` 와 타입 추론
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
Console.WriteLine(Max(3.14, 2.71));
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}**실행 결과**
7
banana
3.14**메모:** `int`, `string`, `double` 은 모두 `IComparable<T>` 를 구현하므로 제약을 만족합니다. `CompareTo` 가 양수면 a 가 더 큼.
예제 3 — `Constraint` : `where` 제약 비교
// class + new() 제약 → 새 인스턴스 생성 가능
static T Create<T>() where T : class, new() => new T();
// struct 제약 → 값 타입만
static T DoubleIt<T>(T x) where T : struct
{
Console.WriteLine($"받은 값: {x}");
return x;
}
Person p = Create<Person>();
p.Name = "지수";
Console.WriteLine($"새로 만든 사람: {p.Name}");
DoubleIt(42);
DoubleIt(3.14);
// DoubleIt("문자열"); ← 컴파일 에러: string 은 struct 아님
public class Person
{
public string Name { get; set; } = "";
}**실행 결과**
새로 만든 사람: 지수
받은 값: 42
받은 값: 3.14**메모:** `new()` 제약 덕분에 `new T()` 호출이 가능해집니다. `struct` 제약은 컴파일 시점에 잘못된 타입을 차단합니다.
예제 4 — `Variance` : 공변성·반변성 한 눈 보기
// IEnumerable<out T> — 공변 (Dog 의 컬렉션을 Animal 컬렉션으로 사용)
IEnumerable<Dog> dogs = [new Dog("바둑이"), new Dog("뽀삐")];
IEnumerable<Animal> animals = dogs; // OK : out 덕분
foreach (Animal a in animals)
{
Console.WriteLine($"동물: {a.Name}");
}
// Action<in T> — 반변 (Animal 처리 함수를 Dog 에 사용 가능)
Action<Animal> describe = a => Console.WriteLine($"이름은 {a.Name}");
Action<Dog> describeDog = describe; // OK : in 덕분
describeDog(new Dog("초코"));
public class Animal(string name)
{
public string Name { get; } = name;
}
public class Dog(string name) : Animal(name);**실행 결과**
동물: 바둑이
동물: 뽀삐
이름은 초코**메모:** 출력 전용(`out`)은 더 일반적인 타입으로, 입력 전용(`in`)은 더 구체적인 타입으로 안전하게 바꿀 수 있습니다. 이 예제의 클래스들은 **기본 생성자(primary constructor)** 문법을 사용했어요.
전체 예제 코드 (src/)
src/Constraint/Constraint.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/Constraint/Program.cs
#nullable enable
// new() 제약 → new T() 호출 가능
Person p = Create<Person>();
p.Name = "지수";
Console.WriteLine($"새로 만든 사람: {p.Name}");
// struct 제약 → 값 타입만 허용
DoubleIt(42);
DoubleIt(3.14);
// DoubleIt("문자열"); // 컴파일 에러: string 은 struct 아님
static T Create<T>() where T : class, new() => new T();
static T DoubleIt<T>(T x) where T : struct
{
Console.WriteLine($"받은 값: {x}");
return x;
}
public class Person
{
public string Name { get; set; } = "";
}
src/GenericClass/GenericClass.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/GenericClass/Program.cs
#nullable enable
// 한 번 정의한 Box<T> 로 어떤 타입이든 담을 수 있다
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "안녕" };
Console.WriteLine($"intBox: {intBox.Value}");
Console.WriteLine($"strBox: {strBox.Value}");
intBox.Show();
strBox.Show();
public class Box<T>
{
public T? Value { get; set; }
public void Show() => Console.WriteLine($"Box 안의 값: {Value}");
}
src/GenericMethod/GenericMethod.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/GenericMethod/Program.cs
#nullable enable
// T 는 호출 시 자동 추론된다
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
Console.WriteLine(Max(3.14, 2.71));
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
src/Variance/Program.cs
#nullable enable
// IEnumerable<out T> 는 공변 → Dog 컬렉션을 Animal 컬렉션으로 사용 가능
IEnumerable<Dog> dogs = [new Dog("바둑이"), new Dog("뽀삐")];
IEnumerable<Animal> animals = dogs; // out 덕분에 OK
foreach (Animal a in animals)
{
Console.WriteLine($"동물: {a.Name}");
}
// Action<in T> 는 반변 → Animal 처리 함수를 Dog 에 사용 가능
Action<Animal> describe = a => Console.WriteLine($"이름은 {a.Name}");
Action<Dog> describeDog = describe; // in 덕분에 OK
describeDog(new Dog("초코"));
public class Animal(string name)
{
public string Name { get; } = name;
}
public class Dog(string name) : Animal(name);
src/Variance/Variance.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
자주 하는 실수
- `new T()` 를 호출하려고 했는데 `new()` 제약이 없어 컴파일 에러.
- `where T : IComparable` (제네릭 없음) 과 `where T : IComparable<T>` 를 혼동.
- 제네릭 클래스의 정적 필드는 **타입별로 따로** 존재 — `Box<int>.X` 와 `Box<string>.X` 는 다른 변수.
- 모든 곳에 `<T>` 를 남발 — 한 가지 타입만 쓰는 곳은 그냥 구체 타입이 더 읽기 좋음.
- `out`/`in` 키워드를 클래스에 붙이려 함 — 변성은 **인터페이스와 델리게이트에만** 적용됩니다.
정리
- 제네릭은 타입을 매개변수처럼 받아 **재사용 가능한 타입 안전 코드**를 만든다.
- `where` 제약으로 `T` 의 능력을 제한·보장할 수 있다.
- `out` 은 공변(출력), `in` 은 반변(입력) — 인터페이스/델리게이트 전용.
- `List<T>`, `Dictionary<K,V>` 등 표준 컬렉션이 모두 제네릭의 결실이다.
과제
**과제 - 13. 제네릭**
문제 1 — 제네릭 스택 직접 구현
- 프로젝트 폴더: `Homework01/`
- 핵심 개념: 제네릭 클래스, 내부에 `List<T>` 사용
요구사항
- `MyStack<T>` 클래스를 정의한다. 내부에 `List<T>` 를 두고 다음을 제공:
- `void Push(T item)`
- `T Pop()` — 비어 있으면 `InvalidOperationException` 던지기
- `T Peek()` — 비어 있으면 `InvalidOperationException`
- `int Count` (프로퍼티)
- `MyStack<int>` 와 `MyStack<string>` 두 가지를 만들어 동작을 확인한다.
예상 출력
=== int 스택 ===
Push: 1, 2, 3
Peek: 3
Pop: 3
Pop: 2
남은 개수: 1
=== string 스택 ===
Push: A, B
Pop: B
Pop: A힌트
- `List<T>` 의 마지막 원소를 사용/제거하면 스택이 된다.
- `list[^1]` 로 마지막 원소 접근.
---
문제 2 — 제네릭 `Swap<T>` 메서드
- 프로젝트 폴더: `Homework02/`
- 핵심 개념: 제네릭 메서드, `ref` 매개변수
요구사항
- `static void Swap<T>(ref T a, ref T b)` 를 작성한다.
- `int` 두 변수와 `string` 두 변수에 대해 호출해 결과를 출력한다.
예상 출력
교환 전: a=10, b=20
교환 후: a=20, b=10
교환 전: s1=hello, s2=world
교환 후: s1=world, s2=hello힌트
- `ref` 매개변수는 호출 측도 `ref` 키워드를 붙여 전달.
- 한 줄 임시 변수면 충분: `T tmp = a; a = b; b = tmp;`
정답 확인
직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.
정답 (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
#nullable enable
Console.WriteLine("=== int 스택 ===");
MyStack<int> ints = new();
ints.Push(1);
ints.Push(2);
ints.Push(3);
Console.WriteLine("Push: 1, 2, 3");
Console.WriteLine($"Peek: {ints.Peek()}");
Console.WriteLine($"Pop: {ints.Pop()}");
Console.WriteLine($"Pop: {ints.Pop()}");
Console.WriteLine($"남은 개수: {ints.Count}");
Console.WriteLine("=== string 스택 ===");
MyStack<string> strs = new();
strs.Push("A");
strs.Push("B");
Console.WriteLine("Push: A, B");
Console.WriteLine($"Pop: {strs.Pop()}");
Console.WriteLine($"Pop: {strs.Pop()}");
public class MyStack<T>
{
private readonly List<T> _items = new();
public int Count => _items.Count;
public void Push(T item) => _items.Add(item);
public T Pop()
{
if (_items.Count == 0)
{
throw new InvalidOperationException("스택이 비어 있습니다.");
}
T top = _items[^1];
_items.RemoveAt(_items.Count - 1);
return top;
}
public T Peek()
{
if (_items.Count == 0)
{
throw new InvalidOperationException("스택이 비어 있습니다.");
}
return _items[^1];
}
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll13</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
#nullable enable
int a = 10, b = 20;
Console.WriteLine($"교환 전: a={a}, b={b}");
Swap(ref a, ref b);
Console.WriteLine($"교환 후: a={a}, b={b}");
string s1 = "hello", s2 = "world";
Console.WriteLine($"교환 전: s1={s1}, s2={s2}");
Swap(ref s1, ref s2);
Console.WriteLine($"교환 후: s1={s1}, s2={s2}");
static void Swap<T>(ref T a, ref T b)
{
T tmp = a;
a = b;
b = tmp;
}
직접 해 보기
cd src/GenericClass
dotnet run
cd ../GenericMethod
dotnet run
cd ../Constraint
dotnet run
cd ../Variance
dotnet run다음 단원
[14_LINQ](../14_LINQ/) — 제네릭 컬렉션을 우아하게 다루는 쿼리 도구, LINQ 를 만납니다.