- 델리게이트란(delegate)?
직역하면 '대리자'라는 뜻이다. 즉, 무언가를 대신해주는 역할을 하는 것으로 c#에서는 메소드를 대신 호출해주는 역할(c++의 함수 포인터와 같은 역할)을 한다. 대리자는 '메서드에 대한 참조'이다.
[한정자] delegate [반환형식] [델리게이트名] (매개변수타입 매개변수명)
ㄴ 대리자는 메서드에 대한 참조이기 때문에 '자신이 참조할 메서드의 반환 형식'과 '매개변수'를 명시해야 한다.
namespace DelegateSample
{
internal class Program
{
delegate void MyDelegate(int val);
static void Main(string[] args)
{
//메서드 직접 호출
DelegateTest(3);
//delegate를 활용한 메서드 대리 호출(간접)
MyDelegate dele = new MyDelegate(DelegateTest);
dele(4);
}
static void DelegateTest(int myVal)
{
Console.WriteLine($"DelegateTest() called {myVal}");
}
}
}
위 예시를 통해 delegate를 활용하여 DelegateTest 메서드를 대신 호출한 dele(4)
과 직접 DelegateTest() 메서드를 호출하여 실행한 DelegateTest(3)
가 같은 결과를 출력하는 것을 알 수 있다. 이처럼, delegate는 대리자의 역할을 한다!
ㄴ ❌ 여기서 주의할 점이 있다. 반환형식이 void인 델리게이트에 반환형식이 int인 메서드를 인자로 넣으면 '오류'가 난다.
💡반환형식이 int인 델리게이트를 별도로 만든 후 메서드를 인자로 받아야 실행할 수 있다. 즉, 대리자가 참조할 메서드들의 반환 형식과 매개변수는 대리자의 반환 형식과 매개변수를 따라야 한다!
이쯤에서 의문이 생긴다. 직접 메서드를 호출하는 코드가 더 짧은데 '왜' delegate를 사용해야 하는 것일까?
위 예시처럼 하나의 메서드만 실행하면 될 경우에는 delegate가 필요없다.
하지만 아래와 같은 경우에는 delegate가 유용하다!
어느 휴게소의 푸드코트에서 모든 가게가 같은 'ConsoleMenu' 시스템을 사용하고 있다고 가정해보자. delegate를 사용하지 않고 메뉴판시스템을 구축할 경우 'Menu1을 선택하면, makeHotdog()가 실행되도록 하라'처럼 미리 실행될 메서드가 지정해야 한다. 이럴 경우, 어느 가게에서든 손님이 Menu1을 선택하면 무조건 makeHotdog()가 실행되어버린다;;; 이를 간단하게 해결해주는 것이 'delegate'의 콜백(Callback)기능이다!
ㄴ 이처럼 ConsoleMenu는 메뉴를 Show()보여주고, Read() 선택을 읽기만 할 뿐 구체적으로 어떤 실행을 할지는 모른다. 그저 읽은 값에 맞는 delegate가 호출자가 가진 실행할 메서드를 대신 호출한다. 즉, 핫도그코너에서 손님이 1번을 선택하면 delegate_1이 핫도그 코너가 가진 makeHotdog() 함수를 호출하고, 라면코너에서 손님이 1번을 선택하면 이번에 delegate_1은 makeRamen() 함수를 호출하는 것이다!
실제 코드는 아래와 같다
namespace DelegateTest
{
internal class Program
{
static void Main(string[] args)
{
DelegateTest dele = new DelegateTest();
dele.Run();
}
}
class DelegateTest
{
ConsoleMenu Menu;
public DelegateTest() //생성자
{
Menu = new ConsoleMenu(); //MenuList가 준비됨
}
public void Run()
{
CreateMenu(); //메뉴 만들기
while (true) //Q 누르기 전까지 무한 루프 생성
{
Menu.Show();
Menu.Read();
Console.WriteLine();
}
}
private void CreateMenu()
{
//MenuItem item = new MenuItem("1", "Menu_Title_1", Menu_1_Callback);
Menu.MenuList.Add(new MenuItem("1", "Menu_Title_1", Menu_1_Callback));
Menu.MenuList.Add(new MenuItem("2", "Menu_Title_2", Menu_2_Callback));
Menu.MenuList.Add(new MenuItem("Q", "프로그램 종료", Quit_Callback));
}
private void Menu_1_Callback(object sender, MenuArgs args)
{
Console.WriteLine($"Menu_1_Callback() 호출됨 sender = {sender.ToString()}, args = {((MenuKeyPressArgs)args).MenuChar}");
//자식 클래스의 MenuChar을 가져오기 위해 (MenuKeyPressArgs)로 캐스팅해줌
}
private void Menu_2_Callback(object sender, MenuArgs args)
{
Console.WriteLine($"Menu_2_Callback() 호출됨 sender = {sender.ToString()}, args = {((MenuKeyPressArgs)args).MenuChar}");
}
private void Quit_Callback(object sender, MenuArgs args)
{
Console.WriteLine($"Quit_Callback() 호출됨 sender = {sender.ToString()}, args = {((MenuKeyPressArgs)args).MenuChar}");
Console.WriteLine("Bye!");
Environment.Exit(0); //콘솔 프로그램 종료 시키기
}
}
class MenuItem
{
public delegate void MenuKeyPressDelegate(object sender, MenuArgs args);
public string MenuChar { get; set; } //1,2, ..., q 등 메뉴 번호
public string MenuTitle { get; set; } //menu1 등 메뉴 이름
public MenuKeyPressDelegate KeyPressDelegate { get; set; } //누른 MenuChar에 따라 delegate가 호출할 함수 //func(sender, args)
public MenuItem(string menu_char, string menu_title, MenuKeyPressDelegate dele) //생성자에 미리 값들을 다 넣으면 여기로 받음
{
MenuChar = menu_char;
MenuTitle = menu_title;
KeyPressDelegate = dele;
}
public MenuItem() : this(null, null, null) { } //이 생성자로 받는다면 추후에 프로퍼티 값들을 넣겠다는 뜻
}
class MenuArgs { } //부모클래스
class MenuKeyPressArgs : MenuArgs //상속
{
//delegate(등록한 함수)한테 반환해줄 것을 작성
//ex) 사용자가 '1'을 눌렀다는 것을 Menu에서 알도록 MenuChar를 반환해줌
public string MenuChar { get; set; }
public MenuKeyPressArgs(string menu_char)
{
MenuChar = menu_char;
}
}
class ConsoleMenu
{
public List<MenuItem> MenuList { get; set; }
public ConsoleMenu()
{
MenuList = new List<MenuItem>();
}
public void Show() //Menu 보여주기
{
foreach (MenuItem item in MenuList)
Console.WriteLine($"{item.MenuChar}. {item.MenuTitle}");
Console.WriteLine();
}
public void Read() //Menu 읽기
{
Console.WriteLine("원하는 메뉴의 번호를 입력하시오: ");
string retVal = Console.ReadLine(); //원하는 메뉴를 입력
foreach (MenuItem item in MenuList)
{
//입력한 값이 MenuList에 있고, Delegate가 주어져 있다면
if (item.MenuChar == retVal && item.KeyPressDelegate != null)
item.KeyPressDelegate(this, new MenuKeyPressArgs(retVal));
//해당 delegate를 호출 (delegate가 가리키고 있는 메서드 호출)
//this : 현재의 ConsoleMenu 클래스 인스턴스
}
}
}
}
게임 중에 Enemy를 죽이면 Coin, Point, Exp를 모두 얻게 되는 코드를 짜보자(delegate 사용 X)
namespace DelegateMultiMethod
{
internal class Program
{
static int userCoin = 0;
static int userPoint = 0;
static int userExp = 0;
//Coin 적립
static void PlusCoin(int coin)
{
userCoin += coin;
Console.WriteLine($"Coin : {userCoin}");
}
static void PlusPoint(int point)
{
userPoint += point;
Console.WriteLine($"Point : {userPoint}");
}
static void PlusExp(int exp)
{
userExp += exp;
Console.WriteLine($"Exp : {userExp}");
}
static void Main(string[] args)
{
PlusCoin(100);
PlusPoint(30);
PlusExp(10);
}
}
}
이렇게 Main()에서 PlusCoin(), PlusPoint(), PlusExp()를 모두 실행시켜야 한다. 만약 실행할 메서드를 더 추가해야하 한다면 번거롭고 복잡해진다.
Delegate를 사용하면 아래처럼 작성할 수 있다.
namespace DelegateMultiMethod
{
delegate void PlusDelegate(int coin, int point, int exp);
internal class Program
{
static int userCoin = 0;
static int userPoint = 0;
static int userExp = 0;
//Coin 적립
static void PlusCoin(int coin, int point, int exp)
{
userCoin += coin;
Console.WriteLine($"Coin : {userCoin}");
}
static void PlusPoint(int coin, int point, int exp)
{
userPoint += point;
Console.WriteLine($"Point : {userPoint}");
}
static void PlusExp(int coin, int point, int exp)
{
userExp += exp;
Console.WriteLine($"Exp : {userExp}");
}
static void Main(string[] args)
{
PlusDelegate killMonster = PlusCoin;
killMonster += PlusPoint;
killMonster += PlusExp;
killMonster(100, 30, 10);
}
}
}
이렇듯 delegate를 사용하면 killMonster() 메서드 하나만 실행시키면 된다. 위 코드에서 처럼 killMonster라는 delegate 변수에 함께 실행할 메서드를 추가(구독)하려면 '+=' 연산자를 사용하고, 메서드를 삭제(구독해제)하려면 '-='를 사용하면 된다. 이렇게 delegate에 다중 메서드를 만드는 것을 '델리게이트 체인(delegate Chain)'이라고 한다.
Event는 c#에서 발생하는 특정한 동작을 나타내는 신호로 해당 동작에 대한 응답을 받을 수 있도록 프로그램에 알림을 제공한다. 즉, 객체 간의 상호작용을 위한 통신 메커니즘이다.
'이벤트'는 발생한 사건을 처리하기 위해 EventHandler
라는 메서드를 등록하고, 이벤트가 발생하면 등록된 모든 이벤트 핸들러가 순차적으로 호출되어 해당 동작에 대한 응답을 처리한다. EventHandler
는 이벤트에 대한 구독자로서, 이벤트가 발생하면 자동으로 호출되어 처리 작업을 수행한다.
이벤트 핸들러(EventHandler)는 Delegate 정의없이 한 번에 이벤트를 사용할 수 있게 도와주는 C# 내부 델리게이트이다.
위에서 Delegate를 사용하여 만들었던 예시를 EventHandler로 바꿔보자.
namespace DelegateTest
{
internal class Program
{
static void Main(string[] args)
{
DelegateTest dele = new DelegateTest();
dele.Run();
}
}
class DelegateTest
{
ConsoleMenu Menu;
public DelegateTest() //생성자
{
Menu = new ConsoleMenu(); //MenuList가 준비됨
}
public void Run()
{
CreateMenu(); //메뉴 만들기
while (true) //Q 누르기 전까지 무한 루프 생성
{
Menu.Show();
Menu.Read();
Console.WriteLine();
}
}
private void CreateMenu() //EventHandler에 실행할 메서드들을 구독시킨다
{
MenuItem item = new MenuItem("1", "Menu_Title_1");
item.MenuKeyPressEventHandler += Menu_1_Callback;
Menu.MenuList.Add(item);
item = new MenuItem("2", "Menu_Title_2");
item.MenuKeyPressEventHandler += Menu_2_Callback;
Menu.MenuList.Add(item);
item = new MenuItem("Q", "프로그램 종료");
item.MenuKeyPressEventHandler += Quit_Callback;
Menu.MenuList.Add(item);
}
private void Menu_1_Callback(object sender, EventArgs args)
{
Console.WriteLine($"Menu_1_Callback() 호출됨 sender = {sender.ToString()}, args = {((MenuKeyPressArgs)args).MenuChar}");
//자식 클래스의 MenuChar을 가져오기 위해 (MenuKeyPressArgs)로 캐스팅해줌
}
private void Menu_2_Callback(object sender, EventArgs args)
{
Console.WriteLine($"Menu_2_Callback() 호출됨 sender = {sender.ToString()}, args = {((MenuKeyPressArgs)args).MenuChar}");
}
private void Quit_Callback(object sender, EventArgs args)
{
Console.WriteLine($"Quit_Callback() 호출됨 sender = {sender.ToString()}, args = {((MenuKeyPressArgs)args).MenuChar}");
Console.WriteLine("Bye!");
Environment.Exit(0); //콘솔 프로그램 종료 시키기
}
}
class MenuItem
{
//public delegate void MenuKeyPressDelegate(object sender, EventArgs args);
public event EventHandler MenuKeyPressEventHandler; //EventHandler가 이미 delegate를 갖고 있기 때문에
public string MenuChar { get; set; }
public string MenuTitle { get; set; }
//public MenuItem(string menu_char, string menu_title, MenuKeyPressDelegate dele)
public MenuItem(string menu_char, string menu_title)
{
MenuChar = menu_char;
MenuTitle = menu_title;
//KeyPressDelegate = dele;
}
public MenuItem() : this(null, null) { }
public void CallEvent(object sender, string args)
{
if(MenuKeyPressEventHandler != null)
MenuKeyPressEventHandler(sender, new MenuKeyPressArgs(args));
}
}
class MenuKeyPressArgs : EventArgs //EventArgs: .NET Framework에서 지원함
{
public string MenuChar { get; set; }
public MenuKeyPressArgs(string menu_char)
{
MenuChar = menu_char;
}
}
class ConsoleMenu
{
public List<MenuItem> MenuList { get; set; }
public ConsoleMenu()
{
MenuList = new List<MenuItem>();
}
public void Show() //Menu 보여주기
{
foreach (MenuItem item in MenuList)
Console.WriteLine($"{item.MenuChar}. {item.MenuTitle}");
Console.WriteLine();
}
public void Read() //Menu 읽기
{
Console.WriteLine("원하는 메뉴의 번호를 입력하시오: ");
string retVal = Console.ReadLine();
foreach (MenuItem item in MenuList)
{
//if (item.menuchar == retval && item.keypressdelegate != null)
if (item.MenuChar == retVal)
item.CallEvent(this, retVal);
}
}
}
}
if (item.MenuChar == retVal && item.KeyPressDelegate != null)
item.KeyPressDelegate(this, new MenuKeyPressArgs(retVal));
public void CallEvent(object sender, string args)
{
if(MenuKeyPressEventHandler != null)
MenuKeyPressEventHandler(sender, new MenuKeyPressArgs(args));
}
이벤트는 대리자에 event 키워드로 수식해서 선언한 것에 불과한데, C#은 왜 event를 지원할까?
'외부에서 직접 사용할 수 없다'라는 이벤트의 성격 때문이다!
이벤트는 public 한정자로 선언되어 있어도 자신이 선언된 클래스 외부에서는 호출이 불가능하다. 반면 대리자는 public이나 internal로 수식되어 있으면 클래스 외부에서라도 얼마든지 호출이 가능하다.
즉, Event를 사용하면 견고한 이벤트 기반 프로그래밍을 할 수 있는 반면에 대리자는 위협을 막을 수 없다. 따라서 대리자는 '콜백' 용도로 사용하고, 이벤트는 '객체의 상태 변화나 사건의 발생을 알리는 용도'로 구분해서 사용해야 한다.
📄참고자료
[인프런] c# 프로그래밍 기초_Delegate를 사용한 ConsoleMenu 만들기 실습_EventHandler 실습
델리게이트를 사용하는 이유_다중 메서드 호출 예시 참고
델리게이트
델리게이트와 이벤트 사용 시 주의사항