본문 바로가기
C#

Delegate 이해하기

by Oz Driver 2025. 3. 10.

Delegate란?

Delegate(대리자)는 C#에서 함수의 참조를 저장하고 호출할 수 있는 객체입니다. 이는 C++의 함수 포인터와 유사하지만, 더 안전한 방식으로 관리됩니다.

Delegate를 사용하면 특정 메서드를 미리 지정하지 않고 실행 시점에서 메서드를 동적으로 호출할 수 있습니다. 이를 통해 코드의 유연성과 확장성을 높일 수 있습니다.

 

 

Delegate 선언 및 사용

Delegate를 선언하는 기본적인 방법은 다음과 같습니다.

// Delegate 선언
public delegate void MyDelegate(string message);

// Delegate를 사용하는 메서드 정의
public class Example
{
    public static void PrintMessage(string message)
    {
        Console.WriteLine($"메시지: {message}");
    }

    public static void Main()
    {
        // Delegate 인스턴스 생성
        MyDelegate del = new MyDelegate(PrintMessage);
        
        // Delegate 호출
        del("Hello, Delegate!");
    }
}

 

Delegate의 동작 방식 이해하기

MyDelegate del = new MyDelegate(PrintMessage);

 

이 부분이 대리자에게 함수를 전달하는 핵심입니다.

즉, 구현한 함수를 직접 호출하는 것이 아니라 누군가에게 대신 호출해달라고 부탁하는 것이죠.

그렇기 때문에 "대리자(delegate)"라고 부르는 것입니다.

 

public delegate void MyDelegate(string message)

이 선언은 특정 형식(반환형과 매개변수 타입)을 가지는 함수를 참조할 수 있는 "형식 정의"일 뿐입니다.

 

PrintMessage(string message)

실제로 실행될 함수입니다. 이 함수의 원형이 MyDelegate의 형식과 일치해야 합니다.

 

MyDelegate del = new MyDelegate(PrintMessage);

PrintMessage 를 참조하는 변수를 생성하는 것입니다.

즉, del이 PrintMessage를 가리키게 되고, del("Hello, Delegate!") 를 호출하면 결국 PrintMessage("Hello, Delegate!")가 실행됩니다.

 

이 과정에서 기존의 함수 호출 방식과 다르게, 함수가 직접 호출되지 않고 del이라는 변수를 거쳐 호출되기 때문에 처음 보면 혼란스러울 수 있습니다.

Delegate의 핵심 개념은 "어떤 형식의 함수를 가리킬 수 있는 변수"라는 점입니다.

그럼 굳이 왜 대리자를 통해 호출을 하는 것인지 살펴보겠습니다.

 

 

왜 Delegate를 사용할까?

상황에 따라 실행할 함수를 변경할 수 있기 때문입니다. 다음과 같은 예제를 살펴보겠습니다.

public class Notification
{
    public delegate void Notify(string message);

    public static void EmailNotification(string message)
    {
        Console.WriteLine("이메일 알림: " + message);
    }

    public static void SMSNotification(string message)
    {
        Console.WriteLine("SMS 알림: " + message);
    }

    public static void Main()
    {
        Notify notifier;
        
        // 이메일 알림을 사용
        notifier = EmailNotification;
        notifier("회원가입 완료");
        
        // SMS 알림으로 변경
        notifier = SMSNotification;
        notifier("회원가입 완료");
    }
}

 

위 예제에서는 notifier가 처음에는 EmailNotification을 가리키고 있다가 나중에는 SMSNotification을 가리키도록 변경됩니다. 즉, 실행 시점에서 실행할 함수를 동적으로 바꿀 수 있는 것이 Delegate의 핵심 장점입니다.

 

이제 좀 더 실질적인 예시를 생각해봅시다.

게임에서 플레이어가 전투 스타일을 변경할 수 있는 시스템을 만들어보겠습니다.

public class Player
{
    public delegate void AttackAction();
    private AttackAction attackStrategy;

    public void SetAttackStrategy(AttackAction newStrategy)
    {
        attackStrategy = newStrategy;
    }

    public void Attack()
    {
        if (attackStrategy != null)
            attackStrategy();
        else
            Console.WriteLine("공격 방식이 설정되지 않았습니다.");
    }

    public static void SwordAttack()
    {
        Console.WriteLine("칼로 강력한 일격!");
    }

    public static void BowAttack()
    {
        Console.WriteLine("활로 먼 거리 공격!");
    }

