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

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12877정성태12/15/20217368스크립트: 36. 파이썬 - pymysql 기본 예제 코드
12876정성태12/14/20217206개발 환경 구성: 616. Custom Sources를 이용한 Azure Monitor Metric 만들기
12875정성태12/13/20216837스크립트: 35. python - time.sleep(...) 호출 시 hang이 걸리는 듯한 문제
12874정성태12/13/20216866오류 유형: 773. shell script 실행 시 "$'\r': command not found" 오류
12873정성태12/12/20217998오류 유형: 772. 리눅스 - PATH에 등록했는데도 "command not found"가 나온다면?
12872정성태12/12/20217843개발 환경 구성: 615. GoLang과 Python 빌드가 모두 가능한 docker 이미지 만들기
12871정성태12/12/20217873오류 유형: 771. docker: Error response from daemon: OCI runtime create failed
12870정성태12/9/20216423개발 환경 구성: 614. 파이썬 - PyPI 패키지 만들기 (4) package_data 옵션
12869정성태12/8/20218734개발 환경 구성: 613. git clone 실행 시 fingerprint 묻는 단계를 생략하는 방법
12868정성태12/7/20217350오류 유형: 770. twine 업로드 시 "HTTPError: 400 Bad Request ..." 오류 [1]
12867정성태12/7/20217008개발 환경 구성: 612. 파이썬 - PyPI 패키지 만들기 (3) entry_points 옵션
12866정성태12/7/202114390오류 유형: 769. "docker build ..." 시 "failed to solve with frontend dockerfile.v0: failed to read dockerfile ..." 오류
12865정성태12/6/20217138개발 환경 구성: 611. 파이썬 - PyPI 패키지 만들기 (2) long_description, cmdclass 옵션
12864정성태12/6/20215528Linux: 46. WSL 환경에서 find 명령을 사용해 파일을 찾는 방법
12863정성태12/4/20217466개발 환경 구성: 610. 파이썬 - PyPI 패키지 만들기
12862정성태12/3/20216165오류 유형: 768. Golang - 빌드 시 "cmd/go: unsupported GOOS/GOARCH pair linux /amd64" 오류
12861정성태12/3/20218423개발 환경 구성: 609. 파이썬 - "Windows embeddable package"로 개발 환경 구성하는 방법
12860정성태12/1/20216455오류 유형: 767. SQL Server - 127.0.0.1로 접속하는 경우 "Access is denied"가 발생한다면?
12859정성태12/1/202112702개발 환경 구성: 608. Hyper-V 가상 머신에 Console 모드로 로그인하는 방법
12858정성태11/30/20219969개발 환경 구성: 607. 로컬의 USB 장치를 원격 머신에 제공하는 방법 - usbip-win
12857정성태11/24/20217284개발 환경 구성: 606. WSL Ubuntu 20.04에서 파이썬을 위한 uwsgi 설치 방법
12856정성태11/23/20219211.NET Framework: 1121. C# - 동일한 IP:Port로 바인딩 가능한 서버 소켓 [2]
12855정성태11/13/20216413개발 환경 구성: 605. Azure App Service - Kudu SSH 환경에서 FTP를 이용한 파일 전송
12854정성태11/13/20218095개발 환경 구성: 604. Azure - 윈도우 VM에서 FTP 여는 방법
12853정성태11/10/20216368오류 유형: 766. Azure App Service - JBoss 호스팅 생성 시 "This region has quota of 0 PremiumV3 instances for your subscription. Try selecting different region or SKU."
12851정성태11/1/20217875스크립트: 34. 파이썬 - MySQLdb 기본 예제 코드
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...