http://blog.naver.com/woom333/60024148905
이 글은 2002년도 11월부터 월간 마이크로소프트웨어지에 4개월 동안 연재한 글이다.
이 게임은 클라이언트로는 플래시를 이용했고, 서버로는 C#을 이용해서 만들었다.
사람들은 보통 플래시를 애니메이션 용도로 많이 생각하는데, 사실 플래시로 소켓 통신도
가능하다. 그래서 포트리스와 같은 네트웍 게임을 제작해 본 것이다.
포트리스에서 기본적으로 되는 것은 다 된다. 채팅도 되고, 탱크 이동, 미사일 각도 조절, 발사,
폭발까지...
처음 이 게임을 기획한 것은 온라인 게임을 따로 프로그램을 설치할 필요 없이 웹에서
바로 할 수 있도록 함이었고, 플래시의 가능성을 널리 알리기 위함이었다.
또한 C#이 가지고 있는 기본적인 비동기 통신을 통해서 대용량 소켓 서버를 구축해 보는
것이 목적이었다.
백문이 불여일견이라고 먼저 실행 동영상을 보자.
--------------------------------------------------------------------------------
[C#과 플래시로 온라인 게임 만들기] ① 델리게이트 이해 한용희 |
연재순서 2 쓰레드 처리 3 게임서버 완성 4 클라이언트 개발 |
올해 3월 닷넷 정식 버전이 발표되면서 C#이 새로운 언어로 떠오르고 있는데, 특히 네트워크 부분에서 기존의 IOCP(IO Completion Port) 기능을 손쉽게 사용할 수 있도록 만들어 놓았다는 점에서 주목할만 하다. |
기존에는 이 기능을 이용하려면 Win sock 2 API를 직접 호출해야 했지만, C#에서는 이 기능이 BCL (Base Class Library) 안에 포함되어 있어 손쉽게 사용할 수 있다. C#에서는 기본적으로 비동기 통신을 하면 자동으로 IOCP를 이용한다. 이는 C# 뿐만 아니라 닷넷의 기본 기능인 것이다. 또한 플래시는 이번에 MX 버전이 출시되면서 많은 기능의 개선이 있었다. 플래시 5부터 XML 소켓을 지원해 지속적으로 연결된 상태에서 네트워크 통신이 가능해졌으며 온라인 게임으로까지 영역을 넓힐 수 있게 됐다.
앞으로 총 4회의 연재를 통하여 온라인 게임 서버로서의 C#의 가능성을 알아보고, 게임 클라이언트로서 플래시의 가능성에 대해 알아볼 것이다. 기존 온라인 게임의 경우 프로그램을 다운받아 플레이해야 했으나 플래시로 온라인 게임을 만들 경우, 스트리밍 방식을 이용하여 별도의 다운로드없이 실시간으로 데이터를 주고받음으로써 즉시 플레이가 가능하다. 초보자도 해당 홈페이지에 접속하기만 하면 바로 플레이할 수 있기 때문에 누구나 쉽게 게임을 시작할 수 있다.
필자는 이러한 플래시와 C#의 특징에 주목하여 그 가능성을 테스트한다는 의미에서 포트리스와 비슷한 게임인 ‘심플 포트리스(Simple Fortress)’를 만들어 보았다. 별도의 다운로드 없이 URL 주소만 입력하면 플레이할 수 있으며, 웹 브라우저 내에서 실행되므로 게임을 하면서도 다른 작업창을 실행할 수 있다는 이점이 있다. 본격적인 설명에 들어가기 전에, 이 게임은 필자가 닷넷과 플래시에 대한 테스트용으로 만든 것으로 상업적으로 사용할 의도가 없으며, 이 게임의 거의 모든 이미지와 사운드 파일은 포트리스 2 공식 홈페이지에서 다운받아 사용한 것임을 미리 밝혀둔다.
심플 포트리스 미리보기
앞으로 우리가 만들 게임이 어떤 게임인지 한 번 보도록 하자. ‘이달의 디스켓’의 압축을 풀면 Server와 Client 두 개의 폴더가 있을 것이다. 이중 Server 폴더에서 FortressServer.exe를 실행하면 서버가 작동한다(이 서버 프로그램은 닷넷 기반 하에서만 작동하기 때문에 최소한 닷넷 프레임워크는 설치되어 있어야 한다). 그 다음 Client 폴더의 fortress.html 파일을 실행시킨다. <화면 1>과 <화면 2>는 서버와 클라이언트의 작동 화면이다.
<화면 1> FortessServer.exe 실행화면
<화면 4> 게임 시작 화면
이때 두 개의 웹 브라우저에서 동시에 배경음악이 나오므로 약간 혼란스러울 수도 있다. 서로 다른 팀을 고른 후, 처음에 들어왔던 사람이 START 버튼을 누르면 게임이 시작된다(<화면 4>). 게임 방법은 <표 1>과 같다
| ||||||||||||
| ||||||||||||
|
먼저 자신의 차례가 되면 자신의 탱크 위에 ‘READY!’라는 글자가 깜빡거린다. 그 상태에서 각도 조정이나 이동을 하면서 조절한 후 스페이스 바를 길게 눌렀다가 떼면 대포가 발사된다(<화면 5>). 서로 번갈아 가면서 대포를 발사하는 방식으로 게임을 쉽게 하기 위하여 폭발의 파편만 닿아도 생명치를 줄게 해 놓았다. 채팅도 지원하므로 대화를 입력해도 된다(<화면 6>). 이제 어떤 게임인지 살펴봤으니 본격적으로 게임 제작에 착수해 보자. 먼저 서버부터 만들어 볼 것이다.
<화면 5> 대포를 발사한 장면
<화면 6> 대화를 나누는 장면
비동기 호출의 기본은 델리게이트
서버 제작에서 중요한 점은 다수의 사용자를 처리해야 하는 데 있다. 한 명이 아니라 여러 명이 동시에 접속하므로 그들의 요구를 동시에 처리해 줘야만 한다. 그런데 일반적으로 프로그래밍하다 보면 한 명을 처리하기 위해 그 대답을 기다리다가 다른 사람의 요구를 못 들어주게 된다.
즉 블럭이 돼 버려서 다수의 사용자를 처리할 수 없게 된다. 이 때의 해결책이 바로 쓰레드이다. 닷넷에서는 이러한 쓰레드를 이용하여 비동기 호출을 지원하는데, 이를 이용하여 많은 사용자들의 요구를 처리할 수 있다. 이는 함수를 호출할 때 동기적으로 그 함수의 호출이 끝날 때까지 기다리는 것이 아니라 함수를 호출하면 그 함수는 새로운 쓰레드 안에서 돌아가고 호출자 또한 기존 쓰레드 내에서 돌아가므로 동시에 일을 처리할 수 있게 된다.
<그림 1> 동기 호출과 비동기 호출
<그림 1>은 소켓의 Accept문을 예로 들어 동기 호출과 비동기 호출의 차이점을 설명한 그림이다. 이 비동기 호출의 핵심 개념에는 바로 델리게이트(delegate)라는 것이 자리잡고 있다. 그러므로 델리게이트의 본질부터 파악하는 것이 비동기 호출의 원리를 이해하는 방법일 것이다.
C# 세계의 브로커, 델리게이트
C#을 배우는 사람들에게 있어 델리게이트는 생소한 개념이 아닐 수 없다. 델리게이트는 C 언어의 함수 포인터에서부터 유래되었다. C 언어에서는 함수 포인터를 잘 안 썼으므로 생소할 수도 있다. 먼저 델리게이트의 사전적인 의미를 살펴보면 ‘대리자’ 또는 ‘위임형’ 등으로 정의하고 있다. 델리게이트라는 것이 어떤 함수를 대신해서 호출되기 때문에 그렇게 이름을 붙인 듯하다. 이해를 돕기 위해 다음과 같은 기상청 시나리오를 살펴보자.
기상청에서는 기상정보를 수집해 그 정보를 필요로 하는 곳에 전달한다. 그런데 누가 언제 그러한 정보를 필요로 할지 미리 알수 없기 때문에 그런 보고 시스템을 미리 구축해 놓을 수 없었다. 그래서 대신 기상 정보 브로커를 고용해 그에게 정보를 주면, 그가 자신에게 연결된 기상 정보를 필요로 하는 사람들에게 그 정보를 주기로 했다. 그렇게 해서 보고 시스템을 완성하게 됐다. 이에 신문사가 제일 처음 그 정보를 달라고 브로커에게 요청을 했다. 브로커는 그 요청을 받아들이고 기상청으로부터 기상 정보를 받는대로 신문사에게 전해 주기로 했다.
| ||||
|
<리스트 1>을 보면 한 가지 궁금증이 떠오를 수도 있다. 마지막 줄을 다시 보자.
new Information(NewspaperCompany.Publish);
여기서 왜 new라는 키워드를 썼는지 궁금증이 일어날 것이다. new라는 것은 새로운 Object를 할당할 때에만 쓰는 키워드인데, 여기서 사용했다는 것은 마치 클래스를 할당하는 것과 비슷하다고 생각할 수 있다. 만약 그렇게 생각했다면 맞다. 델리게이트라는 것이 바로 클래스이기 때문이다. 델리게이트를 ‘위임[형]’이라고 번역하듯이 델리게이트는 클래스 타입이다. 그러면 클래스 바디는 어디에 있는 걸까? 클래스라면 다음과 같이 되어 있어야 한다.
{
}
하지만 클래스의 정의가 다음과 같이 한 줄로 되어 있다.
도대체 바디는 어디에 있는가? 사실 이 한 줄에는 바디를 포함하고 있다(<그림 3>). 즉 리턴형과 인자형에 대한 정보가 클래스 바디가 되는 것이다.
<화면 7> MeteorologicalSystem.exe를 Disassemble한 화면
<화면 8> 아이콘 도움말
이를 통해서 보면 Information은 클래스라는 것과 .ctor, BeginInvoke, EndInvoke, Invoke라는 네 개의 메쏘드를 가지고 있음을 알 수 있다. 또한 Information은 System.Muticastdele gate에서 상속받았다는 정보까지 갖고 있다. 여기에 나오는 4개의 메쏘드중 .ctor은 생성자를 의미한다. BeginInvoke와 EndInvoke는 비동기 호출에 쓰이며, Invoke는 동기 호출에 쓰이는 메쏘드이다. 이들에 대한 코드는 <리스트 2>와 같다.
| |||
|
| ||||||||||||||||
| ||||||||||||||||
|
<리스트 2>를 보면 한 가지 이상한 점을 발견할 수 있을 것이다. 바로 Information의 생성자인데, 우리는 <리스트 1>에서 생성자로 NespaperCompany.Publish를 넘겨줬다. 그런데 <리스트 2>를 보면 생성자는 두 개의 인자가 필요하다. 분명 에러를 발생해야 하는데 잘 되는 것을 보면 이상이 없는 것이다. 여기서 컴파일러는 원본 소스를 컴파일할 때, 앞의 생성자에 맞도록 파싱을 해주기 때문에 에러가 안 나는 것이다.
앞의 두 인자중 target은 메쏘드가 있는 오브젝트를 가리키는데 만약 메쏘드가 static이면 null 값을 넘겨준다. methodPtr은 callback 메쏘드를 가리키는 CLR 내부에서 쓰이는 레퍼런스 값이다. 이들 생성자에서 받은 두 개의 값을 Information 클래스는 따로 Private 필드에 저장해 두는데 그 Private 필드는 <표 2>와 같다.
이중 _prev 값은 나중에 Muticatedelegate에서 설명할 것이다. 그럼 이제 우리가 생성자에게 넘겨준 그 값들을 직접 눈으로 확인해 보자. <리스트 1>에서 main 메쏘드에 다음과 같은 코드를 추가하자.
if ( broker.Target == null )
{
Console.WriteLine(“null”);
}
else
{
Console.WriteLine(broker.Target);
}
Console.WriteLine(broker.Method);
이를 실행하면 다음과 같은 결과를 볼 수 있다.
=== 첫번째 예제 ===
[신문사 출판] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
=== 두번째 예제 ===
null
Void Publish(Int32, Int32, System.String)
우리가 넘겨준 메쏘드가 Static이기 때문에 Target에는 null 값이 들어갔고, 메쏘드에는 대리자에 등록된 메쏘드의 형식이 나왔다. 만약 여기에서 instance 메쏘드를 넘겨주면 어떤 값이 나올까? 앞에서 Kill 메쏘드에서 static을 빼고, NewspaperCompany 클래스를 새로 생성해서 실행해 보면 다음과 같은 결과가 나온다. 즉 메쏘드의 Object를 넘겨주는 것이다.
[신문사 출판] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
=== 두번째 예제 ===
MeteorologicalSystem.NewspaperCompany
Void Publish(Int32, Int32, System.String)
이제 생성자에 대한 비밀은 풀었으나 아직 Information 클래스의 메쏘드에 대한 비밀이 남아 있다. <리스트 2>에서 Invoke 메쏘드가 있는데 이것이 실제 실행하는 메쏘드이다. 그런데 우리는 그 메쏘드를 호출한 적이 없다. 그러면 컴파일러가 알아서 호출해 주는 것일까? 그렇게 생각했다면 정답이다. 우리는 <리스트 1>에서 다음과 같이 호출했다.
broker(25,60,”구름 없음”);
컴파일러는 이 코드를 보고 다음과 같이 번역한다.
broker.Invoke(25,60,”구름 없음”);
그런데 정말 이렇게 번역하는 것일까? 이것 역시 ILDASM을 이용해서 확인해 보자. <화면 9>를 보면 컴파일러가 만들어 준 Invoke 메쏘드를 볼 수 있을 것이다.
<화면 9> Invoke가 호출된 부분
<화면 10> += 연산자가 나타내는 메쏘드
너에게 임무를 추가한다!
이제 델리게이트에 대해 어느 정도 비밀을 풀었다. 그런데 여기서 한 가지 의문이 남아 있다. <표 2>를 보면 _prev라는 필드가 있는데 이 필드의 용도가 무엇이냐 하는 것이다. 이를 위해 다음과 같은 시나리오를 보자.
어느 날 방송사에서도 그 기상 정보를 달라는 요청이 들어왔다. 이미 기상청에서는 브로커에게 그 일을 일임했으므로 방송사는 브로커와 거래를 해 등록함으로써 브로커를 통해 기상청의 정보를 제공받게 된다. 이를 코드로 나타내면 다음과 같다. 먼저 Broadcasting Company 클래스를 다음과 같이 새로 만든다.
// 방송사
class BroadcastingCompany
{
// 방송사에서 방송 보도
public static void Broadcast( int temparature,
int humidity, string nephanalysis)
{
Console.WriteLine(“[방송 보도] 온도:{0}, 습도:{1}, 구름분포:{2}”,
temparature, humidity, nephanalysis);
}
}
그 다음 브로커에 다음과 같이 추가하면 된다.
broker += new Information(BroadcastingCompany.Broadcast);
결과는 다음과 같다.
=== 세번째 예제 ===
[신문사 출판] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
[방송 보도] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
여기서는 단순히 += 연산자를 이용했다. C#에서는 연산자 오버로딩을 지원하기 때문에 += 연산자는 실질적으로 메쏘드를 호출하는 것이다. 그러면 그 메쏘드가 무엇인지 ILDASM을 통해 확인해 보자. <화면 10>을 보면 Combine이라는 메쏘드가 호출됨을 볼 수 있다.
기본적으로 델리게이트 타입은 Muticastdelegate를 상속받으므로 하나의 callback 메쏘드가 아닌 다수의 callback 메쏘드를 가질 수 있다. 앞의 시나리오에서 보듯이 어떤 사건에 의해 다수가 그 영향을 받는 경우가 있기 때문에 이러한 기능을 지원하는 것이다. 델리게이트 내부적으로는 이것을 linked-list로 유지를 한다. linked-list로 유지하기 때문에 앞의 링크를 가리키는 _prev 필드가 필요한 것이다. 이를 그림으로 나타내면 <그림 4>와 같다.
<그림 4> 브로커의 linked-list
이 그림을 보면 한 가지 의문점이 들 것이다. 우선 브로커가 처음에 등록했던 신문사를 가리키는 것이 아니고 나중에 등록한 방송사를 가리킨다는 것과 일반적인 linked-list에서는 _next 필드로 다음 오브젝트를 가리키는 데 비해 여기는 _prev필드를 써서 앞의 오브젝트를 가리킨다는 것이다. 그 이유는 리턴 값 때문에 그렇다. 만약 callback 메쏘드에 리턴 값이 있다면 어떻게 할 것인가? 예를 들어 다음과 같은 경우이다.
string nephanalysis);
이와 같이 리턴 값이 있는 경우 브로커에 등록된 callback 메쏘드가 한 개라면 별 문제가 없지만, 여러 개라면 어떻게 할까? 먼저 C#에서는 여러 개의 callback 메쏘드중 단 한개의 리턴 값만 받아 올 수 있다. 그 여러 개의 리턴 값을 다 받아오려면 다른 방법을 취해야 하는데 그 방법은 나중에 소개할 것이다. 일단 일반적인 상황에서는 리턴 값을 하나만 취한다.
그러면 어떤 리턴 값을 취할 것인가? 상식적으로 생각해 보면 가장 최근에 호출된 callback 메쏘드의 리턴 값이 가장 가치있다고 생각될 것이다. 그래서 가장 나중에 호출된 리턴 값이 필요하기 때문에 _next 필드를 안 쓰고 위로 거슬러 올라가서 호출하는 것이다. _next를 쓴 경우와 _prev를 쓴 경우를 그림으로 비교해 보자. 예를 들어 f1, f2, f3를 브로커에 등록했다고 하면 <그림 5>와 같은 호출 과정을 볼 수 있다.
class Information : Multicastdelegate
{
public int virtual Invoke(int temparature, int humidity,
string nephanalysis)
{
// 앞으로 거슬러 올라간다.
if ( _prev != null ) _prev.Invoke(temparature,
humidity,
nephanalysis);
// 결국 맨 나중에 호출된 callback 메쏘드의 리턴 값이 리턴된다.
return _target.methodPtr ( temparature, humidity,
nephanalysis);
}
}
그럼 이제 _prev를 살펴봤으니, Combine 메쏘드가 내부적으로 어떻게 이들 연결을 만드는지 보자. <리스트 3>은 델리게이트 클래스의 Combine 메쏘드들이다. Combine 메쏘드는 두 개의 델리게이트를 인자로 받는데, 먼저 delegate는 한 번 생성되면 immutable하기 때문에 _prev 필드를 마음대로 변경할 수가 없다. 그래서 Combine을 할 때에는 second와 같은 새로운 델리게이트 오브젝트를 생성하고, 이때 _prev 필드 값을 first로 설정해 준다.
<리스트 3> 델리게이트의 메쏘드
class System.delegate
{
// first와 second를 연결한 후에 second를 리턴한다.
public static delegate Combine(delegate first, delegate second);
// 배열에 의한 델리게이트를 연결시켜 준다.
public static delegate Combine(delegate[] delegateArray);
// 델리게이트를 chain에서 제거
public static Remove(delegate source, delegate value);
}
그렇다면 이를 이용해서 Combine 메쏘드가 정말 새로운 오브젝트를 생성해서 리턴하는지 다음과 같은 코드를 보자. 다음 결과를 보면 False가 나온다. Combine 메쏘드가 새로운 델리게이트를 새로 생성해서 리턴하기 때문이다.
Information broker1 =
new Information(NewspaperCompany.Publish);
Information broker2 = (Information) delegate.Combine(broker1,
broker1);
Console.WriteLine( (object) broker1 == (object) broker2 );
델리게이트에 Combine시키는 방법이 있으니 Remove시키는 방법도 있을 것이다. 다음과 같은 경우를 보자.
MeteorologicalOffice.Report(broker);
앞에서 브로커에게 신문사와 방송사를 다 등록시켰는데 이번에는 신문사를 제거해 봤다. 이 -= 연산자 또한 실제로는 Remove 메쏘드를 나타낸다. 이는 <리스트 3>에서 보듯이 두 개의 인자를 취하는데, 첫 번째는 linked-list를 이루고 있는 델리게이트의 헤드를 가리키며, 두 번째는 삭제할 델리게이트를 가리킨다. 그런데 왜 지우는데 오브젝트를 새로 생성할까? linked-list에서 원하는 것을 찾아서 지워야 하는데, 이를 비교하는 방법에 그 원인이 있다.
우리가 정확히 찾고자 하는 것은 NewspaperCompany. Publish로 이를 비교해야만 하는 것이다. 그런데 앞서서 델리게이트 클래스에서 생성자로 넘겨주는 것으로 _target과 _method Ptr이 있었다. 즉 instance/static이냐 하는 것과, 리턴 값과 인자형에 따라 클래스를 구분할 수 있는 것이다. 그렇기 때문에 델리게이트에서는 동등 비교를 하는 데 있어 _target과 _methodPtr을 이용한다. 다을 실행하면 TRUE를 리턴하는 것을 볼 수 있다.
Information broker3 =
new Information(NewspaperCompany.Publish);
Information broker4 =
new Information(NewspaperCompany.Publish);
Console.WriteLine(broker3.Equals(broker4));
이제 델리게이트에 다른 델리게이트를 쉽게 추가/삭제할 수 있게 됐다. 그러나 앞서 얘기했듯이 델리게이트의 linked-list 호출 구조는 한 가지 단점을 지니고 있다. 중간의 리턴 값들을 무시한다는 것이다. 게다가 만에 하나 리스트들 중에서 exception이 일어나든가 블러킹(blocking)되기라도 하면, 뒤에 딸려 있는 리스트들은 모두 호출되지 못하고 멈춰버린다.
이럴 때에는 알아서 호출하게 놔두지 말고 사용자가 직접 하나하나 체크해 가면서 호출하면 된다. C#에서는 이와 같은 문제를 해결하기 위해 GetInvoca tionList()라는 함수를 제공하고 있다. 이를 이용하면 linked-list의 각 구성원을 똑같이 복사한 배열로 리턴받을 수 있다. 단 이때 _prev 필드는 필요없기 때문에 null로 셋팅이 된다.
delegate[] arraydelegates = broker.GetInvocationList();
foreach(Information agent in arraydelegates)
{
Console.WriteLine(agent.Method);
}
이벤트 핸들러로 임명합니다~
이제 마지막으로 이벤트에 대해 알아 보자. 이벤트는 한 오브젝트에서 어떤 일이 일어나서 그 일을 다른 오브젝트에게 알려줄 때 이용한다. 이는 델리게이트와도 많이 유사한데, 실제로도 델리게이트를 이용하므로 이벤트는 델리게이트의 특별한 용도라고 생각하면 된다. 예를 들어 앞서 정의한 기상정보 시스템을 이벤트로 만들어 보자. 이때 이벤트라는 의미에 좀더 충실하기 위해 기상 특보를 기상청에서 발령한다고 가상해 보았다. 이때 기상청에서는 자신의 이벤트에 등록된 신문사에게 통지해 준다. <리스트 4>를 보자.
------------------------------------박스 시작--------------------------------------------
델리게이트의 변천사
델리게이트의 변천사
처음에 닷넷 베타판이 나왔을 때 델리게이트는 System.delegate와 System.MulticastDelegate의 두 종류로 분리돼 있었다. 델리게이트 선언시 리턴값이 void라면 System.delegate에서 상속을 받는 클래스로 정의되었으며 void가 아닌 리턴값이 있는 거라면 System.MulticastDelegate에서 상속받는 클래스로 정의되었다. 즉 리턴값이 없으면 Single 델리게이트이지만, 있으면 linked-list를 이루는 Muticasting 델리게이트로 정의된 것이다.
그런데 이러한 구분은 사용자들에게 혼란을 증가시켰다. 단순히 리턴값에 의해 델리게이트의 용도를 구분하는 것 자체에 무리가 있었던 것이다. 그래서 베타판 이후에는 이 두개의 델리게이트를 하나로 통합했다. System.MulticastDelegete는 System.delegate를 상속받음으로써 결국 모든 델리게이트는 Multicastdelegate가 된 것이다.
------------------------------------박스 끝----------------------------------------------
| |||
|
이벤트 이용에는 몇 가지 관례가 있다. 먼저 일반적인 델리게이트에서는 인자에 제한이 없지만 이벤트에서는 두 개의 인자를 사용한다. 그 두 개는 보내는 이가 누구인지 나타내는 object형과 System.EventArgs에서 상속받은 클래스를 인자로 받는 것이 있다. 먼저 받는 사람이 여러 사람의 이벤트에 등록해 두면 누가 보냈는지 알 수 없으므로 누가 보냈는지 알기 위해 첫 번째 인자로 object형 인자를 받는다.
예를 들면 신문사는 정보를 기상청으로부터 들을 수도 있지만 소식이 들어오는 경로는 여러 군데일 것이다. 누가 그 정보를 보냈는지 알아야 할 때가 있기 때문에 이런 방법을 사용하는 것이다. 두 번째 인자는 EventArgs를 상속받은 클래스인데 이를 사용면 좀더 깔끔하게 인자 관리를 할 수 있다(그러나 여기서는 편의를 위해서 단순하게 하나의 인자만 썼다).
이벤트에서는 이름을 정하는 데 있어서도 몇 가지 관례가 있다. 먼저 System.EventArgs를 상속받는 클래스는 그 이름 끝에 EventArgs를 붙여준다. 또한 델리게이트를 선언시에도 이름 뒤에 EventHandler를 붙여준다. 마지막으로 이벤트를 발생시키는 메쏘드는 이름 앞에 On을 붙여준다. <리스트 4>를 보면 그냥 이벤트 키워드없이 관례만 따라주면 이벤트가 되지 않느냐고 물을 수 있는데 사실 이벤트는 내부적으로 또 다른 일을 하고 있다. 무슨 일을 내부적으로 꾸미는지 알기 위해 ILDASM으로 확인해 보자.
<화면 11> 이벤트가 들어간 클래스
<화면 11>을 보면 생성하지 않았던 두 개의 함수가 추가되어 있는 것을 볼 수 있다. 게다가 우리는 SpecialReport를 public으로 선언을 했는데 private으로 되어 있다. 이를 가상 코드로 나타내 보자.
private SpecialReportEventHandler SpecialReport = null;
// 이벤트 등록 메쏘드
[MethodImpAttribute(MethodImplOption.Synchronized)]
public void add_SpecialReport(SpecialReportEventHandler handler)
{
SpecialReport = (SpecialReportEventHandler)
delegate.Combine
(SpecialReport, handler);
}
// 이벤트 등록해제 메쏘드
[MethodImpAttribute(MethodImplOption.Synchronized)]
public void remove_SpecialReport(SpecialReportEventHandler handler)
{
SpecialReport = (SpecialReportEventHandler)
delegate.Remove
(SpecialReport, handler);
}
먼저 우리가 public으로 선언한 SpecialReport가 private으로 되어 있으면서 null로 초기화되어 있다. 이는 이벤트라는 것을 외부에서 함부로 접근하지 못하게 막기 위함이다. 예를 들어 태풍이 오는 그런 급박한 상황에서만 이벤트가 발생해야 하는데 이를 public으로 둘 경우 외부에서 마음대로 통제하거나, 바람이 불어도 기상 특보를 발령하는 우를 범할 수가 있기 때문이다. 그래서 이벤트는 그 이벤트를 소유한 클래스 내에서만 발생시킬 수 있게 하기 위해 private 필드로 두는 것이다.
또한 add_*와 remove_*라는 두개의 메쏘드가 추가됐는데, 이는 메쏘드명에서 알 수 있듯이 이벤트에 등록자(listener)들을 등록/해제하는 역할을 한다. +=와 -=연산자를 쓰면 add_*나 remove_*로 컴파일러가 바꿔 준다. Combine이나 Remove를 쓴 경우와 다르지 않게 보일 수 있으나, 자세히 보면 메쏘드 위에 애트리뷰트가 있다.
이는 메쏘드를 동기화시켜서 쓰레드에 대한 안정성을 보장해 준다. 예를 들면 두 개의 리스너가 동시에 이벤트에 등록/해제해도 linked-list가 깨지지 않고 올바로 유지된다(<화면 12>). 결론적으로 이벤트는 결국 델리게이트의 특별한 케이스인데 보다 보안과 안정성에 중점을 둔 케이스라고 할 수 있다.
<화면 12> 동기화된 메쏘드
다음 글에서는 비동기 프로그래밍을
이번 글에서는 서버 구축하는 데 있어 필수인 비동기 소켓 프로그래밍에 앞서, 비동기 프로그래밍의 기본인 델리게이트에 대하 알아봤고, 델리게이트의 특별한 케이스인 이벤트에 대해서도 알아봤다. 다음 연재에서는 비동기 프로그래밍의 원리와 사용 방법에 대해 알아 볼 것이다.
참고자료
* An Introduction to delegates - Jeffrey Richter MSDN Magazine - April 2001
* delegates, Part 2 - Jeffrey Richter MSDN Magazine - June 2001
* Implementation of Events with delegates - Jeffrey Richter MSDN Magazine - August 2001
'[ Program ] > C#' 카테고리의 다른 글
C# 정리 내용 (ADO.NET) (0) | 2012.09.18 |
---|---|
비쥬얼스튜디오에서 디자이너를 제대로 표시하지 못할때 (0) | 2012.09.18 |
DataGridView 활용 Tip (0) | 2012.09.18 |
BackgroundWorker 사용 (0) | 2012.09.18 |
c#어플리에서 HttpWebRequest 와 HttpWebResponse를 이용하여 post,get 처리하기 (0) | 2012.09.18 |
댓글