C#을 사용하다 보면 쉽게 접할 수 있고, 또 많이 사용하게 되는 delegate.
Google에 검색해 보면 Microsoft에서는 대리자라고 설명하고 있고, 뭔가 머리에 딱 정리가 되게끔 깔끔하게 설명을 해주고 있지는 않고 있다. 그래서, 이번 기회에 책들과 여러 자료들을 참고하면서 쉽게 순화해서 delegate를 이해할 수 있는 내용을 한번 블로그에 포스팅해보려고 한다!
요즘 핫한.. 지피티씨에게 그림으로 한번 Delegate를 그려달라고 해봤다.
생각보다 아주 야무지게 잘 그려줘서 감탄을 했다..! 지피티씨의 노력에 힘입어 delegate에 대해 한번 잘 정리해 보겠다.
Delegate의 정의
C나 C++을 사용하셨던 분들이라면 쉽게 말해 함수 포인터라고 볼 수 있다. 특정 매개 변수 목록 및 반환 형식이 있는 메서드들을 참조하는 것이 Delegate이다. (왜 복수형을 강조했는지는 뒤에서 알 수 있다.)
이 Delegate가 사용이 되는 예를 설명을 하자면 다음과 같이 정리를 할 수 있을 것 같다.
특정 시나리오(비즈니스 로직 혹은 프로그램 로직)에서 원형이 같은(매개변수 타입/개수, 반환 타입 등) 메서드를 호출해야 하는 상황에 Delegate를 사용하면 효과적이다.
실제로 Microsoft에서는 Delegate를 사용하는 예로 크게 Event 처리와 Callback 처리를 설명하고 있다. 주로 나는 Winform 프로젝트를 만들어서 작업을 진행하고 있기 때문에 Event 처리를 예시로 Delegate가 무엇인지를 설명해 보겠다.
Winform의 Event 처리에서 보는 Delegate 사용 예
Winform 프로젝트를 만들어서 작업을 하게 되면 다양한 컨트롤을 사용할 수 있고 각 컨트롤들의 이벤트를 사용해서 사용자와 상호작용 하는 프로그램을 만들 수 있다. 그중에서 대표적으로 Applicatin이 시작되고 종료될 때의 이벤트 흐름도를 그림으로 표현해 봤다.
이 그림이 의미하는 것은 간단하다. Program.cs 파일에 Main 함수에 있는 Application.Run() 메서드를 호출해서 Winform 응용프로그램이 실행되게 되면 프로세스가 어떻게 등록이 되어서 스레드가 어쩌고 저쩌고 와 같은 복잡한 절차를 통해서 프로그램이 궁극적으로 실행이 되게 되는데, 그 과정(시나리오) 속에서 Windows가 저런 이벤트 코드를 호출한다는 것이다.
즉, 시나리오 과정에서 때가 되면 Control.HandleCreated 이벤트(메서드)를 호출하고, 그다음 Control.BindingContextChanged 이벤트(메서드)를 호출하고... 이런 순서로 이벤트 코드들을 호출한다는 것이다. 이것을 처리하기 위해 C#에서는 Delegate를 사용해서 처리를 했다. 위에서 보이는 모든 이벤트 메서드들의 원형은 다음과 같다.
우선 메서드 원형을 살펴보자. 반환 타입은 void로 없으며 매개변수는 sender라는 이름의 object 타입 변수와 e라는 이름의 EventArgs 변수를 받고 있는 메서드 형식이다. 그리도 또 하나 살펴보자. 우리가 실제로 Winform 프로젝트에서 이벤트를 생성할 때 Designer.cs 파일에서는 어떻게 저장이 되는지를 말이다. 아래 예시는 Load 이벤트를 생성했을 때의 예시다.
//
// MainForm
//
this.Load += new System.EventHandler(this.MainForm_Load);
// 아래는 이해를 돕기 위해 Load 이벤트는 실제 내부에 어떻게 저장이 되었는지 탐색해봤다.
//
// 요약:
// 폼이 처음으로 표시되기 전에 발생합니다.
[SRCategory("CatBehavior")]
[SRDescription("FormOnLoadDescr")]
public event EventHandler Load
{
add
{
base.Events.AddHandler(EVENT_LOAD, value);
}
remove
{
base.Events.RemoveHandler(EVENT_LOAD, value);
}
}
우리가 Form과 UserControl에서 사용하는 Load 프로퍼티, 이벤트 코드를 만들게 되면 실제로 내부적으로는 base.Events라고 하는 Delegate를 저장하고 있는 List에 추가를 한다.
즉, 이미 Winform 프로젝트는 프로그램을 실행해서 진행하는 시나리오 속에서 호출할 이벤트들의 순서와 이름 규칙이 이미 정의되어 있고, 이 프로젝트를 만들어서 사용하는 우리 개발자들이 원하는 순서에 해당하는 이벤트 메서드(delegate)를 만들어서 추가(+=)만 해주면 알아서 해당 메서드를 호출하게 되는 구조다.
private void MainForm_Load(object sender, EventArgs e)
{
AddControl(new MainPageControl());
DevideMenuClick(this.lbDevideMenu1, null);
}
다시 한번 내가 제일 위에서 설명했던 글을 되짚어 보자.
"특정 시나리오(비즈니스 로직 혹은 프로그램 로직)에서 원형이 같은(매개변수 타입/개수, 반환 타입 등) 메서드를 호출해야 하는 상황에 Delegate를 사용하면 효과적이다."
Winform 이벤트 처리 과정에서 보는 것과 마찬가지로 특정 시나리오에서 어떤 이벤트가 발생이 되게 되는데, 매개변수는 같은 상황에서 해당 이벤트 처리를 각기 다른 사람이 정의한 대로 동작을 시켜야 하는 상황일 때! 그럴 때 사용하는 것이 delegate이다. 메서드 원형이 같은 delegate를 시나리오에서 사용하게 하여, 시나리오 상 코드 변경은 없이 외부에서 정의한 실제 메서드들을 갈아서 끼울 수 있는 형태의 객체지향적인 코드가 될 수 있게 되는 것이다.
Callback 함수로 보는 delegate 사용 예
Callback 함수를 사용하는 C# 코드를 분석하게 되면 조금 더 명확하게 이 delegate가 무엇인지 이해를 할 수 있다. C#의 Delegate 형식에는 비동기 함수를 지원하는 메서드도 존재한다. 이 메서드는 또한, Callback 함수를 사용해서 비동기 함수의 진행 상태에 따른 처리를 진행할 수 있다. 우선 코드를 살펴보자. 아래는 Delegate의 BeginInvoke와 Callback 함수를 사용한 예제 코드다.
delegate int SampleDelegate(int n, out int s);
static void Main()
{
SampleDelegate del = DoSomething;
int rndVal = new Random().Next(10, 1000);
int sum = 0;
DateTime starTime = DateTime.Now;
Console.WriteLine($"{starTime.ToString("yyyy-MM-dd HH:mm:ss")} 실행 시작");
IAsyncResult ar = del.BeginInvoke(rndVal, out sum, new AsyncCallback(SampleCallback), null);
ar.AsyncWaitHandle.WaitOne();
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 실행 종료");
Console.WriteLine($"{(DateTime.Now - starTime).TotalMilliseconds} ms 실행");
}
static int DoSomething(int n, out int sum)
{
Console.WriteLine($"1부터 {n}까지 더하기 작업을 시작합니다.");
sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
Thread.Sleep(10);
}
return sum;
}
static void SampleCallback(IAsyncResult ar)
{
SampleDelegate tempDel = ((AsyncResult)ar).AsyncDelegate as SampleDelegate;
int result = 0;
tempDel.EndInvoke(out result, ar);
Console.WriteLine($"1부터 {ar.AsyncState}까지 더하기 실행 결과는 {result} 입니다.");
}
Microsoft에서는 비동기 처리 프로그래밍 방식을 애초에 Delegate 타입으로 구현을 하여서 교육 자료까지 만들어서 배포했다. (MS 공식 자료)
대리자를 사용한 비동기 프로그래밍 - .NET
자세한 정보: 대리자를 사용한 비동기 프로그래밍
learn.microsoft.com
비동기 작업은 언제 처리가 끝날 지 예상이 불가능한 작업들을 처리하려고 할 때 적용하는 게 좋다. 위 코드는 10에서 1000까지의 랜덤 한 난수를 발생해서 비동기 메서드를 활용하여 1부터 해당 난수까지 덧셈 결과를 받아오는 코드를 작성해 본 것이다. Delegate의 BeginInvoke 메서드를 호출하면 그 즉시 메서드가 호출이 되며, 완료를 기다리지 않고 비동기 호출의 진행률을 모니터링하는 데 사용할 수 있는 IAsyncResult 타입을 반환한다.
BeginInvoke를 실행할 때는 해당 Delegate 타입이 갖는 매개변수들과 비동기 함수가 종료됐을 때 호출이 되는 CallBack 함수 Delegate 타입을 받는다. 물론 Callback 함수를 별도로 생성하지 않은 경우에는 해당 인자를 null 값으로 전달할 수 있다.
즉, 메서드를 참조하는 역할을 타입인 Delegate에서 제공하는 BeginInvoke는 참조되어 있는 메서드를 호출함과 동시에 해당 메서드의 수명이 끝났을 때(메서드가 종료되는 시점)에 호출되는 Callback 메서드 또한 Delegate로 전달받아 종료되는 시점에 전달받은 Callback 메서드를 호출하는 구조로 만들어져 있다.
Callback 함수 역시 이미 정해져 있는 시나리오에서 호출이 되는 시점은 분명하고, 처리하는 방식만 우리 개발자들이 정의를 하면 되는 것이기에 이와 같이 Delegate를 활용하여 만들어지게 된 것이다.
어찌어찌 Delegate에 대해서 한 번 주저리 정리를 해봤는데, 조금이나마 이해하는데 도움이 되길 바라며 오늘의 정리는 여기서 마무리!