본문 바로가기
개발💻/C#

[C#] Observer Pattern 1

by Sports Entrepreneur 2024. 7. 13.

옵저버 패턴

1. 정의

옵저버 패턴은 행동 디자인 패턴 중 하나로, 한 객체의 상태 변화가 다른 객체들에 의해 자동으로 감지되고 업데이트될 수 있도록 일대다 종속성을 정의합니다. 주로 분산 이벤트 처리 시스템을 구현할 때 사용됩니다. 주체(Subject)는 상태를 가지고 있으며, 옵저버(Observer)는 주체의 상태 변화를 감지하고 적절히 반응하는 객체들입니다.

2. 사용 이유와 목적

옵저버 패턴은 다음과 같은 상황에서 유용합니다:

  • 다수의 객체가 특정 객체의 상태 변화를 실시간으로 알아야 하는 경우: 예를 들어, 주식 가격 변동을 여러 클라이언트에게 실시간으로 알리고 싶을 때.
  • 동적인 의존성 관계가 필요한 경우: 객체들 간의 관계를 실행 중에 동적으로 설정하거나 변경해야 할 때.
  • 느슨한 결합을 유지하고 싶을 때: 주체와 옵저버가 서로 직접적으로 참조하지 않고, 인터페이스를 통해 통신함으로써 모듈화를 높이고 유지보수를 쉽게 할 수 있습니다.

3. 사용 예시

using System;
using System.Collections.Generic;

// 주체 클래스
public class Subject
{
    private List<IObserver> observers = new List<IObserver>();
    private int state;

    public int State
    {
        get { return state; }
        set
        {
            state = value;
            NotifyAllObservers();
        }
    }

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

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

    public void NotifyAllObservers()
    {
        foreach (var observer in observers)
        {
            observer.Update();
        }
    }
}

// 옵저버 인터페이스
public interface IObserver
{
    void Update();
}

// 구체적인 옵저버 클래스
public class ConcreteObserver : IObserver
{
    private string name;
    private Subject subject;

    public ConcreteObserver(string name, Subject subject)
    {
        this.name = name;
        this.subject = subject;
        this.subject.Attach(this);
    }

    public void Update()
    {
        Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Subject subject = new Subject();

        ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject);
        ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject);

        Console.WriteLine("첫 번째 상태 변화: 15");
        subject.State = 15;
        Console.WriteLine("두 번째 상태 변화: 10");
        subject.State = 10;
    }
}

이 예제에서 Subject 클래스는 주체로, 상태를 변경하고 모든 옵저버에게 알림을 보냅니다. IObserver 인터페이스는 옵저버들이 구현해야 하는 Update 메서드를 정의합니다. ConcreteObserver 클래스는 실제로 상태 변화를 감지하고 반응하는 구체적인 옵저버입니다.

4. 장점

  • 느슨한 결합: 주체와 옵저버 간의 의존성을 최소화하여 시스템의 모듈화와 재사용성을 높입니다. 예를 들어, 옵저버가 변경되어도 주체는 영향을 받지 않습니다.
  • 확장성: 새로운 옵저버를 추가할 때 주체 코드를 수정할 필요가 없습니다. 예를 들어, 주식 가격을 모니터링하는 새로운 클라이언트를 추가할 때 기존 주체 코드를 수정하지 않아도 됩니다.
  • 재사용성: 주체와 옵저버를 각각 독립적으로 재사용할 수 있습니다. 예를 들어, 동일한 주체가 다양한 종류의 옵저버와 함께 사용될 수 있습니다.
  • 이벤트 기반 시스템 구현: UI 이벤트(MVP/MVC), 네트워크 이벤트, 데이터 변경 이벤트 등 다양한 이벤트 기반 시스템을 쉽게 구현할 수 있습니다.

5. 단점

  • 복잡성 증가: 시스템에 많은 옵저버와 주체가 존재할 경우, 상호작용의 복잡성이 증가할 수 있습니다. 예를 들어, 주체가 많은 옵저버에게 알림을 보낼 때 순서나 타이밍 문제가 발생할 수 있습니다.
  • 메모리 누수: 옵저버가 주체에서 제대로 제거되지 않으면 메모리 누수가 발생할 수 있습니다. 예를 들어, 옵저버가 더 이상 필요 없을 때 주체의 리스트에서 제거되지 않으면 메모리 누수가 발생합니다.
  • 예측 불가능한 동작: 옵저버들이 알림을 받는 순서가 예측 불가능할 수 있어, 의도하지 않은 부작용이 발생할 수 있습니다. 예를 들어, 한 옵저버가 다른 옵저버에 의존하는 경우 알림 순서가 중요할 수 있습니다.
  • 디버깅 어려움: 이벤트가 객체에 의해 처리되기 때문에 디버깅과 로그 추적이 어려울 수 있습니다.

결론

옵저버 패턴은 C#에서 객체들 간의 종속성을 느슨하게 관리하기 위한 강력한 도구입니다. 이를 이해하고 장단점을 균형 있게 고려함으로써 유연하고 유지보수하기 쉬운 소프트웨어 시스템을 만들 수 있습니다. 예를 들어, 주식 거래 시스템, 실시간 알림 시스템, 데이터 바인딩이 필요한 UI 애플리케이션 등 다양한 분야에서 유용하게 사용될 수 있습니다.

 

단점 극복 방법

1. 메모리 누수 방지

메모리 누수를 방지하기 위해 약한 참조(Weak Reference)를 사용할 수 있습니다. C#에서는 WeakReference 클래스를 활용해 옵저버에 대한 참조를 약하게 만들어 주체가 옵저버를 강하게 참조하지 않도록 할 수 있습니다.

using System;
using System.Collections.Generic;

// 옵저버 인터페이스
public interface IObserver
{
    void Update();
}

// 주체 클래스
public class Subject
{
    private List<WeakReference<IObserver>> observers = new List<WeakReference<IObserver>>();
    private int state;

    public int State
    {
        get { return state; }
        set
        {
            state = value;
            NotifyAllObservers();
        }
    }

    public void Attach(IObserver observer)
    {
        observers.Add(new WeakReference<IObserver>(observer));
    }

    public void Detach(IObserver observer)
    {
        observers.RemoveAll(weakRef =>
        {
            IObserver target;
            return weakRef.TryGetTarget(out target) && target == observer;
        });
    }

    public void NotifyAllObservers()
    {
        foreach (var weakRef in observers)
        {
            IObserver observer;
            if (weakRef.TryGetTarget(out observer))
            {
                observer.Update();
            }
        }
    }
}

// 구체적인 옵저버 클래스
public class ConcreteObserver : IObserver
{
    private string name;
    private Subject subject;

    public ConcreteObserver(string name, Subject subject)
    {
        this.name = name;
        this.subject = subject;
        this.subject.Attach(this);
    }

    public void Update()
    {
        Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Subject subject = new Subject();

        ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject);
        ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject);

        Console.WriteLine("첫 번째 상태 변화: 15");
        subject.State = 15;
        Console.WriteLine("두 번째 상태 변화: 10");
        subject.State = 10;

        subject.Detach(observer1);

        Console.WriteLine("세 번째 상태 변화: 5");
        subject.State = 5;
    }
}

 

2. 예측 가능한 알림 순서

알림 순서를 예측 가능하게 하려면 옵저버를 추가할 때 우선순위를 지정할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;

public interface IObserver
{
    void Update();
}

public class Subject
{
    private SortedList<int, List<IObserver>> observers = new SortedList<int, List<IObserver>>();
    private int state;

    public int State
    {
        get { return state; }
        set
        {
            state = value;
            NotifyAllObservers();
        }
    }

    public void Attach(IObserver observer, int priority = 0)
    {
        if (!observers.ContainsKey(priority))
        {
            observers[priority] = new List<IObserver>();
        }
        observers[priority].Add(observer);
    }

    public void Detach(IObserver observer)
    {
        foreach (var observerList in observers.Values)
        {
            observerList.Remove(observer);
        }
    }

    public void NotifyAllObservers()
    {
        foreach (var observerList in observers.OrderByDescending(x => x.Key).Select(x => x.Value))
        {
            foreach (var observer in observerList)
            {
                observer.Update();
            }
        }
    }
}

public class ConcreteObserver : IObserver
{
    private string name;
    private Subject subject;

    public ConcreteObserver(string name, Subject subject, int priority = 0)
    {
        this.name = name;
        this.subject = subject;
        this.subject.Attach(this, priority);
    }

    public void Update()
    {
        Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
    }
}

class Program
{
    static void Main()
    {
        Subject subject = new Subject();

        ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject, 2);
        ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject, 1);

        Console.WriteLine("첫 번째 상태 변화: 15");
        subject.State = 15;
        Console.WriteLine("두 번째 상태 변화: 10");
        subject.State = 10;
    }
}

 

3. 디버깅 용이성 향상

디버깅을 쉽게 하기 위해 로깅을 추가할 수 있습니다. 이는 주체와 옵저버의 상태 변화를 추적하는 데 도움이 됩니다.

using System;
using System.Collections.Generic;

public interface IObserver
{
    void Update();
}

public class Subject
{
    private List<IObserver> observers = new List<IObserver>();
    private int state;

    public int State
    {
        get { return state; }
        set
        {
            state = value;
            Console.WriteLine($"주체의 상태가 {state}로 변경되었습니다.");
            NotifyAllObservers();
        }
    }

    public void Attach(IObserver observer)
    {
        observers.Add(observer);
        Console.WriteLine($"{observer.GetType().Name}이(가) 주체에 추가되었습니다.");
    }

    public void Detach(IObserver observer)
    {
        observers.Remove(observer);
        Console.WriteLine($"{observer.GetType().Name}이(가) 주체에서 제거되었습니다.");
    }

    public void NotifyAllObservers()
    {
        foreach (var observer in observers)
        {
            observer.Update();
        }
    }
}

public class ConcreteObserver : IObserver
{
    private string name;
    private Subject subject;

    public ConcreteObserver(string name, Subject subject)
    {
        this.name = name;
        this.subject = subject;
        this.subject.Attach(this);
    }

    public void Update()
    {
        Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
    }
}

class Program
{
    static void Main()
    {
        Subject subject = new Subject();

        ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject);
        ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject);

        Console.WriteLine("첫 번째 상태 변화: 15");
        subject.State = 15;
        Console.WriteLine("두 번째 상태 변화: 10");
        subject.State = 10;

        subject.Detach(observer1);

        Console.WriteLine("세 번째 상태 변화: 5");
        subject.State = 5;
    }
}

 

이와 같이 약한 참조를 사용하거나, 우선순위를 지정하거나, 로깅을 추가하는 등의 방법을 통해 옵저버 패턴의 단점을 효과적으로 극복할 수 있습니다. 이를 통해 시스템의 안정성과 유지보수성을 높일 수 있습니다.

'개발💻 > C#' 카테고리의 다른 글

[C#] readonly와 const의 차이  (0) 2024.10.05
[C#] Coupling, Cohesion 1  (0) 2024.08.10
[C#] Delegate 2  (0) 2024.06.24
[C#] Delegate 1  (0) 2024.06.08
[C#] Using 지시문  (2) 2022.08.24