Delegate와 Event의 이해(1)

Posted by 나에요임마
2017. 12. 10. 11:19 Program/C#

이제부터 얘기해 주제는  파리~~바게트!” .. 아니다.

바로 C# Delegate And Event

Delegate Event 도대체 무엇일까?

가지에 대해서 가장 아는 방법은 차이점을 따져보면서 쓰이게 됐는지에

대해서 살펴보는 가장 좋다.

(개발에 있어 가지는 "Why?" 는 상당히 중요하다)

그러기에 앞서 가장먼저 파리바게트 글자수마져도 같은

델리게이트(Delegate)” 녀석에 해서 먼저 알아보도록 하자.

 

1.)     Delegate(델리게이트) 무엇일까~?

 -  C/C++ 을 접해본 사람들은 함수포인터(Func Pointer)에 대해서 들어봤을 거라 생각한다.  접해보지 못했다는 분들을 위해 간략하게 "stdlib.h" 내에 있는 qsort함수를 살펴보면 아래와 같다.

 

Code Snippet
  1. void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *));

 

먼가 복잡해 보이는 듯 하지만 다른 건 볼 필요 없고 끝의

 

int(*compar)(const void *, const void *));

 

형태만 보자. 이 형태는 int형을 리턴하고 인자는 2개의 void형 포인터를 갖는 함수형태를 파라미터로 받는 다는 말이다. 이게 바로 함수 포인터의 정체다.(C/C++상에서)

 

일반적으로는

void (*pf)(int *);

위와같이 Definition 을 사용한다.

(함수포인터를 콜하는 등의 예는 다루고자 하는 내용과 거리가 멀어 생략합니다.)

 

즉, 아주 간략히 말하자면 C#에서의 "Delegate" 는 C/C++의 함수포인터와 매우 유사한 면을 지니고 있다. C#에서 "Delegate" 는 간접 접근의 한 방법으로 사용되며 이는 "Delegate"의 사전적의미인 아래의 내용과 동일하게 사용된다.

 

 
delegate   [delig?t, -geit] [US]
  • n.
    • 대표, 사절, 파견 위원, 대리인(deputy)
 
예를 들어, 글쓴이가 저녁에 요리를 해먹기 위해 4:30분에 슈퍼에 가서 두부 한모, 파슬린, 국간장 등을 사오는 행위를 해야하는데 이를 메모지에 적어놓기만 하고 사정상 다른사람에게 부탁을 했다고 치면 부탁받음 사람은 이 일련의 작업을 메모지를 들고가서 글쓴이 대신 처리하게 되는 과정과 비슷하다.
(날더운데 대신 가줄사람이 있겠냐만은....그런 착한 사람이 있다고 치자......)
 
여기서 이 메모장이 하는 역할이 일종의 "Delegate" 즉 대리자와 비슷하다.
그럼 이 "델리게이트" 란 녀석을 왜 쓰고 어디에 쓰는 걸까??
 
위에서 잠깐 언급한 메모지를 다시 생각해보자.
 
"예를 들어 글쓴이가 위 메모지의 내용대로 자주 음식을 해먹기 때문에 슈퍼에 자주 가야하는 상황이 생기고 또 우연히? 항상 슈퍼에 자주 갈수 없는 상황이 생겨서 누군가에게 대신 이러한 작업을 위임해야 한다고 하면 그 대상이 누가됐던간에 글쓴이는 이 메모지를 대신가줄 불쌍한 이?에게 전달만해주면 된다."
 
즉, "Delegate" 는 주로 코드에서
"어떠한 작업을 하고 싶은데 그 작업이 명확치 않은경우" 에 주로 쓰인다.
 
"간단히 말하자면 위의 예에서 글쓴이가 어떤사람이건 아무나 붙잡고 심부름을 시킨다고 쳤을 때 이사람은 아직 뭘 해야 하는지 모르는 상태고 이때 글쓴이가 준 메모지가 이 심부름 하는 사람에게 어떠한 작업을 해야한다...라고 알려줄 수 있는 역할을 한다는 것이다."
 
C#에서 자주쓰이는 대표적인 예가 "Thread" 이다.
아래 코드를 살펴보자.
 
Code Snippet
  1. class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             Thread thread = new Thread(new ThreadStart(Prn));
  6.             thread.Start();
  7.         }
  8.         static void Prn()
  9.         {
  10.             Console.WriteLine("Printing.......Anywhere");
  11.         }
  12.     }
EndFragment
EndFragment
 
위 코드에서 Thread가 시작됨과 동시에 Prn이라는 함수를 호출하고
"Printing........Anywhere"
라는 문자를 찍는 작업을 할 수 있는 건 ThreadStart라는 Delegate 때문이다.
 
