오늘은 디자인 패턴, 그 중에서도 옵저버 패턴에 대해 이야기해보도록 하겠습니다.
이번에는 '옵저버 패턴'에 대해 알아보려고 합니다.
객체 사이의 일대다(One-to-Many) 의존성을 정의하는 패턴입니다.
주제(Subject) 객체의 상태가 변할 때, 이 주체에 의존하는 여러 개의 옵저버(Observer) 객체에 알림을 보내 업데이트를 할 수 있게 하는 패턴입니다.
기상 모니터링 애플리케이션을 만든다고 생각해봅시다. 다음 그림과 같이 여러 센서들을 통해 실제 기상 정보를 수집하는 기상 스테이션(물리적인 장비), 해당 스테이션으로부터 데이터를 얻는 WeatherData 클래스, 각각의 데이터를 요구에 맞춰 사용자에게 보여주는 디스플레이 장비로 구성되어 있습니다.
온도, 습도, 기압이 변하면 디스플레이를 업데이트 해야한다는 것이 핵심입니다.
public class WeatherData
{
private float temperature;
private float humidity;
private float pressure;
private CurrentConditionDisplay currentConditionDisplay;
private StatisticsDisplay statisticsDisplay;
private ForecastDisplay forecastDisplay;
public WeatherData()
{
currentConditionDisplay = new CurrentConditionDisplay();
statisticsDisplay = new StatisticsDisplay();
forecastDisplay = new ForecastDisplay();
}
public void MeasurementsChanged()
{
// 온도, 습도, 기압 데이터 획득
float temp = GetTemperature();
float humidity = GetHumidity();
float pressure = GetPressure();
// 디스플레이 업데이트
currentConditionDisplay.Update(temp, humidity, pressure);
statisticsDisplay.Update(temp, humidity, pressure);
forecastDisplay.Update(temp, humidity, pressure);
}
// 각 데이터의 getter 메서드들
public float GetTemperature()
{
return temperature;
}
public float GetHumidity()
{
return humidity;
}
public float GetPressure()
{
return pressure;
}
// 기타 메서드
}
앞서 말한 점을 바탕으로 WeatherData 클래스를 구현해보았습니다. 이렇게 구현하게 되면 문제가 있습니다. MeasurementsChanged()를 확인해봅시다. WeatherData 클래스와 Display 클래스들이 강하게 결합되어 유지보수와 확장이 어렵습니다.
예를 들자면 새로운 Display 클래스를 추가하거나 기존 클래스를 삭제하기 위해선 WeatherData 클래스의 꽤 많은 부분을 수정해야한다는 것을 알 수 있습니다.
이를 옵저버 패턴을 이용해서 해결해보고자 합니다.
다음과 같이 주제 인터페이스를 구현한 구상 클래스(ConcreteSubject)는 여러 옵저버의 구상 클래스(ConcreteObserver)를 목록에 추가(register), 삭제(remove)할 수 있으며 주제의 데이터가 변경되면 목록의 추가되어 있는 옵저버들에게 알림(notify)을 보낼 수 있습니다.
가상 모니터링 애플리케이션에는 다음과 같이 적용할 수 있습니다. Weather 클래스는 주제 인터페이스가 됩니다. 그리고 각 디스플레이는 옵저버 인터페이스와 디스플레이 인터페이스가 존재합니다.
옵저버 인터페이스가 있다면 어떤 클래스든 옵저버가 될 수 있기 때문에 새로운 디스플레이를 추가하는 것이 어렵지 않습니다. WeatherData와 디스플레이들의 결합을 약하게 만들었다고 할 수 있죠.
public interface ISubject
{
void RegisterObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers();
}
using System.Collections.Generic;
public class WeatherData : ISubject
{
private List<IObserver> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData()
{
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, humidity, pressure);
}
}
public void MeasurementsChanged()
{
NotifyObservers();
}
// 기상 스테이션에서 기상 변경 시, 자동으로 호출하는 메서드
public void SetMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
MeasurementsChanged();
}
}
public interface IObserver
{
void Update(float temperature, float humidity, float pressure);
}
public interface IDisplay
{
void Display();
}
public class CurrentConditionsDisplay : IObserver, IDisplay
{
private float temperature;
private float humidity;
private WeatherData weatherData;
public CurrentConditionsDisplay(WeatherData weatherData)
{
this.weatherData = weatherData;
weatherData.RegisterObserver(this);
}
public void Update(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
Display();
}
public void Display()
{
Console.WriteLine("Current conditions: " + temperature + "°C and " + humidity + "% humidity);
}
}
public class StatisticsDisplay : IObserver, IDisplay
{
private float maxTemperature = float.MinValue;
private float minTemperature = float.MaxValue;
private float temperatureSum = 0;
private int numReadings = 0; // 측정 횟수
private WeatherData weatherData;
public StatisticsDisplay(WeatherData weatherData)
{
this.weatherData = weatherData;
weatherData.RegisterObserver(this);
}
public void Update(float temperature, float humidity, float pressure)
{
temperatureSum += temperature;
numReadings++;
if (temperature > maxTemperature)
{
maxTemperature = temperature;
}
if (temperature < minTemperature)
{
minTemperature = temperature;
}
Display();
}
public void Display()
{
Console.WriteLine("Avg/Max/Min temperature = " + (temperatureSum / numReadings) + "°C / " + maxTemperature + "°C / " + minTemperature + "°C");
}
}
public class ForecastDisplay : IObserver, IDisplay
{
private float lastPressure;
private float currentPressure = 29.92f; // 표준 대기압(초기 값)
private WeatherData weatherData;
public ForecastDisplay(WeatherData weatherData)
{
this.weatherData = weatherData;
weatherData.RegisterObserver(this);
}
public void Update(float temperature, float humidity, float pressure)
{
lastPressure = currentPressure;
currentPressure = pressure;
Display();
}
public void Display()
{
if (currentPressure > lastPressure)
{
Console.WriteLine("Improving weather on the way!");
}
else if (currentPressure == lastPressure)
{
Console.WriteLine("More of the same");
}
else
{
Console.WriteLine("Watch out for cooler, rainy weather");
}
}
}
class Program
{
static void Main(string[] args)
{
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
// 측정값 변경 및 알림
weatherData.SetMeasurements(25.5f, 60.0f, 1013.2f);
// Current conditions: 25.5°C and 60% humidity
// Avg/Max/Min temperature = 25.5°C / 25.5°C / 25.5°C
// More of the same
weatherData.SetMeasurements(30.0f, 65.2f, 1012.0f);
// Current conditions: 30.0°C and 65.2% humidity
// Avg/Max/Min temperature = 27.75°C / 30.0°C / 25.5°C
// Watch out for cooler, rainy weather
}
}
앞서 잠깐 언급했듯이 주제와 옵저버 간의 결합을 느슨하게 만드는 것이 옵저버 패턴의 핵심입니다.
주제는 옵저버의 존재는 알지만 구체적인 옵저버 클래스에 대한 정보는 알지 못합니다.
그래서 특별한 작업없이 새로운 옵저버를 쉽게 추가하거나 삭제할 수 있고 데이터의 변경도 단순히 알림을 주기만 하면 됩니다.
느슨한 결합은 객체지향 프로그래밍의 중요한 지향점 중 하나입니다. 이를 활용해서 우리는 더욱 확장성있는 코드를 구현할 수 있게 됩니다.
현재 CurrentConditionsDisplay 클래스의 Update 함수를 보면 불필요한 정보를 인자로 받고 있는 걸 알 수 잇습니다.
옵저버들이 데이터가 변경될 때 필요한 데이터만 가져올 수 있도록 조금 더 일반화할 수 있습니다.
public interface Observer
{
public void update();
}
public class WeatherData : ISubject
{
private List<IObserver> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData()
{
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();
}
}
public void MeasurementsChanged()
{
NotifyObservers();
}
public void SetMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
MeasurementsChanged();
}
public float GetTemperature()
{
return temperature;
}
public float GetHumidity()
{
return humidity;
}
public float GetPressure()
{
return pressure;
}
}
public class CurrentConditionsDisplay : IObserver, IDisplay
{
private float temperature;
private float humidity;
private ISubject weatherData;
public CurrentConditionsDisplay(ISubject weatherData)
{
this.weatherData = weatherData;
weatherData.RegisterObserver(this);
}
public void Update()
{
temperature = weatherData.GetTemperature();
humidity = weatherData.GetHumidity();
Display();
}
public void Display()
{
Console.WriteLine("Current conditions: " + temperature + "°C and " + humidity + "% humidity");
}
}