프로그래밍/C#

옵저버 디자인 패턴: 객체 간의 유연한 의존성 관리

shimdh 2025. 9. 17. 10:56
728x90

옵저버 디자인 패턴은 객체 간의 일대다 의존성을 정의하여 주체의 상태가 변경될 때 여러 옵저버가 자동으로 통지받고 업데이트될 수 있도록 하는 행동 디자인 패턴입니다. 이 패턴은 애플리케이션의 다양한 구성 요소 간의 일관성을 유지하면서도 이들을 밀접하게 결합하지 않고 관리하고자 할 때 특히 유용합니다.

옵저버 디자인 패턴의 주요 개념

주체(Subject)

주체는 상태를 보유하고 있으며, 옵저버에게 변경 사항을 알리는 객체입니다. 주체는 옵저버를 등록하거나 제거할 수 있는 메서드를 제공합니다.

옵저버(Observer)

옵저버는 주체로부터의 업데이트에 어떻게 반응해야 하는지를 정의하는 인터페이스나 추상 클래스입니다. 옵저버는 주체의 상태 변화에 따라 적절한 행동을 취합니다.

구체 주체(Concrete Subject)

구체 주체는 상태를 유지하고 등록된 옵저버에게 알림을 보내는 주체의 구체적인 구현입니다. 주체의 상태가 변경될 때마다 옵저버에게 알림을 보냅니다.

구체 옵저버(Concrete Observer)

구체 옵저버는 구체 주체로부터의 업데이트에 반응하는 옵저버 인터페이스의 구현입니다. 옵저버는 주체의 상태 변화에 따라 자신의 상태를 업데이트합니다.

728x90

옵저버 디자인 패턴의 장점

  1. 느슨한 결합: 옵저버는 주체와 밀접하게 결합되지 않아 유연성과 유지보수성을 높입니다.
  2. 동적 관계: 런타임에 옵저버를 추가하거나 제거할 수 있어, 사용자 상호작용이나 기타 이벤트에 따라 애플리케이션의 동작을 동적으로 조정할 수 있습니다.
  3. 코드 재사용성: 주체와 옵저버에 대한 공통 인터페이스를 정의함으로써 애플리케이션 전반에 걸쳐 재사용 가능한 구성 요소를 만들 수 있습니다.

실용적인 예: 기상 관측 시스템

기상 관측 시스템에서 여러 디스플레이 유닛(옵저버)이 온도(주체)의 변화가 있을 때마다 그들의 측정값을 업데이트해야 하는 상황을 고려해 보겠습니다.

코드 예제

// 1단계: IObserver 인터페이스 정의
public interface IObserver
{
    void Update(float temperature);
}

// 2단계: ISubject 인터페이스 정의
public interface ISubject
{
    void RegisterObserver(IObserver observer);
    void RemoveObserver(IObserver observer);
    void NotifyObservers();
}

// 3단계: 구체 주체 구현 - WeatherStation
public class WeatherStation : ISubject
{
    private List<IObserver> _observers;
    private float _temperature;

    public WeatherStation()
    {
        _observers = new List<IObserver>();
    }

    public void RegisterObserver(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        _observers.Remove(observer);
    }

    public void NotifyObservers()
    {
        foreach (var observer in _observers)
        {
            observer.Update(_temperature);
        }
    }

    // 온도 변화를 시뮬레이션하는 메서드
    public void SetTemperature(float temperature)
    {
        _temperature = temperature;
        NotifyObservers(); // 새로운 온도에 대해 모든 등록된 옵저버에게 알림
   }
}

// 4단계: 구체 옵저버 구현 - 디스플레이 유닛
public class CurrentConditionsDisplay : IObserver
{
   private float _temperature;

   public void Update(float temperature)
   {
       this._temperature = temperature;
       Display();
   }

   public void Display()
   {
       Console.WriteLine($"현재 상태: {_temperature}°C");
   }
}

public class StatisticsDisplay : IObserver
{
   private float _temperatureSum;
   private int _numReadings;

   public void Update(float temperature)
   {
       this._temperatureSum += temperature;
       this._numReadings++;
       Display();
   }

   public void Display()
   {
       Console.WriteLine($"평균 온도: {_temperatureSum / _numReadings}°C");
   }
}

// 5단계: 옵저버 패턴 사용
class Program 
{
     static void Main(string[] args) 
     {  
         var weatherStation = new WeatherStation();

         var currentDisplay = new CurrentConditionsDisplay();
         var statisticsDisplay = new StatisticsDisplay();

         weatherStation.RegisterObserver(currentDisplay);   
         weatherStation.RegisterObserver(statisticsDisplay);

         // 온도 변화 시뮬레이션         
         weatherStation.SetTemperature(25.0f); // 두 디스플레이에 알림 
         weatherStation.SetTemperature(30.0f); // 두 디스플레이에 알림          
     }  
}

이 예제에서:

  1. IObservable을 주체의 계약으로 정의하여 옵저버를 관리합니다.
  2. WeatherStation은 이 계약을 구현하여 등록된 옵저버 목록을 유지하고 내부 상태(현재 온도)에 변화가 있을 때마다 옵저버에게 알립니다.
  3. 두 개의 구체적인 옵저버 클래스(CurrentConditionsDisplayStatisticsDisplay)는 온도 변화에 대한 알림에 반응하는 로직을 구현합니다.

이 구조를 사용함으로써 데이터 소스(WeatherStation)와 디스플레이 구성 요소 간의 느슨한 결합을 달성하여 기존 코드를 크게 수정하지 않고도 기능을 확장하기 쉽게 만듭니다.

결론

옵저버 디자인 패턴은 서로 밀접하게 결합되지 않고도 동기화가 필요한 다양한 부분이 있는 확장 가능한 애플리케이션을 구축하는 데 필수적입니다. 고급 C#에서 이 패턴은 명확하게 정의된 인터페이스와 관심사의 분리를 통해 복잡성을 효과적으로 관리하고 더 깨끗한 아키텍처를 촉진합니다.

728x90