* Thread는 그 자체가 무엇을 실행해야 할지는 모르지만
  Delegate 를 통해 해당 작업을 수행하게 된다. *
 
-------------
ThreadStart라는 Delegate는 아래와 같이 선언되있다.
 
Code Snippet
  1. namespace System.Threading
  2. {
  3.     // Summary:
  4.     //     Represents the method that executes on a System.Threading.Thread.
  5.     [ComVisible(true)]
  6.     public delegate void ThreadStart(); ->델리게이트타입
  7. }
EndFragment
-------------
 
Delegate라는 Type은 위에서 설명한 것과 같은 대리자, 역할위임의 수행을 하기
때문에 특정 동작에 대한 정보를 담고있는 Type이라고 볼 수 있다.
(Type은 특정 형태를 의미하는 말이며, Instance를 의미하는 건 아니다.)
 
Delegate의 사용법은 위의 Thread 샘플과 같은 방식으로 사용된다.
해당 Delegate Type 과 return 및 파라미터 인자들까지 그 형태가 같아야 된다.
 
Delegate의 또다른 특징은 여러 작업을 동시에 담아둘수도 있으며
동시에 여러번의 작업을 실행 할 수도 있다는 점이다.
 
아래 코드를 보자.
 
 
Code Snippet
  1. class Program
  2. {
  3.     public delegate void Printer();
  4.     static void Main(string[] args)
  5.     {
  6.         Printer prn = new Printer(Prn);
  7.         //Printer prn = Prn; 동일
  8.         prn += Prn2;
  9.         if (prn != null)
  10.             prn();
  11.     }
  12.     static void Prn()
  13.     {
  14.         Console.WriteLine("Printing.......Anywhere");
  15.     }
  16.     static void Prn2()
  17.     {
  18.         Console.WriteLine("Printing2.......Anywhere");
  19.     }
  20. }
EndFragment
 
 
위 결과를 실행하면 당연한 얘기겠지만
Prn(), Prn2()
함수의 내용이 콘솔에 출력된다.
 
물론 -= 연산도 가능하다.
 
본 이야기의 시작점이었던 "왜 쓰이고 어디에 쓰이는지" 에 대한 본론으로 들어가기 전에 조금 더 Deleagate의 내부 동작을 이해하고자 삼천포에 좀더 빠져보도록 하자
 
위 코드를 잠깐 ILDasm 으로 열어보도록 하자.
 
 
모르는 분을 위해 잠깐 언급하자면 IL Dasm은 "IL(중간언어)를 볼수있게 디스어셈"을 해주는 도구로 "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\" 경로 밑에 숨어 살고있다. V7.0A는 상황에 따라 버전명이 다를 수 있다.
 
 
[ILDASM]
 
자세히 볼것 없이 간략히 훑어 본다면
1,2,3 번은 각각 해당 함수에 따른 Delegate 객체를 생성하는 부분이다.
 
위에 언급된 소스대로라면 1,3번만 나와야 맞지만 소스에 동작방식이 동일하다고 주석처리 한 부분을 확인 해 볼수 있게 실제 컴파일시에는 주석을 풀었다.
 
아무튼, 눈여겨 볼 곳은 
 
System.Delegate::Combine(...,...) 과 
(CombineRemove와 짝을 이룬다. 소스에서 "-=" 연산자로 실행 후 확인해보자)
Printer::Invoke() 부분이다.
 
 
즉, 위 소스에서 Delegate Instance 에 함수를 "+=" 연산자를 통해 호출을 하게 되면
Combine메소드가 불려지면서
 
Code Snippet
  1. prn = (Printer) Delegate.Combine(prn, new Printer(Program.Prn2));
EndFragment
 
위와 같이 컴파일러는 재 해석 하게 된다.
 
ILDASM에서도 보이듯이rValue에 대한 Delegate Instance를 내부적으로
생성한 다음 기존의 Delegate와 결합시켜준다. 
 
그 후 호출된 Delegate Instance
 
prn() -> prn.Invoke() -> Prn() -> Prn2()
 
식으로 호출과정을 거치게 된다.
 
컴파일하면 내부적으로 Invoke를 호출해서 하나씩 꺼내와 실행하게 되지만
직접 소스에 prn.Invoke() 라고 쳐도 실행에는 상관이 없다.
 
Delegate에 대한 내부적인 이해를 돕기위한 삼천포는 여기 까지다.!!
 
왜 삼천포로 빠졌는지는 후반 2부에서 언급 될 "Event" 에 대한 설명시
비교를 위해 되새김질 해봐야 하기 때문이므로 이정도면 충분하다.
(충분하지 않다면 당신은 이미.....엘리뜨?!)
 
다시 본론으로 들어와
그렇다면 이런 Delegate를 왜 사용하는 걸까?
그냥 아싸리 Func(); 이런식으로 호출하면 만사 땡 아닌가??
 
Delegate는 위에서 보여진 Combine을 통한 다수의 func에 대한
편리한? 절차적인 호출등의 이점 말고도
 
코드의 유연성을 도와준다는 데에 그 목적이 있다.
즉 코드간의 강한 결합도를 낮춰줄 수 있다는 말이다.
 
예를 들어
A 클래스 안에 항상 Data를 받으면 콘솔로 뿌려주는
Console.Write("블러") 와 같이 코드가 박혀있는 메소드가 있다고 치자.
 
A클래스를 만일 콘솔 Main함수에서 가져다 쓴다면 문제는 없겠지만
콘솔이 아닌 순수한 윈폼이라면(이는 프로젝트 설정에서 Console도 가능하게 한 경우는 제외한다.) 위 함수자체는 쓸모없게 되버릴 것이다.
 
만일 저 출력부를 따로 Delegate 가능하게끔 참조만을 연결해 두었다면 따로 A클래스
쓸대 해당 프로젝트에 따라서 참조로 연결해준 위임자에게 역할만 바꿔주면 될 것이다.
 
이 얼마나 유용성있는가? (예가 좀 억지성이 있겠지만서도..)
 
여기서 드러나는 또 하나의 장점은 메소드 그 자체의 캡슐화(Encapsulating) 기능이다.
호출하는 쪽에서는 그 자체 내부의 메소드 기능이 뭐든간에 그냥 대리자만 불러주면 된다. 이처럼 Delegate는 코드 구조에 따라서 참 유용하게 쓰일 수가 있다.
 
한 가지 빼먹은 점을 더 이야기 하자면 대리자(Delegate)는
BeginInvoke,EndInvoke를 통한 비동기 메소드 호출또한 가능하다.
(MSDN을 통해 테스트 해보도록 하자- http://support.microsoft.com/kb/315582/ko)
 
 
Event Delegate를 한번에 이야기 하려 했으나
주저리 글이 많아지는 바람에 믿거나 말거나 + 헛소리도 많았지만
Delegate에 대한 글은 이정도로 하고 1부를 정리...
 
2부에서 다시 이야기를 시작하도록 하자.
 
 
**
빼먹은 내용이 있어 언급하자면 Delegate InstanceString과 같이 Immutable 하다는 점이다. 즉 위에 ILDASM에서도 보이지만 Combine 자체 기능도 두 개의 각기 다른 Instance를 통해 새로운 Instance를 리턴하는 것이지 기존의 Instance에 대한 변경을 가하는 점이 아니라는 것이다.
**
 
 
Reference : "C# In Depth",
 
=====
 
 


 

귀찮은 분을 위해  Delegate 비동기 호출을 간단히 예제를 통해 확인해 보도록 하자.

 

"BeginInvoke를 호출하게 될 경우 'IAsyncResult' 인터페이스를 리턴하게 되며 이는 EndInvoke시에 다시 사용된다."

 

[Source]

Code Snippet
  1. namespace ConsoleApplication5
  2. {
  3.     class Program
  4.     {
  5.         public delegate int BinaeryOp(int x, int y);
  6.         static public BinaeryOp b;
  7.         static int Add(int x, int y)
  8.         {
  9.             Console.WriteLine("Add() Invoked on thread {0}",
  10.                               System.Threading.Thread.CurrentThread.GetHashCode());
  11.             System.Threading.Thread.Sleep(8000);
  12.             return x + y;
  13.         }
  14.         static void Main(string[] args)
  15.         {
  16.             b = new BinaeryOp(Add);
  17.             IAsyncResult ar = b.BeginInvoke(10, 10, null, null);
  18.             Console.WriteLine("Main Thread {0}",
  19.                              System.Threading.Thread.CurrentThread.GetHashCode());
  20.             //1초마다 해당 Function의 작업이 완료되었는 지를 체크 함
  21.             while (!ar.AsyncWaitHandle.WaitOne(1000, true)) 
  22.             {
  23.                 Console.WriteLine("Doing more work in Main()!");
  24.             }
  25.             int answer = b.EndInvoke(ar);
  26.             Console.WriteLine("10 + 10 is {0}", answer);
  27.         }
  28.     }
  29. }

 


[Result]


'Program > C#' 카테고리의 다른 글

서비스 응용 프로그램 만들기  (0) 2018.01.23
Delegate와 Event의 이해(2)  (0) 2017.12.10
StringBuilder로 문자열 처리를 빠르게  (0) 2017.10.11
Invoke() 이쁘게쓰기!  (0) 2017.10.11
시리얼통신  (1) 2017.10.02