Microsoft MVP성태의 닷넷 이야기
.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [링크 복사], [링크+제목 복사],
조회: 34611
글쓴 사람
정성태 (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]

... 76  77  78  [79]  80  81  82  83  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11994정성태7/22/201922748개발 환경 구성: 454. Azure 가상 머신(VM)에서 SMTP 메일 전송하는 방법파일 다운로드1
11993정성태7/22/201917512오류 유형: 561. Dism.exe 수행 시 "Error: 2 - The system cannot find the file specified." 오류 발생
11992정성태7/22/201919766오류 유형: 560. 서비스 관리자 실행 시 "Windows was unable to open service control manager database on [...]. Error 5: Access is denied." 오류 발생
11991정성태7/18/201916751디버깅 기술: 128. windbg - x64 환경에서 닷넷 예외가 발생한 경우 인자를 확인할 수 없었던 사례
11990정성태7/18/201918704오류 유형: 559. Settings / Update & Security 화면 진입 시 프로그램 종료
11989정성태7/18/201917529Windows: 162. Windows Server 2019 빌드 17763부터 Alt + F4 입력시 곧바로 로그아웃하는 현상
11988정성태7/18/201920787개발 환경 구성: 453. 마이크로소프트가 지정한 모든 Root 인증서를 설치하는 방법
11987정성태7/17/201926745오류 유형: 558. 윈도우 - KMODE_EXCEPTION_NOT_HANDLED 블루스크린(BSOD) 문제 [1]
11986정성태7/17/201918398오류 유형: 557. 드라이브 문자를 할당하지 않은 파티션을 탐색기에서 드라이브 문자와 함께 보여주는 문제
11985정성태7/17/201918382개발 환경 구성: 452. msbuild - csproj에 환경 변수 조건 사용 [1]
11984정성태7/9/201926636개발 환경 구성: 451. Microsoft Edge (Chromium)을 대상으로 한 Selenium WebDriver 사용법 [1]
11983정성태7/8/201915797오류 유형: 556. nodemon - 'mocha' is not recognized as an internal or external command, operable program or batch file.
11982정성태7/8/201915836오류 유형: 555. Visual Studio 빌드 오류 - result: unexpected exception occured (-1002 - 0xfffffc16)
11981정성태7/7/201919280Math: 64. C# - 3층 구조의 신경망(분류)파일 다운로드1
11980정성태7/7/201929063개발 환경 구성: 450. Visual Studio Code의 Java 확장을 이용한 간단한 프로젝트 구축파일 다운로드1
11979정성태7/7/201919998개발 환경 구성: 449. TFS에서 gitlab/github등의 git 서버로 마이그레이션하는 방법
11978정성태7/6/201919201Windows: 161. 계정 정보가 동일하지 않은 PC 간의 인증을 수행하는 방법 [1]
11977정성태7/6/201923657오류 유형: 554. git push - error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413 Request Entity Too Large
11976정성태7/4/201918144오류 유형: 553. (잘못 인증 한 후) 원격 git repo 재인증 시 "remote: HTTP Basic: Access denied" 오류 발생
11975정성태7/4/201926622개발 환경 구성: 448. Visual Studio Code에서 콘솔 응용 프로그램 개발 시 "입력"받는 방법
11974정성태7/4/201922733Linux: 22. "Visual Studio Code + Remote Development"로 윈도우 환경에서 리눅스(CentOS 7) C/C++ 개발
11973정성태7/4/201921127Linux: 21. 리눅스에서 공유 라이브러리가 로드되지 않는다면?
11972정성태7/3/201925343.NET Framework: 847. JAVA와 .NET 간의 AES 암호화 연동 [1]파일 다운로드1
11971정성태7/3/201920960개발 환경 구성: 447. Visual Studio Code에서 OpenCvSharp 개발 환경 구성
11970정성태7/2/201920174오류 유형: 552. 웹 브라우저에서 파일 다운로드 후 "Running security scan"이 끝나지 않는 문제
11969정성태7/2/201920683Math: 63. C# - 3층 구조의 신경망파일 다운로드1
... 76  77  78  [79]  80  81  82  83  84  85  86  87  88  89  90  ...