Silverlight를 설치하려면 여기를 클릭합니다.*
Korea 대한민국변경|Microsoft 전체 사이트
MSDN
|개발자 센터
MSDN Home   MSDN Home

C++ At Work
이벤트 프로그래밍


이 문서에 사용된 코드 다운로드: CAtWork0602.exe (175KB)




Paul DiLascia (영문)

Q 표준 C++에서도, .NET 프레임웍에서 이벤트 핸들링에 사용하는 delegate와 연산자+=와 비슷한 것을 이용해서 이벤트 핸들링 할 수 있는 방법이 있나요?

여러 독자들로 부터

물론 있습니다! Visual C++® .NET 은 매니지드 클래스(managed class)에서 _event 키워드를 이용하여 이벤트를 핸들링하는 것처럼, 네이티브 C++(native C++)에서도 동일한 방식으로 이벤트를 구현할 수 있게 해주는 통합된 이벤트 모델이라고 불리는 메카니즘을 가지고 있습니다. 그러나, 이러한 C++에서의 이벤트들은 마이크로 소프트에서 앞으로 수정할 계획이 없는, 기술적으로 불명확한 문제가 있고, 그래서 마이크로 소프트에서는 공식적으로는 순수 C++에서는 통합된 이벤트 모델을 쓰지 말라고 권장하고 있습니다. 그러나, 이 말이 앞으로 C++ 프로그래머가 이벤트 없이 살아야 한다는 이야기는 단연코 아닙니다. 이러한 방법을 통하지 않고도, 원하는 바를 이룰 수 있는 방법은 많이 있습니다. 여기에서는 저는 아주 약간의 작업으로 근사한 이벤트 시스템을 어떻게 구현하는지 보여 드리겠습니다.

그 작업을 하기 앞서서, 일반적인 이벤트와 이벤트 프로그래밍에 대해서 확인해 보고 가도록 하겠습니다. 이 확인 작업은 아주 중요합니다. 여러분은 현재와 같은 프로그래밍 환경에서 이벤트가 무엇인지 그리고 언제 쓰는지에 대한 확실한 이해가 없이는 프로그래밍을 제대로 하시기가 아주 어렵기 때문이죠.

성공적인 프로그래밍은 모두 어떻게 복잡성을 관리하는 것에 대한 것입니다. 아주 옛날에 함수가 서브루틴(subroutine)이라고 불리우던 시절에는, 이러한 복잡성을 관리하는 주된 방식중에 하나가 바로 탑다운 방식의 프로그래밍이었습니다. 여러분이 "우주를 모델링해라"같은 높은 수준의 목표로부터 시작해서, 이 것을 작은 단위의 작업, 예를 들어서 "은하계를 모델링해라" 혹은 "태양계를 모델링해라", 등과 같은 작은 단위로 나누어서, 단일 함수 안에서 구현할 수 있을 정도의 작은 일이 될 때까지 분할하는 방식입니다. 이러한 탑다운 방식의 프로그래밍은, 절차적인 작업을 구현하는데 아직도 유용합니다. 하지만, 비결정적인 순서(nondeterministic order)로 발생하는 실제 세계의 이벤트에 대응하기에는 잘 맞지 않습니다.

이러한 하향식 방식의 모델에서는, 흔히 제일 상위의 컴포넌트들은 낮은 수준에 있는 컴포넌트들을 DoThis() 혹은 DoThat() 같은 함수를 호출해서 명령을 내립니다. 그러다보니, 이러한 낮은 수준의 컴포넌트들도 반응을 전달할 필요가 생기게 되었습니다. Windows에서 타원 혹은 사각형을 그리기 위해서 Rectangle 혹은 Ellipse 같은 함수를 호출하지만, 결과적으로는 Windows는 요청한 Drawing 작업을 위해서, 여러분의 프로그램을 호출할 필요가 있습니다. 그러나, 코딩 시에는 프로그램이 아직 존재하지도 않는 상태이기 때문에, Windows는 어떤 함수를 호출할 지를 알 수 없습니다. 여기서부터 이벤트 개념이 필요하게 되었습니다.

그림 1 (Top-Down) vs. 상향식(Bottom-Up)
그림 1 (Top-Down) vs. 상향식(Bottom-Up)

