CodeGym /행동 /C# SELF /델리게이트: 고급 시나리오

델리게이트: 고급 시나리오

C# SELF
레벨 54, 레슨 3
사용 가능

1. 콜백과 비동기 프로그래밍

콜백 (callback) — 어떤 작업이 끝났을 때 호출할 메서드를 넘기는 메커니즘이에요. 비동기 작업, 타이머, 데이터 처리, UI에서 자주 쓰입니다.

예제 1: 콜백이 있는 비동기 작업

사용자가 쿼리를 입력하고(예: 인터넷에서) 결과를 지연해서 받아오는 앱을 가정합시다. 데이터를 받은 뒤 화면을 갱신하고 싶을 때 콜백을 씁니다.


// 콜백을 위한 델리게이트
public delegate void DataReceivedHandler(string result);

// 데이터를 비동기적으로 다운로드하는 메커니즘 (모의)
public void DownloadDataAsync(DataReceivedHandler callback)
{
    // 다운로드가 시간이 걸린다고 가정 (타이머로 흉내냄)
    Task.Delay(1000).ContinueWith(_ =>
    {
        string data = "검색 결과: <데이터>";
        callback(data); // 콜백 델리게이트 호출
    });
}

// 사용 예:
DownloadDataAsync(result =>
{
    Console.WriteLine("수신됨: " + result);
});

이 패턴은 결과를 받은 뒤의 로직을 데이터 수신 메커니즘과 완전히 분리할 수 있어서 매우 유연합니다.

2. 메서드 파라미터로서의 델리게이트: 전략과 비교자

자주 있는 요구사항: 사용자에게 로직(함수)을 메서드에 넘겨서 요소들을 어떻게 비교/필터/변환할지 직접 정하도록 하는 것.

예제 2: 델리게이트로 Strategy 패턴 구현

정렬이 있는데 사용자가 이름, 날짜, 크기 등으로 다르게 정렬하고 싶을 수 있죠.


public delegate bool CompareFunc(int a, int b);

public void BubbleSort(int[] arr, CompareFunc compare)
{
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr.Length - 1; j++)
        {
            if (compare(arr[j], arr[j + 1]))
            {
                // 값 교환
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// 내림차순 정렬
CompareFunc descending = (a, b) => a < b;

// 사용 예
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // 출력: 4, 3, 2, 1

이런 방식은 소스 코드를 바꾸지 않고 외부에서 "전략"을 주입하는 범용적인 방법이에요.

3. 익명 메서드, 람다 식과 델리게이트

C#가 발전하면서 매번 별도 클래스나 메서드를 선언하는 게 번거로워졌죠. 다행히 익명 메서드와 람다 식 덕분에 델리게이트를 즉석에서 만들 수 있어요.

예제 3: 람다를 델리게이트로


Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15

예제 4: 이름으로 연산 선택 (switch + Delegates)


Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"

switch (userInput)
{
    case "sum": op = (a, b) => a + b; break;
    case "sub": op = (a, b) => a - b; break;
    case "mul": op = (a, b) => a * b; break;
    case "div": op = (a, b) => a / b; break;
    default: throw new Exception("알 수 없는 연산!");
}
Console.WriteLine(op(6, 2));

입력 검증을 잊지 마세요 — 델리게이트는 유연성과 가독성을 줍니다.

4. 델리게이트와 처리 체인 (“책임 연쇄”)

델리게이트는 다중대상(multi-cast)을 지원하니, 처리기 체인을 쉽게 구성할 수 있어요.

예제 5: 필터 호출 체인

문자열을 처리하는 여러 "필터"가 있다고 생각해보죠.


public delegate string StringFilter(string input);

string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();

StringFilter filters = RemoveDigits;
filters += ToUpper;

// 델리게이트를 통해 모든 필터를 순차적으로 적용
string text = "안녕123";
foreach (StringFilter filter in filters.GetInvocationList())
{
    text = filter(text);
}
Console.WriteLine(text); // 출력: "안녕"

중요: 그냥 filters(text)를 호출하면 체인의 마지막 핸들러 결과만 반환됩니다. 값의 "흐름"을 원하면 위처럼 GetInvocationList()로 명시적으로 순회하세요.

5. 런타임에 동작을 바꾸는 델리게이트

예전에는 동작을 바꾸려면 별도 클래스/인터페이스를 만들어야 했죠. 델리게이트와 람다로 사소한 폴리모피즘을 함수로 표현할 수 있어요.

예제 6: 동적 커맨드를 가진 로봇 행동


public class Robot
{
    public event Action<string>? OnCommandReceived;

    public void ReceiveCommand(string command)
    {
        OnCommandReceived?.Invoke(command);
    }
}

// 사용 예:
var robot = new Robot();

robot.OnCommandReceived += cmd => Console.WriteLine($"로봇 실행: {cmd}");
robot.OnCommandReceived += cmd =>
{
    if (cmd == "켜기")
        Console.WriteLine("시스템 로드 중...");
};
// 테스트
robot.ReceiveCommand("켜기");
robot.ReceiveCommand("앞으로 이동");

이 패턴은 테스트, 프로토타이핑, DI 컨테이너, 비즈니스 로직 전달 등에 자주 쓰입니다.

6. 상태 구독을 위한 델리게이트

어떤 클래스가 상태를 가지고 있고 상태가 바뀔 때 구독자들에게 알리고 싶다면 델리게이트(또는 이벤트)를 쓰면 간단해요.

예제 7: 변경 구독 가능한 클래스


public class Notifier<T>
{
    private T _value = default!;
    public event Action<T>? ValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            if (!Equals(_value, value))
            {
                _value = value;
                ValueChanged?.Invoke(_value);
            }
        }
    }
}

