본문 바로가기

C#

C# 대리자(delegate)의 개념과 대리자를 사용하는 이유

대리자란 무엇인가?

C#에서 대리자(delegate)는 객체 지향 프로그래밍의 핵심 개념 중 하나로, 메서드를 참조하기 위한 타입입니다. 대리자를 사용하면 메서드를 변수처럼 저장하고, 매개변수로 전달하거나, 다른 메서드로부터 반환받을 수 있습니다. 이는 프로그램의 유연성을 높여주며, 이벤트 처리, 콜백 함수 구현, 비동기 프로그래밍과 같은 고급 기능을 가능하게 합니다.

 

 

대리자를 사용하는 방법

  1. 대리자 선언 : 대리자 타입을 선언합니다. 이는 대리자가 참조할 메서드의 시그니처(반환 타입 및 매개변수)를 정의합니다.
  2. 대리자 인스턴스화 : 선언된 대리자 타입을 사용하여 대리자 인스턴스를 생성합니다. 이때, 대리자가 참조할 메서드를 지정합니다.
  3. 대리자 호출 : 대리자 인스턴스를 통해 메서드를 호출합니다. 대리자를 호출하면 대리자가 참조하는 메서드가 실행됩니다.
using System;

namespace DelegateExample
{
    // 1. 대리자 선언
    public delegate void MyDelegate(string message);

    class Program
    {
        static void Main(string[] args)
        {
            // 2. 대리자 인스턴스화
            MyDelegate del = new MyDelegate(DisplayMessage);

            // 3. 대리자 호출
            del("Hello, World!");
        }

        // 대리자가 참조할 메서드
        public static void DisplayMessage(string message)
        {
            Console.WriteLine(message);
        }
    }
}

 

 

대리자를 사용하는 이유

이 글을 읽으면서 대리자의 사용 필요성이 바로 명확해지지 않을 수 있습니다. 메서드를 직접 호출하는 것이 표면적으로 더 단순하고 직관적으로 보일 수 있기 때문입니다. 그러나 대리자는 .NET 고급 프로그래밍의 핵심을 이루며, 콜백 메커니즘의 구현, 이벤트 처리, 비동기 프로그래밍 등 다양한 영역에서 광범위하게 활용됩니다.

이 글에서는 대리자를 사용하는 것과 사용하지 않는 것의 차이를 콜백 메커니즘 구현과 이벤트 처리의 두 가지 주요 사례를 통해 비교해 볼 것입니다. 이러한 비교를 통해, 대리자를 사용하는 이유와 그것이 프로그래밍에 어떤 가치를 더하는지에 대한 이해를 깊게 할 수 있을 것입니다.

 

 

콜백 메커니즘 구현 : 인터페이스 사용

콜백 메커니즘은 프로그램에서 어떤 작업이 완료된 후 특정 동작을 실행할 수 있게 하는 편리한 방법입니다. 대리자를 사용하지 않고 이를 구현하는 방법 중 하나는 인터페이스를 정의하고 사용하는 것입니다.

 

// 콜백 인터페이스 정의
public interface ICallback
{
    void OnComplete();
}

// 콜백 인터페이스를 구현하는 클래스
public class CallbackHandler : ICallback
{
    public void OnComplete()
    {
        Console.WriteLine("Task completed. Now running the callback.");
    }
}

public class WithoutDelegate
{
    public void ProcessTask(ICallback callback)
    {
        // 작업 처리
        Console.WriteLine("Task is being processed.");

        // 콜백 호출
        callback.OnComplete();
    }
}

class Program
{
    static void Main(string[] args)
    {
        WithoutDelegate wd = new WithoutDelegate();
        ICallback callback = new CallbackHandler();

        // 작업 실행과 콜백 전달
        wd.ProcessTask(callback);
    }
}

 

 

이 예시에서는 ICallback 인터페이스를 통해 콜백 메커니즘을 구현하고 있습니다. ICallback 인터페이스는 작업 완료 시 호출될 OnComplete 메서드를 정의합니다. CallbackHandler 클래스는 이 인터페이스를 구현하여 실제로 작업 완료 시 수행될 로직을 포함합니다. WithoutDelegate 클래스의 ProcessTask 메서드는 이 인터페이스를 매개변수로 받아, 작업 처리 후 OnComplete 메서드를 호출하여 콜백을 실행합니다.

 

 

콜백 메커니즘 구현 : 대리자 사용

대리자를 사용하는 콜백 메커니즘 구현은 프로그래밍에서 매우 강력한 패턴 중 하나입니다. 대리자를 활용함으로써, 메서드를 다른 메서드의 매개변수로 전달할 수 있게 되며, 이는 작업 완료 후 실행될 콜백을 유연하게 지정할 수 있게 해줍니다.

 

public delegate void Callback();

public class WithDelegate
{
    public void ProcessTask(Callback callback)
    {
        // 작업 처리
        Console.WriteLine("Task is being processed.");

        // 콜백 호출
        callback?.Invoke();
    }
}

class Program
{
    static void Main(string[] args)
    {
        WithDelegate wd = new WithDelegate();

        // 콜백 메서드 정의
        Callback callback = () => Console.WriteLine("Task completed. Now running the callback.");

        // 작업 실행과 콜백 전달
        wd.ProcessTask(callback);
    }
}

 

 

위의 예시에서 WithDelegate 클래스는 대리자 Callback을 매개변수로 받는 ProcessTask 메서드를 통해 콜백 메커니즘을 구현합니다. 이 대리자는 작업 완료 후 실행할 메서드를 참조합니다. 프로그램 실행 시, 사용자는 람다 표현식을 사용하여 콜백 메서드를 정의하고, 이를 ProcessTask 메서드에 전달합니다. 작업 처리가 완료되면, 전달된 콜백이 호출되어 추가적인 작업을 수행할 수 있습니다. 

 

 

 

이벤트 처리 : 인터페이스 사용

이벤트 처리는 프로그래밍에서 다양한 상황에 대응하기 위해 필수적인 기능입니다. 대리자를 활용하지 않고 이벤트를 처리하는 방법 중 하나는 인터페이스 기반의 콜백 메커니즘을 사용하는 것입니다. 이 접근 방식에서, 이벤트를 구독할 클래스는 특정 인터페이스를 구현해야 하며, 이 인터페이스는 이벤트 발생 시 호출될 메서드를 정의합니다.

 

// 이벤트 핸들러의 역할을 하는 인터페이스 정의
public interface IEventHandler
{
    void HandleEvent();
}

// 인터페이스를 구현하는 클래스 정의 1
public class EventHandler1 : IEventHandler
{
    public void HandleEvent()
    {
        Console.WriteLine("EventHandler1 called.");
    }
}

// 인터페이스를 구현하는 클래스 정의 2
public class EventHandler2 : IEventHandler
{
    public void HandleEvent()
    {
        Console.WriteLine("EventHandler2 called.");
    }
}

// 이벤트를 발생시키는 클래스 구현 (대리자를 사용하지 않음)
public class WithoutDelegate
{
    private List<IEventHandler> subscribers = new List<IEventHandler>();

    public void Subscribe(IEventHandler handler)
    {
        subscribers.Add(handler);
    }

    public void Unsubscribe(IEventHandler handler)
    {
        subscribers.Remove(handler);
    }

    public void TriggerEvent()
    {
        foreach (var handler in subscribers)
        {
            handler.HandleEvent();
        }
    }
}

// 구독자를 등록하고 이벤트를 발생시키는 메인 프로그램
class Program
{
    static void Main(string[] args)
    {
        var publisher = new WithoutDelegate();

        var handler1 = new EventHandler1();
        var handler2 = new EventHandler2();

        publisher.Subscribe(handler1);
        publisher.Subscribe(handler2);

        // 이벤트 발생
        publisher.TriggerEvent();

        // 구독 해제
        publisher.Unsubscribe(handler1);

        // 다시 이벤트 발생
        publisher.TriggerEvent();
    }
}

 

 

이 인터페이스 기반 접근법은 이벤트 처리를 위해 대리자 대신 사용될 수 있으며, 이벤트 발생 시 등록된 모든 구독자 객체의 메서드를 호출하여 이벤트에 반응합니다.

 

 

 

이벤트 처리 : 대리자 사용

반면, 대리자를 사용하는 이벤트 처리 방식은 이벤트를 훨씬 간단하게 정의하고 관리할 수 있게 해줍니다. C#의 event 키워드와 대리자를 사용하면, 이벤트 핸들러의 추가와 제거가 매우 간편해지고, 코드의 가독성이 높아집니다.

 

public class WithDelegate
{
    // 이벤트 정의
    public event Action MyEvent;

    public void TriggerEvent()
    {
        // 이벤트 핸들러 호출
        MyEvent?.Invoke();
    }
}

class Program
{
    static void Main(string[] args)
    {
        WithDelegate wd = new WithDelegate();

        // 이벤트 핸들러
        Action handler1 = () => Console.WriteLine("Event1 triggered!");
        Action handler2 = () => Console.WriteLine("Event2 triggered!");

        // 이벤트 구독
        wd.MyEvent += handler1;
        wd.MyEvent += handler2;

        // 이벤트 발생
        wd.TriggerEvent();

        // 이벤트 구독 해제
        wd.MyEvent -= handler2;
        
        // 이벤트 발생
        wd.TriggerEvent();
    }
}

 

 

대리자를 사용하지 않는 경우, 모든 구독자는 동일한 메서드 시그니처를 구현해야 하고, 이벤트 관리를 위해 추가적인 코드가 필요합니다. 대리자를 사용하면, 이벤트 구독과 발행 과정이 더 유연하고 간결해지며, 다양한 시그니처의 메서드를 쉽게 처리할 수 있습니다.