포스트

[delegate] 01.델리게이트의 정의와 정체, 체이닝, 사용 목적

델리게이트(Delegate)는 C#에서 중요한 개념 중 하나로, 메서드 참조저장하고 호출할 수 있는 형식을 제공하는 기능입니다.

C++의 함수 포인터와 비슷한 개념인데, 좀 더 많은 기능을 제공합니다.

델리게이트를 사용하면 메서드를 매개변수로 전달하거나, 런타임에 호출할 메서드를 동적으로 결정할 수 있습니다.

델리게이트 사용

델리게이트는 delegate 예약어를 사용하여 메서드의 시그니처를 선언하듯 정의합니다.

1
접근제한자 delegate 반환타입 델리게이트_이름([매개변수 목록...])

델리게이트 인스턴스 생성 및 사용

1
public delegate bool ActionBtnDelegate();

만약, 위와 같이 델리게이트를 선언했다면, 클래스 인스턴스 생성하듯 해당 델리게이트 이름으로 인스턴스를 생성할 수 있습니다.

또한, C# 2.0부터는 new 없이도 인스턴스를 쉽게 생성할 수 있는 기능을 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 일반 메서드 정의
private static bool ActionBtn1()
{
    Console.WriteLine("ActionBtn1");
    return true;
}
private static bool ActionBtn2() {
    Console.WriteLine("ActionBtn2");
    return true;
}
static void Main(string[] args)
{
	// 델리게이트 인스턴스 생성
    ActionBtnDelegate actionBtnDelegate1 = new ActionBtnDelegate(ActionBtn1);
    // C# 2.0부터는 new 없이도 생성 가능
    ActionBtnDelegate actionBtnDelegate2 = ActionBtn2;
}

이제 이 델리게이트를 다양하게 사용 할 수 있는데, 이 델리게이트를 테스트 할 수 있는 테스트 클래스를 만들어 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DelegateTest
{
    private ActionBtnDelegate actionBtnDelegates;

    public ActionBtnDelegate GetNowOperableDelegate()
    {
        return actionBtnDelegates;
    }
    public void SetActionDelegate(ActionBtnDelegate actionBtnDelegate)
    {
        actionBtnDelegates = actionBtnDelegate;
    }
    public void Action()
    {
        if (actionBtnDelegates != null)
        {
            actionBtnDelegates();
        }
        else
        {
            Console.WriteLine("등록된 델리게이트가 없습니다.");
        }
    }
}

일단, ActionBtnDelegate가 델리게이트 객체 혹은 타입을 가리키는 것이라고 생각하고 어디어디에 어떤 의도로 메서드들을 만들었는지 보겠습니다.
actionBtnDelegates 필드는 어디서든 저장된 메서드를 호출할 수 있도록 메서드 참조를 저장하는 변수입니다.
GetNowOperableDelegate()는 현재 등록된 델리게이트를 반환하는 메서드, SetActionDelegate()새로운 델리게이트를 등록하는 메서드입니다.
그리고 Action() 메서드에서 현재 actionBtnDelegates 필드에 저장된 메서드를 호출하는 메서드입니다.

즉, 특정한 메서드를 변수에 저장해서 언제 어디서든 사용할 수 있게 만들었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal class Program
{
    private static bool ActionBtn1()
    {
        Console.WriteLine("ActionBtn1");
        return true;
    }
    private static bool ActionBtn2() {
        Console.WriteLine("ActionBtn2");
        return true;
    }

    static void Main(string[] args)
    {
        ActionBtnDelegate actionBtnDelegate1 = new ActionBtnDelegate(ActionBtn1);
        ActionBtnDelegate actionBtnDelegate2 = ActionBtn2;

		// 테스트 클래스 인스턴스 생성
        DelegateTest delegateTest = new DelegateTest();
	    // 현재 범위 내의 메서드를 델리게이트에 등록
        delegateTest.SetActionDelegate(actionBtnDelegate1);
        // 등록된 델리게이트를 확인
        ActionBtnDelegate actionBtnDelegate = delegateTest.GetNowOperableDelegate();
        if (actionBtnDelegate != null)
        {
            Console.WriteLine($"Now Action : {actionBtnDelegate.ToString()}");
            // Now Action : _12_Delegate.ActionBtnDelegate
        }
        // 테스트 클래스 인스턴스 내에서 액션도 호출해 봄.
        delegateTest.Action(); // ActionBtn1
    }
}

위 예제와 같이 Main을 구성하고 테스트를 해보니 Program 클래스 내부의 메서드도 잘 호출하고 있는 모습을 확인할 수 있었습니다.

일급 객체

어떻게 위와 같은 형태의 메서드 저장 및 호출이 가능한 것일까?
그건 델리게이트가 일급 객체이기 때문입니다.

일급 객체란, 간단히 말하면 다음과 같은 속성을 가진 객체를 말합니다.

  1. 변수에 할당될 수 있습니다.
  2. 함수의 인자로 전달될 수 있습니다.
  3. 함수의 반환값으로 사용될 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DelegateTest
{
	// 변수(필드)에 할당
    public ActionBtnDelegate actionBtnDelegates;

	// 반환 값으로 사용
    public ActionBtnDelegate GetNowOperableDelegate()
    {
        return actionBtnDelegates;
    }

	// 인자로 전달 가능
    public void SetActionDelegate(ActionBtnDelegate actionBtnDelegate)
    {
        actionBtnDelegates = actionBtnDelegate;
    }
}

따라서, 위 테스트 클래스를 보면, 변수에 할당하기도 하고, 인자에 사용되기도 하고, 반환 값으로 사용하기도 하는 모습을 볼 수 있습니다.

델리게이트 체이닝

사실, delegateMulticastDelegate 타입의 간편 표기법입니다.

System.MulticastDelegate 타입으로 올라가 보면, 델리게이트와 관련된 기능을 제공하는 추상 클래스라는 것을 알 수 있습니다.
따라서, delegate는 다양한 기능들을 제공하는데, 그 중 하나가 델리게이트 체이닝입니다.

델리게이트는 여러 메서드를 호출할 수 있는 체인으로 결합될 수 있습니다.

하나의 델리게이트 인스턴스에 여러 메서드를 순차적으로 연결하는 기술델리게이트 체이닝이라고 하고, 이렇게 한 번의 호출로 여러 메서드를 동시에 실행할 수 있는 델리게이트멀티캐스트 델리게이트라고 합니다.

델리게이트는 연산자 =로 호출할 델리게이트를 설정하고, +로 메서드를 추가하거나 -로 메서드를 제거할 수 있습니다.

1
2
3
4
5
ActionBtnDelegate test = ActionBtn1;
test = test + ActionBtn2; // test += ActionBtn2;
test();
// ActionBtn1
// ActionBtn2

그 외 델리게이트가 지원하는 멤버

그 외에도 BeginInvoke()EndInvoke() 같은 비동기 호출을 지원하는 메서드라던가, 델리게이트의 메서드 메타데이터를 참조하는 Method 필드, 델리게이트가 참조하는 메서드의 대상 객체를 반환하는 Target 필드 등을 지원합니다.

멤버설명
Invoke()델리게이트가 참조하는 메서드를 동기적으로 호출합니다.
BeginInvoke()델리게이트가 참조하는 메서드를 비동기적으로 호출합니다. 호출이 즉시 반환됩니다.
EndInvoke()BeginInvoke로 시작한 비동기 호출의 결과를 수집합니다.
GetInvocationList()멀티캐스트 델리게이트에 연결된 개별 델리게이트들의 배열을 반환합니다.
Method델리게이트가 참조하는 메서드의 메타데이터를 제공합니다.
Target델리게이트가 참조하는 메서드가 호출될 대상 객체를 반환합니다.

델리게이트를 어디에 쓰는가?

만약, 게임을 만들 때, 어떤 동작(Action)의 버튼을 누르면 다양한 동작이 가능하도록 만든다고 생각해 봅시다.
게임의 Action 버튼을 눌렀을 때, 적의 등 뒤에서는 암살을 수행하고, 문 앞에서는 문을 여는 행동을 하고, 땅에 떨어진 아이템 주변에서는 아이템을 줍는 등의 동작을 할 수 있습니다.

즉, Action 버튼을 눌렀을 때 어떤 동작을 할지 미리 정할 수 없기 때문에 런타임에 실행할 메서드를 동적으로 결정할 수 있어야 합니다.

그럴 때 델리게이트를 쓰면 이벤트를 편리하게 관리 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public delegate bool ActionBtnDelegate();

internal class Player
{
    public ActionBtnDelegate actionBtnDelegates;

    public void OnAction()
    {
        actionBtnDelegates?.Invoke();
    }
}
internal class Item
{
    public bool PickUp()
    {
        Console.WriteLine("아이템을 줍습니다.");
        return true;
    }
}
internal class Door
{
    public bool Open()
    {
        Console.WriteLine("문을 엽니다.");
        return true;
    }
}

internal class Enemy
{
    public bool StealthKill()
    {
        Console.WriteLine("암살을 합니다.");
        return true;
    }
}
static void Main(string[] args)
{
	Player player = new Player();
	Item item = new Item();
	player.actionBtnDelegates = item.PickUp;
	player.OnAction(); // 아이템을 줍습니다.

	Door door = new Door();
	player.actionBtnDelegates = door.Open;
	player.OnAction(); // 문을 엽니다.

	Enemy enemy = new Enemy();
	player.actionBtnDelegates = enemy.StealthKill;
	player.OnAction(); // 암살을 합니다.
}

물론, 런타임에 실행할 메서드를 동적으로 결정한다는 말을 들었을 때, 인터페이스와 전략패턴 등… 다양한 방법이 생각 나셨을 수 있습니다.

물론, 프로그래밍에는 정답이 없기 때문에 다양한 방법으로 기능을 구현할 수 있습니다.

그러나 델리게이트는 특히 콜백 메서드를 구현하기 매우 좋으며, 이벤트 핸들링, 비동기 처리 등의 기능 등에 사용할 수 있습니다.

이를 통해 코드의 재사용성을 높이고 유지 보수를 용이하게 할 수 있습니다.


참고

[delegate] 02. event와 EventArgs (EventHandler)

[delegate] 03. 익명 델리게이트와 Func, Action

인터페이스(Interface)와 추상 클래스(Abstract Class)

[함수 포인터] 01. 함수 포인터와 멤버 함수 포인터

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.