모든 C 혹은 MFC, .NET 프레임웍 를 이용해서 만든 윈도우 프로그램의 중심에는, WM_PAINT, WM_SETFOCUS, WM_ACTIVATE 등과 같은 메세지를 처리해 주는 윈도우 프로시져가 있습니다. 여러분이 만든 프로그램이 순수 API가 아닌 MFC, 혹은 .NET을 이용하여 작성되었다면, MFC, .NET 프레임웍의 각각의 클래스 레이어들이 이 윈도우 프로시져를 처리하고 다시 윈도우에게 이것을 넘겨 줍니다. 윈도우를 다시 그려야 할 때, 포커스가 바뀌었을 때, 프로그램이 활성화 되었을 때, 윈도우 운영체제는 여러분의 프로시져를 적절한 메세지 코드로 호출합니다. 여기서의 메시지가 바로 이벤트입니다. 그리고 윈도우 프로시져가 바로 이벤트 핸들러라고 할 수 있습니다.

만약 절차적 프로그래밍이 하향식(top-down) 방식이라면, 이벤트 프로그래밍은 상향식(bottom-up)이라고 할 수 있습니다. 일반적인 소프트웨어 시스템에서는, 높은 수준의 컴포넌트에서 낮은 수준의 컴포넌트를 호출하는 방식이라면, 이벤트는 반대 방향으로 침투합니다. 그림 1은 이러한 유형을 보여주고 있습니다. 물론, 우리가 살고 있는 실제 세계에서는 이렇게 산뜻하게 위계 조직을 갖추고 있지 않습니다. 많은 소프트웨어 시스템은 오히려 그림 2와 가깝습니다.

그림 2 상향식과 하향식이 섞여있는 모델
그림 2 상향식과 하향식이 섞여있는 모델

다시 한 번, 정확하게 이벤트가 무엇인가를 다시 말씀드리면, 본질적으로는 이벤트는 콜백 함수입니다. 컴파일 타임에 이름이 알려져 있는 함수를 호출하는 대신, 컴포넌트들은 프로그램 실행시 함수를 호출합니다. 윈도우 운영체제에서는 이것은 윈도우 프로시져입니다. .NET 프렘익웍에서는 delegate의 개념과 동일합니다. 용어가 무엇인든지, 이벤트는 소프트웨어 모듈에게 무슨 함수를 호출할지 이름을 모르는 상태에서, 다른 함수를 호출할 수 있게 해줍니다. 이러한 콜백함수를 이벤트 핸들러라고 부릅니다. 이벤트를 발생시키다는 의미는 본질적은 이벤트 핸들러 함수를 부르는 것과 동일한 의미입니다. 이런 이벤트 핸들러 호출을 가능하게 위해서, 이벤트를 받는 모듈에서는 처음에 이벤트 핸들러의 포인터를 전달해 주는 등록 과정을 거칩니다.

이벤트가 흔히 쓰이는 몇 가지 흔한 예를 들어 보겠습니다.

클라이언트에게 실제로 발생하는 사건:  예를 들어서 사용자가 키를 눌렀다, 시계가 열 두시를 가리켰다, 혹은 팬(fan)이 실패했다, CPU가 불타고 있다 등등, 에 대한 이벤트를 알려 줍니다.

긴 작업에 대한 진척 사항을 보고 하기 위해서:  파일을 복사하거나 아주 용량이 많은 데이타 베이스를 검색한다든지 할 때 이러한 모듈은 주기적으로 이벤트를 발생시켜 몇 개의 파일이 복사되었는지 혹은 얼마나 많은 레코드가 검새되었는지 알려 줍니다.

뭔가 중요하고, 관심의 대상이 되는 일이 발생했을 때:  예를 들어서 여러분의 프로그램에서 IWebBrowser2 인터페이스를 호스트해서 프로그램을 만들 때, 브라우져는 새로운 페이지을 열기 전 그리고 열고 난 후, 혹은 새로운 윈도우를 생성시 등등의 일에 이벤트를 발생 시킵니다.

응용 프로그램 고유의 알고리즘을 호출할 때:  C 런타임 라이브러리의 qsort 함수는 객체들의 배열을 정렬합니다. 그러나, 여러분은 객체의 비교를 위해서는 반드시 비교하는 함수를 제공해야 하며, 이러한 방식을 많은 STL 컨테이너들도 똑같이 이용합니다. 많은 프로그래머들은 qsort 콜백함수를 이벤트라고 부르지 않을테지만, 이것을 이벤트라고 부르지 않을 이유는 없습니다. 본질적으로 이 방식은 "이것과 저것을 비교해 줘!" 하는 이벤트를 qsort함수에서 발생시키는 것과 동일하기 때문입니다.