    public static void Main()
    {
        Player player = new Player();
        
        // 처음에는 칼 공격 전략
        player.SetAttackStrategy(SwordAttack);
        player.Attack();
        
        // 전투 중 활 공격 전략으로 변경
        player.SetAttackStrategy(BowAttack);
        player.Attack();
    }
}

 

이제 공격 방식을 변경하는 코드가 더욱 실용적인 형태로 변했습니다.

SetAttackStrategy를 통해 원하는 공격 방식을 동적으로 변경할 수 있으며, Attack 메서드를 호출하면 현재 설정된 공격 방식이 실행됩니다.

 

우리가 가장 많이 헤깔려 하는 부분이 함수 이름이 곧 변수처럼 사용된다는 사실입니다. 

당연하게도 함수를 호출하려면 함수명이 필요한데 이것을 변수처럼 사용하겠다는 의도입니다.

그리고 대리자에게 함수 이름을 넘겨주는 방식으로 delegate 는 동작합니다.

 

// 처음에는 칼 공격 전략
player.SetAttackStrategy( SwordAttack );

여기서 SwordAttack 은 다름 아닌 우리가 호출할 함수의 함수명입니다.

 

대리자를 구현할 때, 가장 먼저 해야 할 것은 대리자를 선언하는 것입니다.

가장 위에 선언한 코드입니다. 

public delegate void AttackAction();

 

대리자의 이름을 AttackAction 이라고 지었습니다. 

이제 AttackAction 대리자 (delegate) 는 자신이 선언한 형식에 맞는 함수는 어떤 것이든 대입만 시켜주면 마치 자기 함수인 것처럼 부를 수 있게 됩니다. 

 

함수의 형식은 void () 입니다.

즉, 반환값은 void 이며, 매개변수는 없는 함수로 선언된 것이면 어떤 함수든 자신이 호출할 수 있는 함수들이 되는 것입니다. 대리자에게 호출을 부탁할 함수 원형은 반환형의 유무, 매개변수의 유무 등 일반 함수와 동일한 형식을 갖추면 됩니다.

 

이제 대리자를 선언했으니 변수도 선언해야 합니다. 

private AttackAction attackStrategy;

 

attackStrategy 라고 변수 이름을 지었습니다.

이제 attackStrategy 는 AttackAction 형식에 맞다면 함수 이름으로 등록시켜서 부를 수 있습니다.

attackStrategy = SwordAttack;

attackStrategy = BowAttack;

처럼 말입니다.

다만, 예제 코드에서는 이 부분을 함수 인자로 받게 했게 했습니다. 

 

이제 player.Attack( 호출되기를 원하는 함수 ) 은 상황에 맞게 매개 변수로 원하는 함수 이름을 넣으면, 그 함수를 실행시킬 수 있게 되는 것입니다.

 

이처럼 Delegate를 사용하면 코드의 유연성이 높아지고, 실행 중에도 동적으로 행동을 변경할 수 있는 구조를 만들 수 있습니다.

 

 

Delegate의 활용

Delegate는 다양한 용도로 사용될 수 있으며, 특히 콜백 함수에서 유용하게 활용됩니다.

 

콜백 함수

콜백 함수는 특정 작업이 완료된 후 실행될 함수를 동적으로 지정할 때 사용됩니다.

여기서는 PrintResult 가 호출할 함수입니다. 

대리자가 원하는 함수의 원형은 void 이며, 매개 변수로 int 를 필요로 합니다. 

그리고 이 형식에 맞게 함수를 구현하고 대리자를 통해 호출을 부탁하는 것입니다. 

public class Process
{
    public delegate void CallbackDelegate(int result);

    public void DoWork(int x, int y, CallbackDelegate callback)
    {
        int result = x + y;
        callback(result); // 작업 완료 후 콜백 호출
    }
}

public class Program
{
    public static void PrintResult(int result)
    {
        Console.WriteLine($"연산 결과: {result}");
    }
    
    public static void Main()
    {
        Process process = new Process();
        process.DoWork(3, 4, PrintResult);
    }
}

 

 

정리

Delegate는 C#에서 메서드를 동적으로 참조하고 실행할 수 있도록 도와주는 기능입니다. 이를 활용하면 보다 유연한 코드 구조를 만들 수 있으며, 이벤트, 콜백 등의 기능을 효과적으로 구현할 수 있습니다.

 

특히, Delegate를 처음 접할 때 가장 어려운 부분은 함수를 직접 호출하는 것이 아니라 변수를 통해 호출한다는 점입니다. 이 개념을 이해하면 Delegate의 활용 방법이 더욱 명확해질 것입니다.