// 사용 예:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"새 값: {v}");
intValue.Value = 5; // 이벤트 트리거
intValue.Value = 10;

이건 일종의 "미니멀 반응형 프로그래밍"이라서 MVVM, data binding, 모던 UI 프레임워크의 기초가 됩니다.

7. 델리게이트, 클로저와 렉시컬 스코프

람다와 익명 메서드는 주변 컨텍스트의 변수를 캡처(closure)할 수 있어요. 편리하지만 때때로 의외의 버그를 만들기도 합니다.

예제 8: 변수 캡처와 루프의 함정


Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.WriteLine(i);
}

foreach (var a in actions)
    a(); // 세 번 모두 3이 출력됨 (!)

왜 그럴까요? 클로저가 반복 변수 i 하나를 참조하고 있기 때문이에요. 루프가 끝난 뒤 i는 3이 됩니다. 각각 0,1,2를 고정하려면?


for (int i = 0; i < 3; i++)
{
    int loopValue = i; // 현재 값을 "얼려" 둠
    actions[i] = () => Console.WriteLine(loopValue);
}

이제 기대한 대로 동작합니다. 이런 함정은 람다 초보들이 가장 자주 겪는 실수 중 하나예요!

8. 델리게이트 결합(조합)

멀티캐스트 델리게이트는 호출 목록을 가지고 있고, 처리기를 추가(+=)하거나 제거(-=)할 수 있어요.

특징: 참조와 시그니처로 제거됨


void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");

Action a = Handler1;
a += Handler2;
a -= Handler1; // Handler2만 남음
a?.Invoke();   // "2" 출력

예제: 핸들러를 동적으로 관리하기


Action a = Handler1;
a += Handler1;
a -= Handler1; // 이제 리스트에는 Handler1이 하나 남음!

9. 확장성과 제어의 역전(IoC)으로서의 델리게이트

대규모 앱에서 컴포넌트가 외부 코드를 호출하면서도 느슨하게 결합되어 있어야 할 때 델리게이트는 플러그인, 확장, 콜백을 깔끔하게 넣을 수 있게 도와줘요.

예제: 생성자에 동작 주입


public class Greeter
{
    private readonly Func<string> _getName;

    public Greeter(Func<string> getName)
    {
        _getName = getName;
    }

    public void Greet() => Console.WriteLine($"안녕, {_getName()}!");
}

// 다른 동작 주입:
var greeter1 = new Greeter(() => "안야");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());

greeter1.Greet(); // "안녕, 안야!"
greeter2.Greet(); // "안녕, 14:35!"

실무에서는 테스트하기 좋고 유지보수하기 쉬운 코드를 만들 때 이런 패턴을 자주 씁니다.

10. 유용한 팁

표준 인터페이스와 LINQ에서의 델리게이트

LINQ, 컬렉션, 비동기 작업을 다루면 델리게이트와 피할 수 없이 마주치게 돼요.

  • 많은 메서드들(예: List<T>.Find, Array.Sort, Where, Select)은 델리게이트(Func<T, bool>, Comparison<T> 등)를 받습니다.
  • LINQ 메서드로 필터링, 변환, 집계 로직을 별도 클래스 없이 넘길 수 있어요.

예제: 객체 정렬을 위한 비교자


var people = new[] { "이반", "마리아", "표트르" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // 이반, 표트르, 마리아

델리게이트와 커리잉(부분 적용)

익명 메서드/람다로 일부 인자를 고정해서 새로운 함수를 만들 수 있어요.

예제: 부분 적용


Func<int, int, int> sum = (x, y) => x + y;

// 항상 10을 더하는 함수 생성
Func<int, int> add10 = y => sum(10, y);

Console.WriteLine(add10(5)); // 15

델리게이트 비교의 특징

C#에서는 델리게이트를 서로 비교(==)할 수 있는데, 호출 목록(invocation list)이 같으면 같다고 판단합니다.


void Handler1() { }
void Handler2() { }

Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True

Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True

다만 익명 메서드나 람다 기반 델리게이트는 인스턴스 자체로 비교되니 주의하세요.

델리게이트 직렬화

델리게이트를 직렬화할 수는 있지만, 참조하는 메서드가 있는 클래스가 직렬화 가능해야 하고 모든 타입이 접근 가능해야 합니다. .NET 8부터는 BinaryFormatter가 기본적으로 비활성화되어 있고 구식으로 간주됩니다. 실무에서 델리게이트 직렬화는 거의 쓰이지 않습니다.

델리게이트와 이벤트의 차이: 어디에 델리게이트를 쓰고 어디에 이벤트를 써야 할까?

  • 델리게이트는 호출 가능한 타입/변수로, 외부에서 직접 호출할 수 있어요.
  • 이벤트(event)는 델리게이트에 대한 접근을 제한하는 수단입니다: 외부에서는 구독/구독해제(+=/-=)만 할 수 있고 호출은 클래스 내부에서만 가능해요.
  • 이벤트는 항상 델리게이트 타입이지만, 모든 델리게이트가 이벤트인 건 아니에요.

적용 기준: 로직을 클래스 밖에서 정의하게 두고 싶다면 델리게이트를 쓰고, 구독 관리를 보호하고 싶다면 이벤트를 사용하세요.

11. 델리게이트 사용 시 흔한 실수들

실수 #1: 델리게이트와 이벤트 혼동하기.
public 필드 델리게이트(public Action MyAction;)를 이벤트(public event Action MyAction;) 대신 쓰면 캡슐화가 깨져요. 외부 코드가 구독자들을 덮어쓰거나(instance.MyAction = null;) 직접 호출할 수 있어서 클래스 로직을 망가뜨릴 수 있습니다.

실수 #2: 다중대상 델리게이트의 반환값 처리 오류.
델리게이트가 값을 반환하는 경우(예: Func<string, int>), 보통 호출(myDelegate("test"))하면 체인의 마지막 메서드 결과만 돌아옵니다. 모든 구독자 결과를 얻으려면 GetInvocationList()로 순회해야 해요.


// 모든 구독자의 결과를 순회하는 예
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
    var r = ((Func<string, int>)d)("test");
    Console.WriteLine(r);
}

실수 #3: 루프 변수 캡처로 인한 클로저 문제.
람다를 루프 안에서 만들면 반복자 변수를 캡처해서 현재 값을 고정하지 않고 변수 자체를 참조합니다. 초보자들이 자주 겪는 고전적인 함정이에요.


// 잘못된 예: 모든 액션이 마지막 i 값을 출력함
for (int i = 0; i < 3; i++) 
{
    actions[i] = () => Console.WriteLine(i);
}

// 올바른 예: 각 반복마다 로컬 복사본 생성
for (int i = 0; i < 3; i++) 
{
    int copy = i;
    actions[i] = () => Console.WriteLine(copy);
}

실수 #4: 메모리 누수 유발.
인스턴스 메서드가 수명 긴 객체의 델리게이트에 구독하고 나서 구독 해제를 하지 않으면 메모리 누수가 생깁니다. 수명 긴 객체가 구독자를 참조하고 있으면 GC가 구독자를 해제하지 못하거든요. 특히 UI 컴포넌트 같은 수명이 짧은 객체에서는 반드시 구독 해제를 관리하세요.

2
과제
C# SELF, 레벨 54, 레슨 3
잠금
델리게이트를 사용한 텍스트 처리 체인
델리게이트를 사용한 텍스트 처리 체인
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION