Microsoft MVP성태의 닷넷 이야기
.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [링크 복사], [링크+제목 복사],
조회: 26259
글쓴 사람
정성태 (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)
13448정성태11/20/20232987닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
13447정성태11/16/20232934닷넷: 2162. ASP.NET Core 웹 사이트의 SSL 설정을 코드로 하는 방법
13446정성태11/16/20232856닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20233146Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232885닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232844개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232616개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20233043닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232607Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20233239닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232860닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20233018닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20233234닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20233162닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232983스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232599스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232769오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20233169스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232963닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20233184닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20233324닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233508닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233609스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233396닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233400스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233514닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...