
"정수(int) 두 개를 바꾸는 메서드를 만들었는데,
이번에는 문자열(string) 두 개를 바꿔야 하네?
하는 일은 똑같은데, 형식만 다르다고 메서드를 따로 만들어야 하나요?"
똑같은 로직을 형식(Type)만 바꿔서 반복적으로 작성하는 건 비효율적입니다.
이럴 때 C#에서는 '만능 상자'를 만드는 도구, 제네릭(Generic)을 제공합니다.
먼저 제네릭이 왜 필요한지, 그 불편함을 직접 느껴보는 것부터 시작하겠습니다.
두 개의 변수 값을 서로 맞바꾸는 Swap메서드를 만들어 볼게요.
[제네릭을 사용하기 전]
// 1. 정수(int)를 교환하는 메서드
static void SwapInt(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
// 2. 문자열(string)을 교환하는 메서드
static void SwapString(ref string a, ref string b)
{
string temp = a;
a = b;
b = temp;
}
// ... double, float, Person 객체 등등 타입마다 계속 만들어야 함 ...
단지 매개변수의 형식이 다르다는 이유로 중복 코드가 계속 발생합니다.
이건 객체 지향의 DRY(Don't Repeat Yourself) 원칙에도 어긋나죠.
"여기서 object 형식을 사용하면 해결할 수 있지 않나요?"
좋은 생각일까요? 한번 코드를 수정해 봅시다.
static void SwapObject(ref object a, ref object b)
{
object temp = a;
a = b;
b = temp;
}
이제 SwapObject메서드 하나로 정수든, 문자열이든 교환할 수 있게 되었습니다.
정말 문제가 해결된 건지 코드를 통해 자세히 살펴보겠습니다.
[object 형식을 사용해서 해결?]
class Program
{
static void Main()
{
int num1 = 10;
int num2 = 20;
// 1. int에서 object로 '박싱(Boxing)' 발생
object obj1 = num1;
object obj2 = num2;
SwapObject(ref obj1, ref obj2);
// 2. object에서 int로 '언박싱(Unboxing)' 발생
num1 = (int)obj1;
num2 = (int)obj2;
Console.WriteLine($"num1: {num1}, num2: {num2}");
int number = 100;
string text = "Hello";
object obj3 = number;
object obj4 = text;
SwapObject(ref obj3, ref obj4);
// 프로그래머가 실수로 다른 타입을 교환하려고 하면 어떻게 될까요?
number = (int)obj3; // 런타임 오류 발생! o1에는 "Hello"가 들어있음
}
static void SwapObject(ref object a, ref object b)
{
object temp = a;
a = b;
b = temp;
}
}
[실행 결과]
num1: 20, num2: 10
❌예외 발생 System.InvalidCastException: 'Unable to cast...
이 코드에는 두 가지 심각한 문제가 숨어있습니다.
성능 저하 (박싱/언박싱)
값 형식(Value Type)을 object로 변환하면 박싱/언박싱이 발생합니다.
박싱/언박싱 과정에서 눈에 띄는 성능 저하를 일으킬 수 있습니다.
타입 불안정성 (컴파일 시점에 타입 오류를 못 잡음)
컴파일러는 object에 원래 어떤 형식이 들어있었는지 신경 쓰지 않습니다.
컴파일은 통과하지만, 잘못된 형변환 시 런타임 오류가 발생할 수 있습니다.
이런 종류의 버그는 찾아내기도 어렵고 매우 치명적입니다.
이런 문제들을 해결하며 코드의 재사용성을 극대화하는 것이 바로 제네릭입니다.
제네릭 메서드는 다양한 타입을 처리할 수 있는 '만능 메서드'입니다.
메서드 이름 뒤에 꺾쇠괄호 < >를 붙이고, 그 안에 타입 매개변수를 넣어 정의합니다.
타입 매개변수는 보통 T를 사용하는데, 'Type'의 약자입니다.
[코드]
class Program
{
static void Main()
{
int num1 = 10;
int num2 = 20;
Swap(ref num1, ref num2); // 컴파일러가 T를 int로 추론
Console.WriteLine($"num1: {num1}, num2: {num2}");
string str1 = "Hello";
string str2 = "World";
Swap(ref str1, ref str2); // 컴파일러가 T를 string으로 추론
Console.WriteLine($"str1: {str1}, str2: {str2}");
}
// <T>를 사용하여 "어떤 타입 T든 들어올 수 있다"고 선언
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
[실행 결과]
num1: 20, num2: 10
str1: World, str2: Hello
타입 안정성과 코드 재사용성, 두 마리 토끼를 모두 잡았습니다!
메서드뿐만 아니라 클래스도 '만능 클래스'로 만들 수도 있습니다.
제네릭 클래스가 다루는 데이터의 타입을 인스턴스를 생성할 때 결정하도록 합니다.
이때 List<T>, Dictionary<TKey, TValue>가 바로 대표적인 제네릭 클래스입니다.
어떤 타입이든 담을 수 있는 Box<T>클래스를 만들어 봅시다.
[코드]
// 클래스 이름 뒤에 <T>를 붙여 일반화 클래스로 선언
public class Box<T>
{
// 내부 필드도 T 타입으로 선언
private T _item;
public void SetItem(T item)
{
_item = item;
}
public T GetItem()
{
return _item;
}
}
class Program
{
static void Main()
{
// 클래스를 사용할 때는 T에 들어갈 실제 타입을 명시적으로 지정해 주어야 합니다.
// 1. 정수를 담는 상자
Box<int> intBox = new Box<int>();
intBox.SetItem(123);
int number = intBox.GetItem();
Console.WriteLine($"정수 상자 속 아이템: {number}");
// 2. 문자열을 담는 상자
Box<string> stringBox = new Box<string>();
stringBox.SetItem("Generic Class!");
string message = stringBox.GetItem();
Console.WriteLine($"문자열 상자 속 아이템: {message}");
// Box<int>와 Box<string>은 컴파일 시점에 완전히 다른 타입으로 취급됩니다.
// stringBox.SetItem(456); // 컴파일 오류 발생!
}
}
[실행 결과]
정수 상자 속 아이템: 123
문자열 상자 속 아이템: Generic Class!
제네릭은 코드의 중복을 줄이고, 타입 안정성을 높여주는 필수적인 도구입니다.
object를 사용할 때 발생하는 박싱/언박싱이 없어 성능상 이점이 있습니다.