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

... 181  182  183  [184]  185  186  187  188  189  190  191  192  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
355정성태10/9/200624985개발 환경 구성: 19. Internet_Zone 하위에 새로운 코드 그룹을 추가하는 예제 [4]파일 다운로드2
353정성태12/31/200633271개발 환경 구성: 18. 윈도우즈 인증서 서비스 이야기 [3]
354정성태10/23/200635873    답변글 개발 환경 구성: 18.1. 윈도우즈 인증서 서비스 설치
372정성태12/31/200637724    답변글 개발 환경 구성: 18.2. 웹 사이트에 SSL을 적용 [3]
373정성태10/24/200629097    답변글 개발 환경 구성: 18.3. 사용자 입장에서의 HTTPS 접근 (1)
374정성태10/25/200626360    답변글 개발 환경 구성: 18.4. 사용자 입장에서의 HTTPS 접근 (2)
391정성태11/7/200630506    답변글 개발 환경 구성: 18.5. 사용자 인증서 발급
392정성태11/11/200643758    답변글 개발 환경 구성: 18.6. 인증서 관리 (1) - 내보내기/가져오기
394정성태11/9/200628262    답변글 개발 환경 구성: 18.7. 인증서 관리 (2) - 개인키를 내보낼 수 있는 유형의 인증서 발급 [1]
395정성태11/9/200640369    답변글 개발 환경 구성: 18.8. 인증서 관리 (3) - 인증서 MMC 관리자 사용
414정성태12/23/200632067    답변글 개발 환경 구성: 18.9. CRL(Certificate Revocation List) 관리
428정성태12/31/200644974    답변글 개발 환경 구성: 18.10. IIS 7 - SSL 사이트 설정하는 방법 [4]
429정성태12/31/200630900    답변글 개발 환경 구성: 18.11. 서비스를 위한 인증서 설치
352정성태10/2/200620597개발 환경 구성: 17. VPC에 Linux 설치하는 방법 [1]
351정성태10/8/200623143개발 환경 구성: 16. 성태의 무식한(!) 리눅스 탐방기. [4]
349정성태9/26/200621922디버깅 기술: 10. C++/CLI에서 제공되는 명시적인 파괴자의 비밀
347정성태10/6/200625685디버깅 기술: 9. .NET IDisposable 처리 정리 [1]
346정성태9/23/200619217개발 환경 구성: 15. 툴박스에 컨트롤이 자동으로 나타나도록 해주는 옵션 설정
345정성태9/20/200618411오류 유형: 12. WCF 오류 메시지 - Error while trying to reflect on attribute 'MessageContractAttribute'
343정성태10/18/200630240개발 환경 구성: 14. SandCastle 사용법 (NDoc을 대체하는 문서화 도구) [1]파일 다운로드1
344정성태9/20/200620491    답변글 개발 환경 구성: 14.1. 오류 유형 - GAC 에 등록된 DLL 에 대한 문서화 시 오류
340정성태9/15/200619665개발 환경 구성: 13. ISO 파일을 가상 CD-ROM으로 매핑해주는 프로그램
339정성태9/14/200619143오류 유형: 11. ProtocolsSection?
338정성태2/4/200727412개발 환경 구성: 12. BUG: 웹 서비스에서 DataTable 사용하기 [2]파일 다운로드1
350정성태10/2/200620607    답변글 개발 환경 구성: 12.1. ASMX 2.0 and SchemaImporterExtensions파일 다운로드1
335정성태8/20/200628359디버깅 기술: 8. COM+ 서버 응용 프로그램에 대한 F5 디버깅 방법
... 181  182  183  [184]  185  186  187  188  189  190  191  192  193  194  195  ...