이 글은 [헤드 퍼스트 디자인 패턴]의 2장 옵저버 패턴을 정리한 글입니다.
기상 모니터링 애플리케이션 예제
요구사항
- 현재 기상 조건(온도, 습도, 기압)의 변화를 실시간으로 디스플레이 장비에 반영해야 합니다.
- 갱신된 기상 정보는 WeatherData 객체의 세터 메소드를 통해 WeatherData 내부의 상태를 변화시키는 방식으로 동작한다고 가정합니다.
- 디스플레이 장비는 로직이 다양하며 확장 가능하다는 것을 전제합니다. 현재는 기상 조건, 기상 통계, 기상 예보 세 가지의 디스플레이 장비들이 있습니다.
먼저 나이브하게 WeatherData 클래스를 만들어 보겠습니다. MeasurementChanged 메소드는 getter 메소드를 통해 현재 기상 정보를 가져온 후 각 디스플레이 객체의 update 메소드를 호출합니다.
public class WeatherData {
private double temperature;
private double humidity;
private double pressure;
// 디스플레이 인스턴스 변수 선언 ...
// 생성자 ...
public void measurementChanged(){
// 디스플레이를 업데이트하는 코드
double temp = getTemperature();
double humidity = getHumidity();
double pressure = getPressure();
currentConditiondisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
// getters, setters ...
}
위 코드의 문제점은 다음과 같습니다.
- 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩하고 있습니다.
- 새로운 디스플레이가 추가될 때마다 코드를 변경해야 합니다.
- 실행중에 디스플레이 항목을 추가하거나 제거할 수 없습니다.
- 바뀌는 부분을 캡슐화하지 않았습니다.
이 상황을 해결하기 위해서 옵저버 패턴을 알아본 후 코드에 적용해 보겠습니다.
옵저버 패턴 이해하기
신문 구독 예제
- 신문사가 신문을 발행합니다.
- 독자가 신문 구독 신청을 하면 새로운 신문이 나올 때마다 배달을 받을 수 있습니다. 구독을 해지하기 전까지 신문을 계속 받을 수 있습니다.
- 구독 해지 신청을 하면 더이상 신문이 오지 않습니다.
여기서 신문사를 주체(subject), 구독자를 옵저버(observer)라고 부르면 옵저버 패턴을 이해하기 용이합니다.
- 주제 객체는 중요한 데이터를 관리합니다.
- 옵저버 객체들은 주제를 구독하고 있으며 주제 데이터가 바뀌면 갱신 내용을 전달받습니다.
- 옵저버가 아닌 객체는 주제 데이터가 바뀌어도 아무 연락을 받지 못합니다.
옵저버 패턴(Observer Pattern)은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의합니다.
옵저버 패턴은 보통 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인으로 구현합니다. 기상 모니터링 예제에 옵저버 패턴을 정의해 보겠습니다. 먼저, 주제, 옵저버, 디스플레이 인터페이스를 정의합니다.
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObserver(Observer o);
}
public interface Observer {
public void update(double temperature, double humidity, double pressure);
}
public interface DisplayElement {
public void display();
}
여기서 주요한 점은 주제가 옵저버를 등록하고 알려주는 주체가 된다는 것입니다. 따라서 Subject 인터페이스를 구현하는 WeatherData 객체 안에는 옵저버 리스트를 가지고 있습니다. WeatherData 클래스를 다시 만들어 보겠습니다.
class WeatherData implements Subject {
private double temperature;
private double humidity;
private double pressure;
// 옵저버 리스트
private List<Observer> observers;
// 생성자 - 생성시 빈 옵저버 리스트 초기화
public WeatherData() {
observers = new ArrayList<Observer>();
}
// 옵저버 등록
public void registerObserver(Observer o) {
observers.add(o);
}
// 옵저버 삭제
public void removeObserver(Observer o) {
observers.remove(o);
}
// 등록한 모든 옵저버들에게 알림
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void measurementChanged(){
notifyObservers();
}
// 관측값 업데이트시 기상 정보 상태 변경 후 알림
public void setMeasurements(double temperature, double humidity, double pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
System.out.println("업데이트중...");
measurementChanged();
}
}
옵저버 인터페이스를 구현하는 디스플레이 객체들도 만들어 보겠습니다.
class CurrentConditionDisplay implements Observer, DisplayElement {
private double temperature;
private double humidity;
private double pressure;
private WeatherData weatherData;
public CurrentConditionDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(double temperature, double humidity, double pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
System.out.printf("온도: %f, 습도: %f, 기압: %f\n", temperature, humidity, pressure);
}
}
class StatisticsDisplay implements Observer, DisplayElement {
private double temperature;
private double humidity;
private double pressure;
private WeatherData weatherData;
public StatisticsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(double temperature, double humidity, double pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
System.out.printf("온도 + 습도 + 기압: %f\n", temperature + humidity + pressure);
}
}
class ForecastDisplay implements Observer, DisplayElement {
private double temperature;
private double humidity;
private double pressure;
private WeatherData weatherData;
public ForecastDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(double temperature, double humidity, double pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
System.out.printf("내일 온도: %f, 내일 습도: %f, 내일 기압: %f\n", temperature + 1, humidity + 1, pressure + 1);
}
}
이제 메인 메소드에서 테스트하는 코드를 작성해 보겠습니다.
public class MyClass {
public static void main(String args[]) {
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4);
weatherData.setMeasurements(100, 45, 31.423);
weatherData.setMeasurements(84, 15, 33.4);
}
}
/*
업데이트중...
온도: 80.000000, 습도: 65.000000, 기압: 30.400000
온도 + 습도 + 기압: 175.400000
내일 온도: 81.000000, 내일 습도: 66.000000, 내일 기압: 31.400000
업데이트중...
온도: 100.000000, 습도: 45.000000, 기압: 31.423000
온도 + 습도 + 기압: 176.423000
내일 온도: 101.000000, 내일 습도: 46.000000, 내일 기압: 32.423000
업데이트중...
온도: 84.000000, 습도: 15.000000, 기압: 33.400000
온도 + 습도 + 기압: 132.400000
내일 온도: 85.000000, 내일 습도: 16.000000, 내일 기압: 34.400000
*/
체감 온도 디스플레이를 추가해 보겠습니다. HeatIndexDisplay를 정의한 후, 메인 메소드에서 추가해 주기만 하면 됩니다.
class HeatIndexDisplay implements Observer, DisplayElement {
private double temperature;
private double humidity;
private double pressure;
private WeatherData weatherData;
public HeatIndexDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(double temperature, double humidity, double pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
private double computeHeatIndex(double t, double rh) {
double index = ((16.923 + (0.185212 * t) + (5.37941 * rh) - (0.100254 * t * rh) +
(0.00941695 * (t * t)) + (0.00728898 * (rh * rh)) +
(0.000345372 * (t * t * rh)) - (0.000814971 * (t * rh * rh)) +
(0.0000102102 * (t * t * rh * rh)) - (0.000038646 * (t * t * t)) + (0.0000291583 *
(rh * rh * rh)) + (0.00000142721 * (t * t * t * rh)) +
(0.000000197483 * (t * rh * rh * rh)) - (0.0000000218429 * (t * t * t * rh * rh)) +
0.000000000843296 * (t * t * rh * rh * rh)) -
(0.0000000000481975 * (t * t * t * rh * rh * rh)));
return index;
}
public void display() {
System.out.printf("체감 온도: %f\n", computeHeatIndex(temperature, humidity));
}
}
public class MyClass {
public static void main(String args[]) {
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
// 체감온도 디스플레이 추가
HeatIndexDisplay heatIndexDisplay = new HeatIndexDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4);
weatherData.setMeasurements(100, 45, 31.423);
weatherData.setMeasurements(84, 15, 33.4);
}
}
/*
업데이트중...
온도: 80.000000, 습도: 65.000000, 기압: 30.400000
온도 + 습도 + 기압: 175.400000
내일 온도: 81.000000, 내일 습도: 66.000000, 내일 기압: 31.400000
체감 온도: 82.955351
업데이트중...
온도: 100.000000, 습도: 45.000000, 기압: 31.423000
온도 + 습도 + 기압: 176.423000
내일 온도: 101.000000, 내일 습도: 46.000000, 내일 기압: 32.423000
체감 온도: 114.626305
업데이트중...
온도: 84.000000, 습도: 15.000000, 기압: 33.400000
온도 + 습도 + 기압: 132.400000
내일 온도: 85.000000, 내일 습도: 16.000000, 내일 기압: 34.400000
체감 온도: 79.247060
*/
저희는 WeatherData 객체의 코드를 한줄도 변경하지 않고 HeatIndexDisplay를 추가할 수 있었습니다. 옵저버 패턴을 통해 확장 가능한 기상 모니터링 어플리케이션을 만들 수 있었습니다.
더 생각해 볼 점으로는 푸시와 풀 방식이 있습니다. 현재 어플리케이션은 주제가 모든 상태를 옵저버에게 알려주는 방식을 사용하고 있습니다(푸시 방식). 풀 방식은 옵저버마다 update 메소드 안에서 WeatherData의 getter 메소드를 사용해 필요한 상태값을 각각 가져오도록 구현하는 방식입니다. 기상 조건의 종류와 디스플레이 종류가 다양해지고 변화무쌍하다면 각 옵저버들이 원하는 기상 조건을 가져올 수 있도록 하면 큰 변경 없이 확장할 수 있다고 책은 말하고 있습니다.
또한 옵저버 패턴은 pub/sub 패턴과 친척 관계에 있습니다. pub/sub 패턴은 여러 개의 주제와 메시지 유형이 있는 복잡한 상황에서 사용합니다.
옵저버 패턴은 주제가 옵저버들이 Observer 인터페이스를 구현한다는 것을 제외하면 옵저버에 관해 모릅니다. 이들 사이의 결합은 느슨한 결합입니다.
'디자인패턴' 카테고리의 다른 글
[디자인 패턴] 전략 패턴 (4) | 2023.10.02 |
---|