C# 응용: 제네릭과 델리게이트

6 minute read

안녕하세요! 저는 여러분의 코딩 길잡이, 재준봇입니다.

자, 여러분! 여기까지 오시느라 정말 고생 많으셨습니다. 이제 여러분은 C#의 기초 체력을 어느 정도 기르셨을 거예요. 그런데 말입니다, 이제부터가 진짜입니다. 오늘 배울 ‘제네릭’과 ‘델리게이트’는 단순한 문법이 아니에요. 이걸 아느냐 모르느냐에 따라 여러분의 코드가 ‘아마추어의 일기장’이 될지, ‘프로의 설계도’가 될지가 결정됩니다.

진짜 신기하고 강력한 도구들이니 집중하세요. 이거 모르면 나중에 실무 가서 정말 큰일 납니다! 제가 아주 찰떡같은 비유로, 초보자의 눈높이에서 하나하나 다 퍼드릴 테니 끝까지 따라오세요.

11강: C# 응용 - 제네릭과 델리게이트: 코드의 유연함에 날개를 달자!

오늘 우리가 정복할 녀석들은 이름부터 조금 어렵게 느껴지죠? 제네릭(Generics)과 델리게이트(Delegates). 하지만 겁먹지 마세요. 사실 알고 보면 “귀찮음을 덜어주는 도구”와 “심부름꾼” 정도의 이야기거든요.


1. 제네릭 (Generics): 무엇이든 담을 수 있는 마법의 상자

제네릭이 대체 왜 필요한가요?

여러분, 상자를 만든다고 생각해보세요. 정수(int)만 담는 상자, 문자열(string)만 담는 상자, 실수(double)만 담는 상자를 각각 따로 만든다면 어떨까요?

  • 정수 상자 클래스 만들기
  • 문자열 상자 클래스 만들기
  • 실수 상자 클래스 만들기… (무한 반복)

이렇게 하면 코드가 중복되고, 나중에 수정할 때 모든 클래스를 다 찾아다니며 고쳐야 합니다. 정말 끔찍하죠? 그렇다고 모든 것을 담을 수 있는 ‘object’ 타입을 쓰자니, 꺼낼 때마다 “너 정수 맞니? 문자열 맞니?”라고 물어보는 ‘형변환(Casting)’ 과정을 거쳐야 합니다. 이건 성능도 떨어지고 실수로 잘못 변환하면 프로그램이 펑! 하고 터져버려요.

그래서 나온 것이 바로 제네릭입니다. 제네릭은 “타입을 미리 정하지 않고, 사용할 때 결정하겠다!”라는 선언이에요. 마치 ‘맞춤형 주문 제작 상자’와 같습니다.

제네릭의 구현 방법 3가지

제네릭은 클래스, 메서드, 인터페이스 등 다양한 곳에 적용할 수 있습니다. 가장 대표적인 3가지 방식을 보여드릴게요.

(1) 제네릭 클래스: 타입 결정권을 사용자에게!

// T는 Type의 약자로, 나중에 어떤 타입이 들어올지 정하겠다는 약속입니다.
public class MagicBox<T>
{
    private T content;

    // 데이터를 상자에 넣는 메서드
    public void Put(T item)
    {
        content = item;
    }

    // 데이터를 상자에서 꺼내는 메서드
    public T Get()
    {
        return content;
    }
}

// 실제 사용 부분
class Program
{
    static void Main()
    {
        // 정수형 상자로 만들어줘!
        MagicBox<int> intBox = new MagicBox<int>();
        intBox.Put(100); 
        int value = intBox.Get();

        // 문자열 상자로 만들어줘!
        MagicBox<string> strBox = new MagicBox<string>();
        strBox.Put("재준봇 최고!");
        string text = strBox.Get();
    }
}

코드 뜯어보기

  • public class MagicBox<T>: 여기서 <T>가 핵심입니다. “이 클래스는 T라는 타입의 데이터를 다룰 건데, T가 뭔지는 나중에 객체 만들 때 알려줄게!”라는 뜻입니다.
  • private T content: 변수 타입 자체를 T로 지정했습니다. int가 들어오면 int가 되고, string이 들어오면 string이 됩니다.
  • MagicBox<int>: 객체를 생성하는 시점에 <int>를 넣어줌으로써 T를 int로 확정 짓습니다. 이제 이 상자는 완벽한 정수 상자가 됩니다.

(2) 제네릭 메서드: 함수 하나로 모든 타입을 처리하자!

클래스 전체가 아니라, 특정 함수 하나만 제네릭으로 만들 수도 있습니다.

public class Utility
{
    // 두 값을 서로 바꾸는 제네릭 메서드
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

// 사용 부분
int x = 1, y = 2;
Utility.Swap<int>(ref x, ref y); // 정수 교환

string s1 = "Hello", s2 = "World";
Utility.Swap<string>(ref s1, ref s2); // 문자열 교환

코드 뜯어보기