어떤 독자들은 가끔 저에게 "예외와 이벤트의 차이는 무엇인가요?"라고 물어 봅니다. 주된 차이점은 예외는 일어나지 않았어야 하는 상황을 반영한다는 점입니다. 예를 들어, 여러분의 프로그램의 메모리가 고갈되거나 0으로 나누는 divide-by-zero를 만났을 때를 가정합니다. 이런 비정상적인 상황이 일어나지 않기를 원하시겠지만, 그래도 여러분은 이런 상황에 대비해야 합니다. 반면에 이벤트는 충분히 일어날 수 있고, 매일 발생하는 정상적인 동작의 일부입니다. 사용자가 마우스를 움직이거나 키를 누르는 것과 같은 일들, 혹은 브라우져가 새로운 페이지를 불러 오는 일들과 같은 일들을 예로 들 수 있습니다. 제어 측면에서 보자면 이벤트는 함수 호출이지만, 예외는 문제가 발생한 객체들을 파괴하면서, 제어권를 원래의 호출했던 함수 측으로 넘기면서 스텍을 건너 뛰는 메모리 번지의 이동입니다.

이벤트에 대한 흔한 오해 중에 하나는 이벤트에 비동기성을 기대하는 것입니다. 이벤트가 사용자에게 비동기적으로 발생하는 사용자 입력과 다른 행동을 처리하는데 사용되지만, 이벤트 자체는 동기화적인 형태를 가지고 있습니다. 이벤트를 발생시키는 것은 이벤트 핸들러를 호출하는 것과 같기 때문이죠. 의사코드(Pseudo code)에서는, 이벤트 핸들러를 호출하는 것은 아래의 코드처럼 보일 것 입니다:

// 이벤트의 호출
for (/* 각각의 이벤트에 등록된 객체에 대해서 */) {
  obj->FooHandler(/* args */);
}

제어권이 즉각 이벤트 핸들러로 넘어가고, 이벤트 핸들러의 동작이 끝날 때 까지 제어권이 반환되지 않습니다. 어떤 시스템에서는 이벤트를 비동기적으로 처리할 수 있는 방법을 제공해 주기도 합니다. 예를 들어, Windows는 여러분께 SendMessage 대신 PostMessage를 사용할 수 있게 해줍니다. PostMessage에서는 제어권이 즉각 반환되고, 이벤트 핸들링은 나중에 처리됩니다. 그러나 .NET 프레임웍의 이벤트와 여기서 제가 이야기하는 이벤트는 여러분이 발생시키면 즉각 처리됩니다. 물론, 여러분은 언제든지 다른 쓰레드에서 이벤트를 발생시키거나, 혹은 이벤트 핸들링을 쓰레드 풀에서 처리하기 위해서 비동기 delegate 호출을 할 수 있습니다.

윈도우 프로시져와 타입이 고정된 WPARAM과 LPARAM만을 이용하는 Windows의 이벤트 방식은 현대 프로그래밍 기준에 비해서 상당히 원시적인 방법입니다. 비록 모든 윈도우 프로그램이 이와 동일한 메카니즘을 사용하고, 어떤 프로그래머는 단지 이벤트 전달을 위해서 눈에 안보이는 윈도우를 생성하는 것에 비해서 말입니다. 그러나 윈도우 프로시져는 윈도우당 오직 하나의 윈도우 프로시져만을 허용하기 때문에, 진정한 이벤트 메카니즘은 아닙니다. 비록 여러 개의 윈도우 프로시져가 서브 클래싱에 의하여 서로 링크되어 있을 수 있지만, 진정한 이벤트 시스템에서는 하나 이상의 받는 측이 똑같은 이벤트에 대해서 등록할 수 있어야 합니다. 클래스의 각 계층에 상관없이 말입니다.

.NET 프레임웍에는, 어떤 객체들도 이벤트를 정리할 수 있고, 여러 개의 객체가 이 이벤트를 받을 수 있습니다. . NET에서는 이벤트는 delegate를 사용하여 동작하고, 이것은 Framework에서 콜백을 부르는 용어입니다. 가장 중요한 점은 delegate는 type-safe합니다. 더 이상 void* 혹은 WPARMA/LPARAM을 사용하지 않아도 됩니다.

매니지된 확장을 이용하여 이벤트를 정의하기 위해서는 __event 키워드를 사용하면 됩니다. 예를 들어서, Windows::Forms 네임스페이스 하부의 Button 클래스는 아래와 같은 클릭 이벤트를 가지고 있습니다:

// 버튼 클래스에서
public:
  __event EventHandler* Click;
이벤트 핸들러는 객체(보낸이-Object형)과 EventArgs 형을 받는 delegate 함수입니다:
public __delegate void EventHandler(
   Object* sender,
   EventArgs* e
);

