Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

뭐라도 쓰겠지

25.03.28 / 템플릿(Template), 제네릭(Generic) 본문

프로그래밍

25.03.28 / 템플릿(Template), 제네릭(Generic)

김데피 2025. 3. 28. 14:54

템플릿은 C++에서 일반화 프로그래밍의 기초이다. 강력한 형식의 언어인 C++에서는 모든 변수에 프로그래머가 명시적으로 선언하거나 컴파일러에서 추론한 특정 형식이 있어야 한다. 그러나 많은 데이터 구조와 알고리즘이 어떤 형식에서 작동하든 동일하게 보인다. 템플릿을 사용하면 클래스 또는 함수의 작업을 정의하고, 그러한 작업이 어떤 구체적인 형식에서 작동해야 하는지를 사용자가 지정하도록 할 수 있다.

 

내가 다루는 C#에서는 C++의 템플릿과 정확히 같은 개념은 없지만, 크게 두 가지 문맥에서 템플릿이라는 말을 접할 수 있다.

 

첫번째로 제네릭(Generic)이 있다. C#에서 흔히 템플릿과 비슷한 역할을 하는 것이 바로 제네릭이다. 제네릭은 C++의 템플릿과 유사하게, 클래스나 메소드를 정의할 때 형식 매개변수(Type Parameter)를 사용하여 형식에 대한 의존성을 줄이고 재사용성을 높이는 기능을 제공한다.

 

private static void TemplateMethod<T>(T _t)
{
    Console.WriteLine(_t?.GetType());
}

 

이 코드는 제너릭 메소드의 정의부이다. T라는 타입 매개변수를 사용하고, 메소드 이름 뒤에 <T>가 붙어서 이를 통해 호출 시점에서 구체적인 타입을 지정할 수 있다.

TemplateMethod<int>(5);
TemplateMethod<string>("STRING");

 

호출부에서 TemplateMethod<int>(5); 라고 적으면 T가 int로 결정되어, 메소드 내부의 모든 T가 int로 동작한다.

TemplateMethod<string>("STRING"); 라고 적으면 T가 string으로 결정되어, 메소드 내부의 모든 T가 string으로 동작한다.

 

C++의 함수 템플릿과 비슷하게 하나의 메소드 정의로 서로 다른 타입에 대해 재사용이 가능하다. 형식 안정성(Type Safety)을 보장받기 때문에, 런타임에 형변환 예외가 발생할 가능성을 줄일 수 있다.

 

class TemplateClass<A, B>
{
    A a;
    B b;

    public TemplateClass()
    {
        Console.WriteLine(a.GetType());
        Console.WriteLine(b.GetType());
    }
}

 

이 코드는 제네릭 클래스의 정의부이다. TemplateClass<A, B>처럼 여러 개의 타입 매개변수를 받을 수 있다. 클래스 내부 필드 a와 b는 각각 A, B 형식으로 선언되어있고, 생성자에서 a.GetType(), b.GetType()을 출력해보면 실제 인스턴스화할 때 지정한 타입 매개변수를 확인할 수 있다.

TemplateClass<int, int> tc1 = new TemplateClass<int, int>();
TemplateClass<double, float> tc2 = new TemplateClass<double, float>();

 

호출부에서 tc1 생성 시 A = int, B = int가 들어가게 되고, tc2 생성 시 A = double, B = float가 들어가게 된다.

 

이렇게 하면 타입별로 클래스를 새로 정의할 필요 없이, 하나의 범용 클래스로 여러 타입 조합을 유연하게 다룰 수 있다.

 

C++에서는 템플릿 특수화(Template Specialization)이라는 걸 지원하는데, 특정 타입 조합에 따라 다른 구현을 제공하는 기능이다. 하지만 C#은 제네릭에 대해 별도의 템플릿 특수화 문법을 제공하지 않는다. 특정 타입 조합을 달리 처리하려면 오버로드(Overload) 혹은 제네릭 제약(Constraints) 등을 사용해야 한다.

// (C# 에서는 특수화 대신) 오버로드로 해결하는 예시
public void DoSomething(int value)
{
    // int 전용 로직
}

public void DoSomething<T>(T value)
{
    // 그 외 타입 로직
}

 

예를 들자면 이런식이다.

 

C++ 템플릿의 동작 방식과 C# 제네릭의 동작 방식에는 차이가 있다.

C++ 템플릿은 컴파일 타임에 다양한 타입에 대해 실제 코드가 생성되고, 그에 따라 타입별로 다른 바이너리를 만들 수 있다.

C# 제네릭은 런타임에 공통 IL(중간 언어) 코드로 동작하며, JIT(Just-In-Time) 컴파일 시점에 필요한 타입에 대해 최적화된 머신 코드를 생성한다.

 

.NET이 미리 설계해놓은 자료 구조도 있는데, 이걸 컬렉션(Collections)라고 부른다. 아래는 대표적인 제네릭 컬렉션들을 사용하는 예시이다.

private static void ListExample()
{
    List<int> list = new List<int>();
    list.Add(5);
    list.Add(100);
    list.Insert(1, 5);
    list.RemoveAt(0);
    // ...
    
    // 정렬 예시: 람다식을 이용해 사용자 정의 정렬
    list.Sort((int _l, int _r) =>
    {
        return _l < _r ? 1 : -1;
    });

    foreach (int data in list)
    {
        Console.WriteLine("List: {0}", data);
    }
}

 

List<int>는 int 형식 전용 리스트다. 배열 대신 많이 사용되며, 용량이 자동으로 확장되는 동적 배열의 기능을 제공한다.

그리고 Add, Insert, RemoveAt 등 편리한 메소드가 있고, Sort 메소드를 통해 정렬도 쉽게 할 수 있다. <int>부분을 <string>, <float> 등으로 바꿔, 그에 맞춘 타입 안전한 리스트를 생성할 수도 있다.

 

private static void StackExample()
{
    Stack<float> stack = new Stack<float>();

    stack.Push(1.1f);
    stack.Push(2.2f);

    int cnt = stack.Count;
    for (int i = 0; i < cnt; ++i)
    {
        Console.WriteLine("Stack: {0}", stack.Pop());
    }
}

 

Stack의 경우 전형적인 자료구조인 스택을 제공한다. Push와 Pop 연산이 가능하고, 스택이 그렇듯 제네릭 컬렉션 Stack도 LIFO(Last In First Out) 구조를 지원한다.

 

private static void QueueExample()
{
    Queue<decimal> queue = new Queue<decimal>();

    queue.Enqueue(1.1m);
    queue.Enqueue(2.2m);
    Console.WriteLine("Peek: " + queue.Peek());

    int cnt = queue.Count;
    for (int i = 0; i < cnt; ++i)
    {
        Console.WriteLine("Queue: {0}", queue.Dequeue());
    }
}

 

Queue도 마찬가지다. 전형적인 자료구조인 큐를 지원하고, Enqueue와 Dequeue 연산을 제공한다. 역시 큐처럼 FIFO(First In First Out) 구조를 지원한다.

 

private static void DictionaryExample()
{
    Dictionary<int, string> dic = new Dictionary<int, string>();

    dic.Add(1, "Hello");
    dic.Add(7, "World");

    Console.WriteLine("dic[7]: " + dic[7]);

    foreach (KeyValuePair<int, string> item in dic)
    {
        Console.WriteLine($"Key: {item.Key} / Value: {item.Value}");
    }
}

이건 키와 값을 쌍으로 저장하는 해시 테이블 기반의 컬렉션인 Dictionary이다. Dictionary<int, string>은 int를 키, string을 값으로 사용하는 딕셔너리이다.

 

정리하자면 C# 제네릭의 장점은 아래 4가지가 있다.

  • 1. 타입 안정성(Type-Safe)
    • 컴파일 타임에 타입을 확정하므로, 잘못된 형변환으로 인한 런타임 오류를 줄일 수 있다.
  • 2. 코드 재사용성
    • 하나의 제네릭 클래스/메소드 정의로 다양한 타입에 중복 코드 없이 처리 가능하다.
  • 3. 성능
    • 박싱/언박싱(주로 object를 사용할 때 발생하는 비용)을 줄일 수 있고, JIT 최적화를 통해 실행 시 성능이 우수하다.
  • 4. 표준 라이브러리 연계
    • .NET 표준 라이브러리(컬렉션, 인터페이스, LINQ 등)에서 제네릭이 광범위하게 적용되어 일관된 개발 경험을 제공한다.

또 하나 C#에서 템플릿이라는 말을 접할 수 있는 경우는 T4(Text Template Transformation Toolkit) 텍스트 템플릿이라는 게 있는데, 이건 다음에 기회가 되면 다뤄보겠다.