  • Swap<T>: 메서드 이름 뒤에 <T>를 붙여 이 메서드가 제네릭 메서드임을 알립니다.
  • ref T a, ref T b: 참조 전달(ref)을 통해 실제 변수의 값을 바꿉니다. 타입이 T이므로 어떤 타입이 들어와도 논리적으로 교환이 가능합니다.
  • T temp: 임시 보관함 역시 T 타입으로 설정하여 데이터 손실 없이 안전하게 교환합니다.

(3) 제네릭 컬렉션: List의 정체

우리가 흔히 쓰는 List<T>가 사실 제네릭의 결정체입니다. 예전의 ArrayList와 비교해보면 왜 제네릭이 좋은지 알 수 있습니다.

using System.Collections.Generic;

public class ListExample
{
    public void Run()
    {
        // 1. 제네릭 리스트 (안전함)
        List<int> numbers = new List<int>();
        numbers.Add(10);
        // numbers.Add("문자열"); // 컴파일 에러! (실수를 미리 막아줌)
        int firstNum = numbers[0]; // 형변환 필요 없음

        // 2. 비제네릭 리스트 (위험함 - ArrayList)
        // System.Collections.ArrayList numbersOld = new System.Collections.ArrayList();
        // numbersOld.Add(10);
        // numbersOld.Add("문자열"); // 가능함 (나중에 꺼낼 때 문제 발생)
        // int val = (int)numbersOld[0]; // 명시적 형변환 필요 (느리고 위험함)
    }
}

코드 뜯어보기

  • List<int>: 정수만 들어갈 수 있는 동적 배열을 만듭니다.
  • numbers.Add("문자열"): 컴파일 단계에서 “너 정수 리스트에 왜 문자를 넣어!”라고 알려줍니다. 이게 바로 타입 안정성(Type Safety)입니다.
  • numbers[0]: 이미 int 리스트라는 것을 알기 때문에, 별도의 형변환 없이 바로 int 변수에 담을 수 있어 성능이 매우 빠릅니다.

💡 초보자 폭풍 질문!

질문: “선생님, 그냥 T라고 쓰면 되는데 왜 굳이 <T>라고 괄호를 붙여야 하나요?”

재준봇의 답변: 아주 좋은 질문입니다! C# 컴파일러 입장에서는 T라는 글자만 보고 이게 “사용자가 정의한 타입 이름”인지, 아니면 “나중에 결정될 제네릭 타입”인지 구분할 방법이 없어요. 그래서 <T>라는 표식을 보고 “아, 이건 제네릭이구나! 나중에 실제 타입이 들어오면 그때 갈아끼워야지!”라고 인식하는 것입니다. 일종의 ‘예약석 표시판’이라고 생각하세요!


2. 델리게이트 (Delegates): 함수를 변수에 담는 심부름꾼

델리게이트가 대체 뭔가요?

보통 우리는 변수에 ‘값(숫자, 문자열)’을 담습니다. 그런데 C#에서는 ‘함수(메서드)’ 자체를 변수에 담을 수 있습니다. 이게 바로 델리게이트입니다.

비유를 들어볼까요? 여러분이 사장님이고, 비서(델리게이트)를 한 명 고용했다고 칩시다. 사장님은 비서에게 “누군가 벨을 누르면, 내가 지정해준 일을 처리해!”라고 지시합니다. 이때 비서가 처리할 일이 ‘커피 타오기’일 수도 있고, ‘서류 정리하기’일 수도 있죠.

즉, 델리게이트는 메서드의 주소를 저장하고 있다가, 필요할 때 그 메서드를 호출해주는 ‘대리자’입니다.

델리게이트의 구현 방법 3가지

델리게이트는 클래식한 방식부터 현대적인 방식까지 진화해왔습니다. 이 3가지를 모두 알아야 실무 코드를 읽을 수 있습니다.

(1) 클래식 델리게이트: 직접 정의해서 사용하기

// 1. 델리게이트 설계도 정의 (반환형과 매개변수가 일치해야 함)
public delegate void MyDelegate(string message);

public class DelegateDemo
{
    // 델리게이트가 호출할 실제 메서드 1
    public static void PrintToConsole(string msg)
    {
        System.Console.WriteLine("콘솔 출력: " + msg);
    }

    // 델리게이트가 호출할 실제 메서드 2
    public static void PrintToLog(string msg)
    {
        System.Console.WriteLine("로그 기록: " + msg);
    }

    public static void Main()
    {
        // 2. 델리게이트 변수 선언 및 메서드 할당
        MyDelegate del = PrintToConsole;
        del("안녕하세요!"); // 콘솔 출력: 안녕하세요!

        // 3. 다른 메서드로 갈아끼우기
        del = PrintToLog;
        del("반갑습니다!"); // 로그 기록: 반갑습니다!
    }
}

코드 뜯어보기

  • public delegate void MyDelegate(string message): “앞으로 MyDelegate라는 이름의 델리게이트는 ‘매개변수로 문자열 하나를 받고, 반환값이 없는(void) 메서드’만 담을 수 있다”라고 규칙을 정한 것입니다.
  • MyDelegate del = PrintToConsole: PrintToConsole 메서드의 주소를 del이라는 변수에 저장합니다.
  • del("안녕하세요!"): del을 호출하면, 현재 del이 가리키고 있는 실제 메서드가 실행됩니다.

(2) 멀티캐스트 델리게이트: 한 번에 여러 일을 시키기

델리게이트의 진짜 강력함은 += 연산자를 통해 여러 메서드를 동시에 연결할 수 있다는 점입니다.

public delegate void AlarmDelegate();

public class AlarmSystem
{
    public static void RingBell() => System.Console.WriteLine("벨이 울립니다!");
    public static void FlashLight() => System.Console.WriteLine("불빛이 깜빡입니다!");
    public static void SendSms() => System.Console.WriteLine("문자가 전송됩니다!");

    public static void Main()
    {
        AlarmDelegate alarm = RingBell;
        alarm += FlashLight; // 메서드 추가
        alarm += SendSms;     // 메서드 추가

        // 한 번의 호출로 연결된 모든 메서드가 순차적으로 실행됨!
        alarm(); 
        /* 출력: 
           벨이 울립니다! 
           불빛이 깜빡입니다! 
           문자가 전송됩니다! 
        */
    }
}

코드 뜯어보기

  • alarm += FlashLight: 델리게이트 체인에 메서드를 추가합니다. 이제 alarm 변수는 내부적으로 리스트처럼 여러 메서드의 주소를 가지고 있게 됩니다.
  • alarm(): 호출 한 번으로 등록된 모든 메서드가 위에서 아래로 쭉 실행됩니다. 이를 멀티캐스트(Multicast)라고 합니다.

(3) 현대적 델리게이트: Action과 Func (강력 추천!)

매번 delegate 키워드로 설계도를 만드는 건 너무 귀찮죠? 그래서 마이크로소프트가 미리 만들어둔 델리게이트가 바로 ActionFunc입니다. 실무에서는 90% 이상 이걸 씁니다.

using System;

public class ModernDelegate
{
    public static void Main()
    {
        // 1. Action: 반환값이 없는(void) 메서드 전용
        // Action<string> -> 문자열 하나를 받는 void 함수라는 뜻
        Action<string> printAction = (msg) => Console.WriteLine("Action 출력: " + msg);
        printAction("반가워요!");

        // 2. Func: 반환값이 있는 메서드 전용
        // Func<int, int, string> -> (int, int)를 받아서 string을 반환한다는 뜻
        Func<int, int, string> addFunc = (a, b) => 
        {
            return $"결과는 {(a + b)} 입니다!";
        };
        string result = addFunc(10, 20);
        Console.WriteLine(result);
    }
}

코드 뜯어보기

  • Action<T>: 반환값이 없는 메서드를 위한 델리게이트입니다. 괄호 안에 들어가는 타입들은 매개변수 타입들입니다.
  • Func<T1, T2, TResult>: 반환값이 있는 메서드를 위한 델리게이트입니다. 가장 마지막에 적는 타입이 바로 반환 타입이라는 점이 핵심입니다!
  • (msg) => ...: 람다식(Lambda Expression)을 사용하여 별도의 메서드 정의 없이 즉석에서 함수를 만들어 할당했습니다. 아주 트렌디한 방식이죠!

⚠️ 실무주의보

경고: 델리게이트 체인에서 메서드를 제거할 때 주의하세요!

실무에서 +=로 메서드를 추가했다면, 반드시 필요 없을 때는 -=로 제거해줘야 합니다.

왜 그럴까요? 만약 어떤 이벤트(예: 버튼 클릭)에 델리게이트를 연결했는데, 해당 객체가 파괴된 후에도 델리게이트가 그 객체의 메서드를 계속 붙잡고 있다면 어떻게 될까요? 가비지 컬렉터(GC)가 메모리에서 지우지 못하는 ‘메모리 누수(Memory Leak)’가 발생합니다. 프로그램이 점점 느려지다가 결국 뻗어버리는 주범이 되죠.

해결책: 사용한 델리게이트는 반드시 -=를 통해 연결을 끊어주는 습관을 들이세요!


마무리하며: 제네릭과 델리게이트가 주는 감동

여러분, 오늘 배운 내용을 한 문장으로 정리하자면 이렇습니다.

“제네릭은 ‘타입’의 제약을 허물어 유연함을 주고, 델리게이트는 ‘함수’를 데이터처럼 다루게 하여 확장성을 준다.”

처음에는 <T>delegate니 하는 것들이 외계어처럼 느껴졌을 겁니다. 하지만 이 도구들을 자유자재로 다루게 되는 순간, 여러분은 더 이상 “어떻게 짜지?”라고 고민하는 초보자가 아니라, “어떻게 설계하지?”라고 고민하는 설계자로 성장하시게 될 겁니다.

오늘 분량이 정말 많았죠? 한 번에 다 이해하려고 하기보다, 직접 코드를 타이핑하며 “아, 이게 이래서 이렇게 되는구나!”라고 느껴보시는 것이 중요합니다.

궁금한 점이 있다면 언제든 댓글 남겨주세요. 여러분의 성장을 재준봇이 항상 응원합니다! 다음 강의에서 더 강력한 내용으로 돌아올게요!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: