Microsoft MVP성태의 닷넷 이야기
.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [링크 복사], [링크+제목 복사],
조회: 26356
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# 관점에서의 Observer 패턴 구현

최근에 아래의 글을 읽게 되었습니다.

옵저버 패턴의 이해 - 윈폼예제
; http://hoons.kr/Lecture/LectureMain.aspx?BoardIdx=48739&kind=26&view=0

사실, 제가 추천해 드리는 것은 저 글 자체보다는 'daniel'님이 그 글에 남긴 질문입니다.

설명을 깔끔하게 잘 해주신 것 같습니다. 예제 또한 군더더기 하나 없이 좋네요.. ^^ㅋ 좋은 글 감사합니다. 한가지 질문을 드린다면, 어느 경우에 C#에서 이벤트 말고, Observer 패턴을 쓰는게 효율적인지 문의 드립니다.


Pattern이란 것들이 대부분 보면, 'interface'에서 출발합니다. 한마디로, 'interface'를 얼마나 다양하게 활용할 지에 대한 숱한 나열을 한 것이 '디자인 패턴'이라고 여겨질 정도입니다.

그런데, 이 패턴들이 자주 언급되는 프로그램 언어가 바로 'Java'라는 사실에 주목할 필요가 있습니다.

자바는, 언어 자체에 '함수 포인터' 개념이 없기 때문에 이런 부분을 대체해서 구현할 방법이 interface 밖에는 없습니다. 아마도 일반적인 윈도우 개발자들이 자바의 swing 라이브러리를 이용하여 윈도우 프로그램을 할 때 당황스러워하는 것 중의 하나가 이벤트 핸들러를 구현하는 차이라는 점에서 많은 분들이 동의하실 것입니다.

예를 들면, 자바에서는 Button의 Click 이벤트에 대해 다음과 같은 식으로 구현을 합니다.

public class MyFrame extends JFrame implements ActionListener 
{
  private JButton button1 = new JButton("Click me!");
  private JButton button2 = new JButton("Click me too!");

  public MyFrame() 
  {
    button1.addActionListener(this);
    button2.addActionListener(this);
  }

  public void actionPerformed(ActionEvent evt) 
  {
    Object src = evt.getSource();
    if (src == button1) 
    {
      ... [Click Event Handler Code] ...
    } else if (src == button2) 
    {
      ... [Click Event Handler Code] ...
    }
  }
}

보시는 것처럼, JButton 개체는 addActionListener 메서드를 통해서 '함수 포인터'가 아닌 개체 인스턴스를 받습니다. 그리고, 그 개체 인스턴스는 JDK에 다음과 같이 정의된 ActionListener 인터페이스를 구현하고 있어야 합니다. (닷넷 개발자들은 낯설겠지만, 자바에서는 인터페이스에 "I" 접두사를 붙이는 것이 관례가 아닙니다.)

public interface EventListener { }

public interface ActionListener extends EventListener
{
    public void actionPerformed(ActionEvent e);
}

반면에, 비교를 위해 동일한 목적의 C# 구현예제를 볼까요?

public class MainForm : Form
{
  private Button button1 = new Button("Click me!");
  private Button button2 = new Button("Click me too!");

  public MyFrame() 
  {
    button1.Click += Button1_Clicked;
    button2.Click += Button2_Clicked;
  }

  void Button1_Clicked(object sender, EventArgs e) 
  { 
      ... [Click Event Handler Code] ...
  }
  
  void Button2_Clicked(object sender, EventArgs e) 
  { 
      ... [Click Event Handler Code] ...
  }
}

이처럼, C#(및 VB.NET 같은 언어들)에서는 '함수 포인터(C#에서는 delegate)' 개념이 있기 때문에 이벤트 핸들러를 구현하기 위해 인터페이스까지 관여시킬 필요가 없습니다.




swing의 addActionListener를 구현하는 JButton 개체는 내부적으로 Observer 패턴을 구현하고 있는 것입니다. 마찬가지로 C#의 경우에는 button1.Click 이벤트가 Observer 패턴을 구현하고 있는 것이고.

이에 대해서 좀 더 들어가볼까요?

우선, '함수 포인터'가 있기 때문에 interface 구문을 제거할 수 있다고는 하지만 그것이 전부는 아닙니다. 일례로, C++의 경우에도 함수 포인터는 제공이 되지만 "옵저버 패턴의 이해 - 윈폼예제"에서 보여지는 Add/Remove/Notify에 대한 관리 코드는 별도로 구현을 해주어야 합니다. 즉, interface 키워드만 제거될 뿐 함수 포인터 값에 대한 목록 관리는 Java가 아닌 다른 언어들에서도 동일하게 해주어야 합니다.

하지만... C#에서는 그런 작업이 필요하지 않습니다.

이제, 'daniel' 님의 질문에 대한 답이 나옵니다. 단적으로 말하면, Observer 패턴은 사실 C# 개발자라면 모른 체로 사용해왔던 개념입니다. 이미 delegate를 사용해 왔다면 그것 자체가 Observer 패턴을 구현한 것이나 다름 없습니다.

이해를 돕기 위해... ^^ 잠깐 예제와 함께 설명해 볼까요?

아래의 코드는 "옵저버 패턴의 이해 - 윈폼예제"에서 설명한 IObserverCenter 인터페이스입니다.

public void IObserverCenter
{
    void AddObserver(IObserver observer);
    void RemoveObserver(IObserver observer);
    void NotifyObservers(int parameter);
}

그리곤, 내부적으로 Add/Remove한 observer 개체들을 관리하기 위해 IObserverCenter 인터페이스를 다음과 같이 구현해 주고 있습니다.

public class CountObserverCenter : IObserverCenter
{
    private List _observerList = new List();

    ... [Add/Remove/Notify 구현 생략]...
}

닷넷에서는 위의 코드에 대한 모든 역할을 아예 프레임워크 자체에 내장하고 있는데 그것이 바로 MulticastDelegate 타입입니다. 그리고, C#에서는 언어적으로 이를 자연스럽게 연동할 수 있도록 delegate 키워드로 정의된 함수 형식을 타입으로 지원하고 있어서 컴파일 시에 MulticastDelegate으로 번역되도록 바꿔줍니다.

따라서, 다른 언어에서는 IObserverCenter 인터페이스를 구현해주는 작업을 해야 하지만, C#의 경우에는 그럴 '필요'가 없습니다. 실제로, "옵저버 패턴의 이해 - 윈폼예제" 글에서의 Observer 패턴을 구현한 장황한 예제 코드를 delegate를 사용하여 표현하면 다음과 같이 간단합니다.

public partial class Form1 : Form
{
    public delegate void NumberChanged(int number);
    public NumberChanged FormNumberChanged; // MulticastDelegate 타입으로 바뀌면서 CountObserverCenter 클래스가 할 일을 모두 내장

    int _currentNum;

    public Form1()
    {
        InitializeComponent();

        _currentNum = 0;

        Screen1 screenView1 = new Screen1();
        this.FormNumberChanged += screenView1.NotifyAction; // IObserverCenter.AddObserver 메서드 역할

        Screen2 screenView2 = new Screen2();
        this.FormNumberChanged += screenView2.NotifyAction;

        ...[생략]...
    }

    private void button1_Click(object sender, EventArgs e)
    {
        _currentNum++;

        if (FormNumberChanged != null)
        {
            FormNumberChanged(_currentNum); // IObserverCenter.NotifyObservers 메서드 역할
        }
    }
}

public partial class Screen1 : UserControl // IObserver 인터페이스 상속 없이 NotifyAction 메서드를 그대로 사용
{
    int _currentNum;

    public Screen1()
    {
        InitializeComponent();
    }

    public void NotifyAction(int parameter) // IObserver.NotifyAction 메서드 역할
    {
        _currentNum = parameter;
        this.label1.Text = string.Format("Screen 1 : {0}", _currentNum);
    }
}

public partial class Screen2 : UserControl
{
    ...[생략: Screen1과 유사한 코드 반복]... 
}

훨씬 심플하죠? 이렇게 좋은 delegate를 보고 있자면 굳이 교과서적인 방법으로 Observer 패턴을 구현해야 하는 이유가 모호해집니다.

물론, 이럴지라도 interface를 사용해야 할 필요가 나옵니다. 예를 들어, 다른 곳에서 Form의 이벤트를 받고 싶을 때 직접 개체를 참조해야 하는 제약을 끊고 싶은 경우 인터페이스를 이용해야 하기 때문인데요. 그럴 필요가 있다면 다음과 같이 간단하게 인터페이스를 추가해서 정의해 주는 것만으로 해결이 됩니다.

public interface INumberChanged
{
    NumberChanged FormNumberChanged { get; set; }
}

public partial class Form1 : Form, INumberChanged
{
    ...[생략]...
    public NumberChanged FormNumberChanged { get; set; }
}     

여기까지... 눈에 들어오시나요? 이처럼, C#에서는 함수 포인터의 지원과 함께 MulticastDelegate 타입을 지원하는 .NET Framework의 도움으로 Observer 패턴 자체가 녹아져 있다고 보시면 됩니다.

그렇다고, Observer 패턴의 정석적인 방법을 모르고 사는 것도 좀 그렇지요. ^^ 닷넷을 벗어나면 다시 그렇게 쓸 줄 알아야겠고... 어쨌든 명시적으로 Observer 패턴을 알고 있다면 이해의 범위가 더 커지는 데 도움이 될 것입니다.

*** 첨부 파일은, "옵저버 패턴의 이해 - 윈폼예제" 글에서 첨부한 예제 프로젝트를 본문에서 설명한 delegate 구문으로 동등하게 바꾼 프로젝트입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 1/9/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2013-07-30 02시51분
[이동우] 잘보고 갑니다~

"Observer 패턴을 구현해야 하는 이유가 모호해집니다." 라는 말이 귀에 와 닿습니다.
[guest]

1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13323정성태4/16/20234555개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20235386VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20234179개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20234049개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234565개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234447개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234495오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233761오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20234073Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20234198개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20234211개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20234274Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234700C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20234355C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20234464.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20234409스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20234148.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20234063Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20234397Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234688VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/20234034Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/20234627Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
13301정성태3/29/20234776Windows: 239. C/C++ - Windows 10 Version 1607부터 지원하는 /DEPENDENTLOADFLAG 옵션파일 다운로드1
13300정성태3/28/20234501Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
13299정성태3/27/20234222Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
13298정성태3/27/20234211Windows: 236. Win32 - MessageBeep 소리가 안 들린다면?
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...