CodeGym /행동 /C# SELF /패턴 "관찰자" ( Observer)...

패턴 "관찰자" ( Observer)

C# SELF
레벨 53, 레슨 3
사용 가능

1. 소개

어떤 동작을 수행하는 객체가 있다고 가정해보자 — 예를 들어 버튼이나 우리 코드의 Worker 같은 것. 동시에 이 동작에 반응해야 하는 여러 객체가 존재할 수 있어. 만약 Worker 클래스에 가능한 모든 "리스너"를 하드코딩하면 유지보수가 지옥이 된다: 구독자 목록이 바뀔 때마다 Worker 내부를 수정해야 하기 때문이지.

이건 개방-폐쇄 원칙(OCP)을 위반하고, 좋은 아키텍처 관행이 아니다.

패턴 Observer: 기본 아이디어

패턴 "관찰자" (Observer)는 이 문제를 해결해. 발행자가 변경을 알릴 수는 있지만, 누가 어떻게 반응하는지는 전혀 알 필요가 없게 해주는 패턴이야. 발행자는 그냥 "신호"를 보내고, 관심 있는 쪽이 알아서 반응하는 구조지.

비유: 뉴스레터 구독. 편집부나 채널(발행자)은 새 소식을 보내고, 모든 구독자(관찰자)는 그걸 받지. 편집부는 누가 구독했는지 신경 쓸 필요가 없어.

흥미로운 사실: "관찰자"는 워낙 유명해서 GoF(밴드 오브 포)의 디자인 패턴에 들어있어.

Observer in C#: 이벤트와 델리게이트로 구현

C#에서는 이벤트와 델리게이트 메커니즘으로 이미 Observer 패턴이 언어 레벨에서 지원돼. 이벤트는 확장 지점(extension point)으로, 여러 핸들러가 구독할 수 있어. 수동으로 구독자 리스트를 관리하는 대신 언어의 이벤트 메커니즘에 맡기면 편해. 아래에서 먼저 '수동' 구현을 보고, 그다음 이벤트 기반 구현을 보자.

2. 이벤트를 사용하지 않은 고전적인 Observer 구현

언어에 이벤트가 없다면 어떻게 보일지 살펴보자:


// 관찰자 인터페이스
public interface IObserver
{
    void Update(string message);
}

// 발행자
public class Worker
{
    private List<IObserver> observers = new List<IObserver>();

    public void Subscribe(IObserver observer)
    {
        observers.Add(observer);
    }

    public void Unsubscribe(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void DoWork()
    {
        Console.WriteLine("Worker 작동 중...");
        NotifyObservers("작업 완료!");
    }

    private void NotifyObservers(string message)
    {
        foreach (var observer in observers)
        {
            observer.Update(message);
        }
    }
}

// 구체적인 관찰자
public class WorkListener : IObserver
{
    public void Update(string message)
    {
        Console.WriteLine($"
WorkListener가 메시지를 받음: {message}");
    }
}

초기화 예:


var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();

참고: 여기서는 구독자 리스트(List<IObserver> observers)를 수동으로 관리하고, 구독/구독해제는 명시적인 Subscribe/Unsubscribe 메서드로 처리돼.

3. 이벤트와 델리게이트 — C# 스타일의 고수준 Observer

같은 기능을 이벤트로 더 간단하고 깔끔하게 구현할 수 있어. 이게 바로 C# 스타일의 Observer야:


public class Worker
{
    public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;

    public void DoWork()
    {
        Console.WriteLine("Worker 작동 중...");
        OnWorkCompleted("작업 완료!");
    }

    protected virtual void OnWorkCompleted(string message)
    {
        WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
    }
}

public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; set; }
}

public class WorkListener
{
    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"WorkListener가 메시지를 받음: {e.Message}");
    }
}

// 구독:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();

이 접근의 장점:

  • 구독자 리스트를 수동으로 유지할 필요가 없음.
  • 이벤트의 모든 기능 사용 가능: 다중 구독, 구독 해제, 람다 등.
  • 안전성 확보: 이벤트를 호출할 수 있는 건 발행자뿐임.
  • 약한 결합: 발행자는 리스너들에 대해 아무 것도 모름.

4. "관찰자"가 우리 애플리케이션에 어떻게 들어맞는가

이제 Observer 패턴을 콘솔 앱에 통합해보자. Worker가 여러 핸들러를 가지게 해서 작업 완료에 대해 각각 다르게 반응하게 하자: 누군가는 콘솔에 출력하고, 누군가는 완료된 작업 수를 세고, 누군가는 "사장님! 다 끝났어요!"라고 메일을 보낼 수도 있어.

예제로 코드 확장하기


// 두번째 리스너 — 카운터
public class WorkCounter
{
    public int Count { get; private set; }

    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Count++;
        Console.WriteLine($"작업이 반영됨. 총: {Count} 완료됨.");
    }
}

// 객체 생성
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();

// 두 가지 구독
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;

// 여러 작업 흉내내기
worker.DoWork();
worker.DoWork();
// Output:
// Worker 작동 중...
// WorkListener가 메시지를 받음: 작업 완료!
// 작업이 반영됨. 총: 1 완료됨.
// Worker 작동 중...
// WorkListener가 메시지를 받음: 작업 완료!
// 작업이 반영됨. 총: 2 완료됨.

이처럼 필요할 때마다 "관찰자"를 추가하면 Worker의 코드 한 줄도 안 건드리고 시스템 동작을 확장할 수 있어. Worker 클래스는 그대로고, 전체 시스템의 행동은 구독자들을 통해 확장돼.

5. 유용한 뉘앙스

실례: 인터페이스와 GUI에서의 Observer

Observer 패턴은 모든 GUI 프레임워크의 기반이야. Windows Forms나 WPF에서 버튼 클릭은 Click 이벤트를 발생시켜. 너는 그 이벤트에 반응하는 핸들러(관찰자)를 작성하면 되고 — Button 클래스나 .NET 라이브러리는 네 구독자에 대해 아무 것도 알 필요가 없어.


// WPF나 WinForms에서 (대략)
myButton.Click += (s, e) => MessageBox.Show("사용자가 버튼을 눌렀음!");

실제 프로젝트에서의 Observer

  • UI(클릭, 변경, 타이머 등 반응 처리).
  • 알림/이벤트 시스템.
  • 플러그인 확장 시스템(코어가 이벤트를 발생시키고 확장이 구독).
  • 분산 시스템이나 게임 엔진(약하게 결합된 반응 체인).

요약하자면, 어떤 부분이 다른 부분의 변화에 반응하도록 확장 가능한 시스템을 만들고 싶다면 — Observer must have!

7. 특징, 흔한 실수와 예방

Observer 사용 시 잠재적 문제

메모리 누수. 구독자가 이벤트를 구독했지만 구독 해제를 하지 않으면(특히 수명이 긴 발행자에 대해) 가비지 컬렉터가 그 구독자를 수거하지 못할 수 있어. 발행자가 델리게이트를 통해 해당 객체를 참조하고 있기 때문이야. 구독자가 더 이상 필요하지 않은데 발행자가 계속 살아 있으면 심각한 문제로 이어질 수 있어.

중복 구독. 같은 핸들러가 두 번 구독되면 두 번 호출돼서 동작이 중복되거나 의도치 않은 효과가 날 수 있어.

핸들러에서의 예외. 어떤 핸들러가 예외를 던지면 이후 구독자들의 실행이 중단될 수 있어. 핸들러의 복원력(resilience)을 고려하고, 필요하다면 수동으로 try-catch로 감싸서 하나가 실패해도 다른 구독자들이 실행되게 하자.

자주 보이는 누수 패턴

flowchart LR
    Publisher["발행자
(Worker)"] -- 이벤트 --> ObserverA["리스너 A (살아있음!)"] Publisher -- 이벤트 --> ObserverB["리스너 B (메모리 누수: 구독 해제 잊음)"]
2
과제
C# SELF, 레벨 53, 레슨 3
잠금
C#의 이벤트를 활용한 옵저버 패턴 구현
C#의 이벤트를 활용한 옵저버 패턴 구현
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION