C#에서는 Listener 패턴을 구현하기 쉽다. 특히 event라는 예약어를 제공하고 있어서, Callback 형태의 구현이 어렵지 않다.


class Test 
{ 
        int m_nCurrentIndex = 0; 
        public delegate void IndexChangedEventHandler(); 
        public event IndexChangedEventHandler ChangeCurrentIndex; 
        public void ChangePage(int nIndex) 
        { 
               m_nCurrentIndex = nIndex; 
               ChangeCurrentIndex(); 
               m_nCurrentIndex = m_nCurrentIndex + 1; 
        } 
}

위의 코드는 바로 그 event 형태로 어떻게 구현하는지를 나타낸다.

이를 사용하는 로직은 아래와 같다.

class Program 
{ 
        [STAThread] 
        static void Main() 
        { 
             Test test = new Test();

             test.ChangeCurrentIndex 
                 +=  new IndexChangedEventHandler(test_ChangeCurrentIndex); 
             test.ChangePage(1);

             Console.WriteLine(“Complete!”); 
        }

        private void test_ChangeCurrentIndex() 
        { 
               Console.WriteLine(“event!”); 
        } 
}

 

“test.ChangeCurrentIndex” 이렇게 쓰고 “+=” 만 추가하면, 자동으로 코드가 생성되면서 아래에 함수가 하나 생긴다.
이것이 바로 event 구현 작업이다. 즉 test 내부에서 “ChangeCurrentIndex()” 이 함수가 불리는 순간, 이벤트 구독한 함수로 호출을 하게 된다.

즉 역으로 함수를 부르는 일종의 Callback  함수로 생각하면 된다.

위의 작업을 한 줄씩 실행되는 순서를 보면 아래와 같은 순서로 진행된다.

  1. Test test = new Test(); –> Test 클래스의 인스턴스를 만든다.
  2. test.ChangeCurrentIndex +=  new IndexChangedEventHandler(test_ChangeCurrentIndex); –> 이벤트를 구독한다.
  3. test.ChangePage(1); –> Test 클래스의 ChangePage 함수를 호출한다.
  4. m_nCurrentIndex = nIndex;   -> TestClass 내부 : m_nCurrentIndex에 nIndex 값을 대입한다.
  5. ChangeCurrentIndex();   -> Event를 발생시킨다.
  6. Console.WriteLine(“event!”)  -> Main 쪽의 이벤트 구독할 때 등록한 함수(test_ChangeCurrentIndex)로 들어가서 “event!” 라는 문자열을 찍는다.
  7. m_nCurrentIndex = m_nCurrentIndex + 1; –> +1 를 한다. 그리고 ChangePage 함수를 종료한다.
  8. Console.WriteLine(“Complete!”);   -> Main 으로 돌아와서 Complete를 찍고 종료한다.


이것을 실행하는 순서를 다이어그램으로 나타내면 아래와 같다.

 

Test라는 클래스와 Program이라는 클래스가 서로 통신을 한다고 했을 때, 위와 같은 Listen 구조가 안된다면, 결국 Program 이라는 클래스에서 호출하는 작업이 없다면 Test가 Program으로 데이터를 보낼 방법이 없다. 만일 저 위와 같은 형태가 안되는 구조라면, Test 클래스 안에는 Program 개체의 레퍼런스를 들고 있어야 한다.

class Test
{

        Program m_parent = null;
        int m_nCurrentIndex = 0;

        ~~~~~~~~~
}

그리고 Program 클래스 안에는 Test를 통해 처리해야 할 함수 부분에서 private를 public 으로 바꿔야된다.

        public void test_ChangeCurrentIndex()
        {
               Console.WriteLine(“event!”);
        }

호출할 때는 기존에 함수형태로 된 것을 Program을 통해서 부르도록 해야 된다.

        public void ChangePage(int nIndex)
        {
               ~~~~~~~~~~~
               m_parent .test_ChangeCurrentIndex();
               ~~~~~~~~~~~
        }

서로 인스턴스를 주고 받아야 저런 통신이 가능하게 되는 것이다.

하지만 위와 같이 하게 되는 경우 같은 DLL 이나 EXE 의 경우면 어차피 같은 프로젝트 안에 있는 내용이니 큰 문제가 없지만, 만일 서로 다른 프로젝트로 되어 구성된 경우라면, 서로 인스턴스를 주고 받으려면 상당히 난감해 질 수 밖에 없다.

이렇게 훌륭한 event 구성에도 심각한 문제가 있다.

그건 바로 실행되는 순서에 있다. 실행되는 순서 중 5~7 사이를 보도록 하자.
만일 5->7->6 의 순서로 실행하고 싶다면 어떻게 해야 할까? 즉, ChangePage 함수가 종료되면 자동으로 test_ChangeCurrentIndex 가 실행되고 싶을 때라는 것이다. 간단한 구조의 프로그램이나, 구성의 경우에는 이런 부분을 신경 쓰지 않지만, Multi Thread 나 Windows UI 응용 프로그램인 경우 문제가 발생한다.

왜 문제가 발생할까?

만일 저 Event 구조가 연달아 실행되는 형태라면? 이라는 가정으로 시작해보자.

만일 저런 형태라면, E에서 부터 시작해서 A까지 모든 작업이 끝나야 실제 동작이 끝나게 된다. 이 경우 함수를 연달아 부른 구조와 별 다를게 없다. 그래서 간혹 중간에 시간이 오래 걸리는 작업이 있는 경우, 메인 프로그램의 UI가 마치 다운된 것과 같다.

UI가 다운되지 않은 것처럼 하기 위해서는 E 가 완전히 끝난 뒤, D가 실행되고, 다음에는 D가 완전히 다 실행한 뒤, C가 실행되는 형태가 되어야 한다.

즉 맨 위의 예제 코드를 기준으로 보면, 1->2->3->4->5->6->7->8 이 아니라, 1->2->3->4->5->7->6->8 또는 1->2->3->4->5->7->8->6 순으로 실행되면 되는 것이다. 어찌되었던 간에, ChangePage 메소드가 종료 된 뒤에, 실행을 요청한 코드가 실행되어야 한다는 것이다.

이렇게 하려면 어떻게 해야 할까?

1가지 방법은 Windows Message를 사용하는 것이다. SendMessage 나, PostMessage 같은 것으로 호출하는 방법이다. 최소한 SendMessage나 PostMessage는 Windows Message Queue에다 던지는 형식이기 때문에, Windows Message Queue에서 해당 메시지가 나올 때 까지 나머지 부분을 계속 실행하게 된다. 즉 5 번 단계의 실행에서 바로 7단계로 넘어 간 뒤에, Windows Message Queue에서 Message 가 언제 나오는지에 따라, 6 번은 그에 맞게 실행되는 것이다.

이 작업은 .NET에서 Windows Message를 다루는 방법이므로 일단 이 방법은 넘어가도록 하자.

순수 .NET 코드에서는 어떻게 해야 할까?

바로 Invoke 라는 함수를 쓰는 것이다.

Invoke의 정확한 목적은 해당 개체에 있는 함수를 강제적으로 동작하게 하는 것인데, 이것은 상대 클래스내의 함수를 무조건 실행하는 것이다. public 이든, protected 든, 심지어 private 일지라도 강제적으로 실행하는 방법이다.

이 때, 중요한 것은 Invoke 명령어는 System.Windows.Form 네임스페이스에 있는 Control 기반의 Windows 개체만 동작한다는 것이다.  위의 예제 코드는 다음과 같이 다시 작성을 해야 한다. 먼저 Windows Form 기반 응용 프로그램이여야 할 것이다.

public partial class Form1 : Form 
{ 
        Test m_test = null; 
        public Form1() 
        { 
            InitializeComponent(); 
            m_test = new Test(this); 
            m_test.ChangeCurrentIndex 
              +=  new IndexChangedEventHandler(m_test_ChangeCurrentIndex); 
            test.ChangePage(1); 
            Console.WriteLine(“Complete!”); 
        }

        private void m_test_ChangeCurrentIndex() 
        { 
               Console.WriteLine(“event!”); 
        } 
} 

class Test 
{ 
        Form m_parent = null; 
        int m_nCurrentIndex = 0; 
        public delegate void IndexChangedEventHandler(); 
        public event IndexChangedEventHandler ChangeCurrentIndex; 
        public Test(Form parent) 
        { 
             m_parent = parent; 
        } 
        public void ChangePage(int nIndex) 
        { 
               m_nCurrentIndex = nIndex; 
               ChangeCurrentIndex(); 
               m_nCurrentIndex = m_nCurrentIndex + 1; 
        } 
}

앞서 event 기반의 Listen 패턴 관련해서 상호간 인스턴스가 필요없다고 했는데, 이번에는 필요하다. 왜냐면, event를 호출하는 로직을 할 때, Invoke를 해야 하는데, 그 Invoke를 당하는 대상의 인스턴스가 필요하기 때문이다.

다음 로직을 보자. 이제 ChangePage 라는 함수를 수정해야 하기 때문이다.

public void ChangePage(int nIndex) 
{ 
      m_nCurrentIndex = nIndex; 
      m_parent.Invoke(ChangeCurrentIndex); 
      m_nCurrentIndex = m_nCurrentIndex + 1; 
 } 

앞에서는 ChageCurrentIndex 라는 이벤트를 직접 함수처럼 실행했지만, 이번에는 Invoke 라는 것을 사용해서 실행하기 때문이다. 즉 Windows개체.Invoke(delegation함수) 형태로 실행하는 것이다.

m_parent.Invoke(ChangeCurrentIndex)라는 문구가 올 때, ChageCurrentIndex 에 연결된 함수가 실행되지 않고, 일단 m_parent 한테 실행하라고 지시만 한 상태가 된다. 일단 저 안의 함수 내용이 완료될 때까지는 저 Invoke가 실행되지 않는다. 그러므로 ChangeCurrentIndex에 구독한 부분은 나중에 실행되게 되고, 바로 그 아래에 있는 코드가 실행되게 된다. 그리고 그 함수가 완전히 종료되면, 그제서야 Invoke 한 함수를 실행하게 되는 것이다.

이 모든 작업은 COM Component 기반의 동작이 가능한 System.Windows.Form 계열의 Windows Form, Control 등만이 가능한 기능인 것이다.

728x90

+ Recent posts