[로봇활용_11주차] C# 제네릭 대리자

최윤호·2025년 10월 19일
post-thumbnail

제네릭 대리자: 만능 심부름꾼

우리가 고용한 심부름꾼에게는 한 가지 아쉬운 점이 있었습니다.

"정수(int)를 계산하는 Calculate대리자를 만들었는데,
이번엔 실수(double)를 계산해야 하네? 하는 일은 똑같은데..."

우리의 심부름꾼이 오직 '정수 심부름'만 할 수 있는 전문가였던 탓입니다.
만약 어떤 종류의 심부름이든 척척 해내는 '만능 심부름꾼'이 있다면 얼마나 좋을까요?

1)제네릭 대리자 직접 만들기

먼저, 제네릭이 없을 때의 불편함을 다시 한번 코드로 확인해 봅시다.

[타입별로 대리자를 선언해야 하는 번거로움]

// 정수 계산용 대리자
public delegate int CalculateInt(int a, int b);

// 실수 계산용 대리자
public delegate double CalculateDouble(double a, double b);

// ... string, float 등 타입마다 계속 만들어야 함 ...

똑같은 '두 개의 입력을 받아 하나를 반환하는' 구조임에도 불구하고,
타입이 다르다는 이유로 코드가 중복되고 있습니다.
이 문제를 해결하기 위해, 제네릭 메서드나 제네릭 클래스처럼
대리자에도 타입 매개변수(<T>)를 사용할 수 있습니다.

[제네릭 대리자 사용]

// "T라는 타입 두 개를 받아, T 타입 하나를 반환한다"는 만능 명세서!
public delegate T Calculate<T>(T a, T b);

Calculate<T>선언으로 모든 숫자 타입의 계산을 처리할 수 있습니다!
코드 중복이 사라지고 훨씬 깔끔해졌습니다!


2)닷넷 내장 제네릭 대리자

사실, 위와 같은 제네릭 대리자를 직접 선언할 일은 거의 없습니다. 왜냐하면
.NET 프레임워크가 가장 흔하게 사용되는 형태의 제네릭 대리자들을 미리 만들어서
기본으로 제공하기 때문입니다. 우리는 이 '기성품'들을 가져다 쓰기만 하면 됩니다!

1. Func<T, TResult>

Func대리자는 반환 값이 있는 모든 종류의 메서드를 참조할 수 있습니다.
심부름을 시키면 반드시 결과물(Return Value)을 들고 돌아옵니다.

[특징]
Func<T, TResult>: T타입의 입력 1개를 받아 TResult타입의 결과를 반환.
Func<T1, T2, TResult>: T1, T2타입의 입력 2개를 받아 TResult타입의 결과를 반환.
Func<T1, T2, ... TResult>: 최대 16개의 입력 매개변수까지 지원합니다.
가장 마지막 타입 매개변수(TResult)는 반환 타입입니다.

[코드]

class Program
{
    static void Main()
    {
        Func<int, int, int> intCalcFunc = AddInt;
        int result = intCalcFunc(10, 20);
        Console.WriteLine($"정수 계산 결과: {result}");

        Func<double, double, double> doubleCalcFunc = AddDouble;
        double result2 = doubleCalcFunc(3.14, 1.59);
        Console.WriteLine($"실수 계산 결과: {result2}");
    }

    static int AddInt(int a, int b)
    {
        return a + b;
    }
    static double AddDouble(double a, double b)
    {
        return a + b;
    }
}

[실행 결과]

정수 계산 결과: 30
실수 계산 결과: 4.73

아까 만든 Calculate<T>Func로 완벽하게 대체할 수 있습니다.

2. Action<T>

Action대리자는 Func와 정반대로, 반환 값이 없는(void) 모든 메서드를 참조합니다.
지시받은 임무를 수행하기만 할 뿐, 아무것도 들고 돌아오지 않습니다.

[특징]
Action: 입력 매개변수가 없음.
Action<T>: T타입의 입력 1개를 받음.
Action<T1, T2>: T1, T2타입의 입력 2개를 받음.
Action<T1, T2, ...>: 최대 16개의 입력 매개변수를 지원합니다.

[코드]

class Program
{
    static void Main()
    {
        Action<string> logger = LogToConsole;
        logger("프로그램이 시작되었습니다.");
    }

    static void LogToConsole(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

[실행 결과]

[LOG] 프로그램이 시작되었습니다.

3. Predicate<T>

Predicate는 하나의 입력을 받아 bool값을 반환합니다.
주로 어떤 조건이 참(true)인지 거짓(false)인지 검사하는 용도로 사용됩니다.

[코드]

class Program
{
    static void Main()
    {
        Predicate<int> numberChecker = IsEven;
        Console.WriteLine($"10은 짝수인가? {numberChecker(10)}");
        Console.WriteLine($"7은 짝수인가? {numberChecker(7)}");
    }

    static bool IsEven(int number)
    {
        return number % 2 == 0;
    }
}

[실행 결과]

10은 짝수인가? True
7은 짝수인가? False

3)실습 예제

'유연함'이 왜 강력한지, 조금 더 발전된 예제로 보여드릴게요.
숫자 목록에서 '짝수'만 골라내고, 다른 목록에서는 '홀수'만 골라내고 싶다고 해봅시다.

[코드]

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 원본 데이터
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // 1. "짝수만 골라줘" 라는 '규칙(IsEven)'을 전달
        List<int> evenNumbers = FilterNumbers(numbers, IsEven);
        Console.WriteLine("짝수 목록:");
        PrintNumbers(evenNumbers);

        // 2. "홀수만 골라줘" 라는 '규칙(IsOdd)'을 전달
        List<int> oddNumbers = FilterNumbers(numbers, IsOdd);
        Console.WriteLine("\n홀수 목록:");
        PrintNumbers(oddNumbers);

        // 3. "5보다 큰 수만 골라줘" 라는 '규칙(IsGreaterThanFive)'을 전달
        List<int> greaterThanFive = FilterNumbers(numbers, IsGreaterThanFive);
        Console.WriteLine("\n5보다 큰 수 목록:");
        PrintNumbers(greaterThanFive);
    }

    // 핵심! 필터링하는 '규칙'을 대리자(filterCondition)로 받습니다.
    static List<int> FilterNumbers(List<int> numbers, Predicate<int> filterCondition)
    {
        List<int> result = new List<int>();
        foreach (int number in numbers)
        {
            // 전달받은 '규칙'으로 숫자를 검사합니다.
            // 이 규칙이 IsEven일수도, IsOdd일수도 있습니다.
            if (filterCondition(number))
            {
                result.Add(number);
            }
        }
        return result;
    }

    // --- 필터링 '규칙'으로 사용될 메서드들 ---
    static bool IsEven(int number)
    {
        return number % 2 == 0;
    }

    static bool IsOdd(int number)
    {
        return number % 2 != 0;
    }

    static bool IsGreaterThanFive(int number)
    {
        return number > 5;
    }

    // --- 결과를 출력하는 도우미 메서드 ---
    static void PrintNumbers(List<int> numbers)
    {
        foreach (int number in numbers)
        {
            Console.Write(number + " ");
        }
        Console.WriteLine();
    }
}

[실행 결과]

짝수 목록:
2 4 6 8 10

홀수 목록:
1 3 5 7 9

5보다 큰 수 목록:
6 7 8 9 10

4)정리

대부분의 상황에서 delegate키워드를 직접 사용할 필요는 없습니다.
특별한 경우가 아니라면, Func, Action, Predicate를 사용하세요!

구분Func<...>Action<...>Predicate<T>
반환 값O (있음)X (없음, void)O (bool 고정)
목적값을 계산하고 결과를 반환특정 동작을 수행조건이 참/거짓인지 판별
비유결과를 가져오는 심부름꾼임무만 수행하는 심부름꾼"Yes/No" 전문가
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글