이벤트를 받기 위해서는, 여러분은 올바른 함수 시그니쳐와 이것을 감쌀 수 있는 delegate를 생성해서, 이벤트 핸들러 함수를 구현해야 합니다. 그리고 나서, 이벤트의 오퍼레이터+=를 이용해서 여러분의 핸들러와 delegate를 등록해 주어야 합니다. 클릭 이벤트의 경우는, 아래와 같이 등록 해주어야 합니다:

// 이벤트 핸들러
void CMyForm::OnAbort(Object* sender, EventArgs *e)
{
  ...
}
// 이벤트 핸들러 등록
m_abortButton->Click += new EventHandler(this, OnAbort);

핸들러 함수가 delegate에 의해 정의된 함수 시그니쳐(function signature)와 일치해야 한다는 사실을 반드시 기억하시기 바랍니다. 그러나, 여러분은 .NET 프레임웍의 매니지된 이벤트과 아닌 순수 이벤트에 대해서 물어봤었습니다-"어떻게 순수 C++에서 이벤트를 구현할 수 있나요?" C++은 특별히 따로 정의된 이벤트 메카니즘이 없습니다. 그러면 뭘 할 수 있을까요? 여러분은 콜백을 typedef를 이용해서 정의하고, 클라이언트에게 정의된 콜백을 제공할 것을 요구하는 방식으로 이벤트 비슷하게 구현할 수 있습니다. 처음에 보았던 qsort함수에서와 비슷하게 말이죠. 그러나 여러분이 여러 개의 이벤트를 핸들링해야 할 때, 이 방법은 부적절하고, 그리고 멤버함수를 콜백함수로 쓰고 싶은 경우는, static extern 함수와는 반대로 상당히 너저분해집니다.

좀 더 좋은 방법은 이벤트를 정의하는 인터페이스를 생성하는 것입니다. COM에서 실제로 이런 방식으로 하고 있습니다. 그러나, 순수 C++만을 이용하면 COM의 오버헤드를 감수할 필요가 없을 때, 좀 더 간단한 클래스를 이용할 수 있습니다. 제가 소수를 찾아주는 CPrimeCalculator라는 클래스를 만들어서 이렇게 하는 방법을 보여 드리겠습니다. 그림 3은 코드를 제가 말씀드린 코드를 보여줍니다.

CPrimeCalculator::FindPrimes(n) 는 소수 n개를 찾아 주는 역활을 합니다. 이 클래스의 객체가 동작하는 중에, CPrimeCalculator은 두개의 이벤트, 동작완료와 동작중 이벤트, 를 발생합니다. 이 이벤트들은 인터페이스 IPrimeEvents에 정의되어 있습니다. IPrimeEvents은 .NET이나 COM의 인터페이스가 아닌, 각각의 이벤트 핸들러의 시그니쳐(함수 인자와 결과값)를 정의하는 순수 C++ 추상 기본 클래스입니다. CPrimeCalculator 이벤트를 처리하는 클라이언트들은 반드시 IPrimeEvents라는 인터페이스를 구현하고, CPrimeCalculate::Register에 이 인터페이스를 등록해야 합니다. 그러면, CPrimeCalculator은 내부 리스트에 이 객체/인터페이스를 추가하게 됩니다. 소수를 찾기 위해서 모든 정수를 검사하기 때문에, CPrimeCalculator 클래스는 주기적으로 지금까지 얼마나 많은 소수를 찾았는지를 보고하게 됩니다:

// CPrimeCalculator::FindPrimes 함수 안에서
for (UINT p=2; p<max; p++) {
   //p가 소수인지 확인
   if (/* 검색의 진행사항을 알려 주기 위해서 이벤트를 날려줌 */)
      NotifyProgress(GetNumberOfPrimes()); 
   ...
}
NotifyDone();

CPrimeCalculator는 내부적인 헬퍼 함수인 NotifyProgress 와 NotifyDone 를 호출해서 이벤트를 발생시키게 됩니다. 이 함수들에서는 클라이언트 리스트안에 있는 모든 클라이언트들에게, 적절한 이벤트 핸들러를 호출하게 해줍니다. 코드 상으로는 아래와 같습니다:

void CPrimeCalculator::NotifyProgress(UINT nFound)
{
  list<IPrimeEvents*>::iterator it;
  for (it=m_clients.begin(); it!=m_clients.end(); it++) {
    (*it)->OnProgress(nFound);
  }
}

STL에 그렇게 썩 익숙하지 않은 독자들은, 반복자(iterator)의 역참조연산자(dereference operator)가 현재의 객체를 반환해 준다는 것을 기억하시기 바랍니다. 그래서 루프 안에 있는 코드는 실제적으로는 아래와 같습니다:

IPrimeEvents* obj = *it;
obj->OnProgress(nFound);

그리고 동작 완료 이벤트를 발생시키는, NotifyDone 함수에 대한 코드 역시 그림 3에서 볼 수 있습니다.

FindPrimes 함수가 제어권을 반환하기 이전에 한 가지 더 고려해야 할 사항이 있습니다. 이벤트를 받는 클라이언트가 하나 이상이 될 수 있고, CPrimeCalculator::FindPrimes를 호출하는 클라이언트와 다른 클라이언트들이 동일하지 않을 수 있다는 사실입니다. 그림 4는 저의 테스트 프로그램인 PrimeCalc의 코드를 보여줍니다. PrimeCalc는 Prime 이벤트에 대한 서로 다른 두 이벤트 핸드러를 구현하는 것을 보여줍니다. 처음의 이벤트 핸들러가 주 다이얼로그인, CMyDlg이고 이 윈도우는 다중 상속을 이용하여 IPrimeEvents를 구현하고 있습니다. 이 다이얼로그는 진행중, 진행 완료 이벤트에 대해 다이얼로그 윈도우에 진행사항을 표시해주고, 진행 완료가 되면 비프(beep)음을 내줍니다. 다른 이벤트 핸들러인, CTracePrimeEvents 역시 IPrimeEvents를 구현하고 있고, 이 클래스는 진행사항을 진단(TRACE)스트림으로 정보를 표시해 줍니다. 그림 6TraceWin에서의 시험 동작 화면을 보여주고 있습니다. (저의 2004년 3월 (영문) 글을 보시거나, 혹은 www.dilascia.com (영문)에서 TraceWin 다운 받으시길 바랍니다.)

그림 5 소수계산기
그림 5 소수계산기

CPrimeCalculator를 이용하는 프로그래머의 입장에서, 이벤트 처리를 하는 것은 간단하고, 수월합니다. IPrimeEvents을 상속받아서, 핸들러 함수를 구현해 주고, Register 함수만 호출해 주면 됩니다. 이벤트를 발생시키는 프로그래머의 입장에서는, 이 작업은 약간 더 지루할 수 있습니다. 처음에는 이벤트 인터페이스를 정의해야하고, 그리고 Register 함수와 Unregister 함수를 작성해야 합니다. 그리고 각각의 이벤트에 대해서 NotifyFoo와 같은 헬퍼 함수들도 작성을 해야 하는 것은 물론이구요. 15개의 이벤트를 가졌다면, 상당히 지루한 작업이 될 수 있습니다. 특히, NotifyFoo와 같은 헬퍼 함수들이 모두 동일한 패턴을 지녔다면 말입니다:

void CMyClass::NotifyFoo(/* args */)
{
  list<IPrimeEvents*>::iterator it;
  for (it=m_clients.begin(); it!=m_clients.end(); it++) {
    (*it)->OnFoo(/* args */);
  }
}

그림 6 TraceWin에서의 소수계산기의 메세지
그림 6 TraceWin에서의 소수계산기의 메세지

NotifyFoo 는 클라이언트 리스트를 순환하여, 각각의 등록된 클라이언트에게 적절한 OnFoo 핸들러를 호출해서, 필요한 함수인자를 넘겨줘야 합니다. 이렇게 지루하고 반복되는 코드에서부터 여러분을 지켜줄 수 있는 일반적으로 사용되는 매크로나 템플릿등이 없을까요? 사실은 있습니다. 제가 여러분께 다음 달에 보여드리도록 하겠습니다. 똑같은 시간, 똑같은 채널에서 제가 여러분께 보여드리겠습니다. 그 때까지 행복한 프로그래밍 하십시요!



  • 이 문서는 한국 개발자를 위하여 Microsoft MVP 가 번역하여 제공한 문서입니다.
  • Microsoft 는 이 문서를 번역자의 의도대로 제공해 드리며 더 정확한 표현 또는 여러분의 의견을 환영합니다.
  • Microsoft 는 커뮤니티를 위해 번역자의 의도대로 이 문서를 제공합니다.



  • 질문이나 코멘트는 여기로 보내주시기 바랍니다  cppqa@microsoft.com.

    Paul DiLascia는 프리랜서 소프트웨어 컨설턴트이고 크게는 웹/UI 디자이너이며, Windows++; Writing Reusable Windows Code in C++ (Addison-Wesley, 1992)를 저술하기도 하였습니다. 틈틈히 PixieLib 이라는 MFC 라이브러리를 계속 개발하고 있습니다. 이 라이브러리는 www.dilascia.com (영문)에서 만나보실 수 있습니다.


    페이지 맨 위로페이지 맨 위로QJ: 060214

    Microsoft