본문 바로가기

성장하는 중 입니다?/디자인패턴

옵저버 패턴

이 포스트는 한빛미디어 'HeadFirst - Design Pattern' 를 공부하면서 작성되었습니다.

 

 

! 결합을 느슨하게 하기

두 객체가 느슨하게 연결되어 있으면, 상호작용은 할 수 있으나 서로에 대해 아는 것은 아주 적다.

옵저버 패턴은 subject 와 observer들이 느슨하게 결합된 객체 디자인을 제공한다.

 

 

? 왜

subject 가 observer 에 대해 아는 유일한 것은 observer 가 어떤 observer interface 를 구현한다는 사실 뿐이다.

observer 의 concrete class 를 알 필요도 없고, 그게 뭐하는 건지 등등 알 필요가 없다. 

언제든지 새로운 observer 를 추가할 수 있다. subject 가 연연하는 것은 observer interface 를 구현하는 object들의 목록 뿐이라서, 우리는 언제든 새로운 observer 를 추가할 수 있다. 사실은 runtime 중에 어떤 observer를 다른 observer로 교체할 수도 있고, 그래도 subject는 계속해서 말을 건낼 수 있다. 아니면, 언제든 observer 를 제거할 수도 있다.

새로운 타입의 observer 를 추가하기 위해 subject를 고칠 필요가 없다. observer 가 되기 위한 새로운 concrete class 가 있다고 가정해보자. 새로운 class 타입을 수용하려고 subject 를 변경할 필요는 없다. 단지 observer interface 를 새로운 class에 구현하고, observer 를 등록하면 된다. subject 는 observer interface 를 구현하는 어떠한 object 에게든 알람을 전달할 것이다. 

subject 와 observer 는 서로 독립적으로 재사용할 수 있다. 둘이 서로 단단하게 결합되어 있지 않기 때문!

subject 나 observer 의 변경은 나머지에게 영향을 주지 않는다. 두 object 는 느슨하게 연결되어 있기 때문에, object 가 subject interface 또는 observer interface 를 구현하기만 한다면어느것이든 바꾸기가 자유롭다. 

 

*concrete class (구상클래스)

추상클래스를 상속받아 구체화 한 하위 클래스

 

*interface (인터페이스)

ex. FlyBehavior, QuackBehavior

완전히 추상클래스임, class 언급 없이 interface 로 정의하고, implements 로 interface 를 구현할 수 있다. 

ex. public interface FlyBehavior {} / public class FlyNoWay implements FlyBehavior {}

 

 

 

 

그러면 날씨 정보를 표출하기 위한 Weather Cast 프로젝트에서 subject 와 observer 를 구분지어 보자.

 

subject : Weather Station Data

-> subject data - temp, humidity, press / subject methods - regist, remove, update data

observers : Displays (Current/Statistics/Forecast)

-> observer interface : Display / concrete classes : Current/Statisics/Forecast)

 

 

근데 이제 observer 는 subject data 를 업데이트 하는 역할을 수행하고, Display interface 가 display 를 수행하는 것으로 나눈다. 

그리고 구상클래스들은 update 와 display 를 구현한다. 

 

 

 

 

1. 인터페이스 Subject 와 Observer 와 DisplayElement

 

package weatherproject;

public interface Subject {
	public void registerObserver(Observer o);
	public void removeObserver(Observer o);
	public void notifyObservers();
}
package weatherproject;

public interface Observer {
	public void update(float temp, float humidity, float pressure);
}
package weatherproject;

public interface DisplayElement {
	public void display();
}

 

2. Subject 를 구현하는 WeatherData 클래스

 

package weatherproject;

import java.util.ArrayList;

public class WeatherData implements Subject {
	private ArrayList observers;
	private float temperature;
	private float humidity;
	private float pressure;
	
	public WeatherData() {
		observers = new ArrayList();
	}
	
	
	// Subject 인터페이스를 구현 - registerObserver, removeObserver, notifyObservers
	public void registerObserver(Observer o) {
		observers.add(o);
	}
	
	public void removeObserver(Observer o) {
		int i = observers.indexOf(o);
		if(i >= 0)
			observers.remove(o);
	}
	
	// Observer에 측정값이 알려지면 Display 함수 호출
	public void notifyObservers() {
		for (int i = 0; i < observers.size(); i++) {
			Observer observer = (Observer)observers.get(i);
			// update -> display 호출
			observer.update(temperature, humidity, pressure);
		}
	}
	
	
	// 기상 스테이션으로 부터 갱신된 측정값들을 가져와서 Observer 들에게 알림
	public void measurementsChanged() {
		notifyObservers();
	}
	
	// 기상 스테이션으로 부터 갱신된 측정값들을 가져옴
	public void setMeasurements(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}
	
	// 기타 WeatherData 메소드
}

 

3. Observer 와 DisplayElement 를 구현하는 클래스들 - CurrentConditionsDisplay 와 StatisticsDisplay 와 Forecastdisplay

 

package weatherproject;

public class CurrentConditionsDisplay implements Observer, DisplayElement {
	private float temperature;
	private float humidity;
	// Subject 레퍼런스 변수 선언
	private Subject weatherData;
	
	// 생성자 - 이 디스플레이 항목을 Observer 로 등록함
	public CurrentConditionsDisplay(Subject weatherData) {
		// Subject 레퍼런스 변수에 저장 - 나중에 removeObserver 에서 유용할 수 있음 (?)	
		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() {
		System.out.println("Currenet conditions: " + temperature + "F degrees and " + humidity + "% himidity");
	}
}
package weatherproject;

public class StatisticsDisplay implements Observer, DisplayElement {
	private float maxTemp = 0.0f;
	private float minTemp = 200;
	private float tempSum= 0.0f;
	private int numReadings;
	private Subject weatherData;

	public StatisticsDisplay(Subject weatherData) {
		weatherData.registerObserver(this);
	}

	public void update(float temperature, float humidity, float pressure) {
		float temp = temperature;
		tempSum += temp;
		numReadings++;

		if (temp > maxTemp) {
			maxTemp = temp;
		}
 
		if (temp < minTemp) {
			minTemp = temp;
		}

		display();
	}

	public void display() {
		System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
			+ "/" + maxTemp + "/" + minTemp);
	}
}
package weatherproject;

public class ForecastDisplay implements Observer, DisplayElement {
	private float currentPressure = 29.92f;  
	private float lastPressure;
	private Subject weatherData;

	public ForecastDisplay(Subject weatherData) {
		weatherData.registerObserver(this);
	}

	public void update(float temperature, float humidity, float pressure) {
		lastPressure = currentPressure;
		currentPressure = pressure;
		display();
	}

	public void display() {
		System.out.print("Forecast: ");
		if (currentPressure > lastPressure) {
			System.out.println("Improving weather on the way!");
		} else if (currentPressure == lastPressure) {
			System.out.println("More of the same");
		} else if (currentPressure < lastPressure) {
			System.out.println("Watch out for cooler, rainy weather");
		}
	}
}

 

4. 테스트 !

 

package weatherproject;

public class WeatherStation {

	public static void main(String[] args) {
		// Subject의 구상클래스인 WeatherData 의 객체 생성
		WeatherData weatherData = new WeatherData();
		
		// Observer, Display 의 구상클래스들의 객체 생성
		CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
		//StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
		//ForeCastDisplay forecastDisplay = new ForecastDisplay(weatherData);
		
		
		// 날씨 정보가 측정된 것처럼 시뮬레이션
		weatherData.setMeasurements(80, 65, 30.4f);
		weatherData.setMeasurements(82, 45, 29.2f);
		weatherData.setMeasurements(78, 90, 29.2f);
		
	}
}

 

결과는 ..

 

 

 

Observer 인터페이스를 구현한 클래스들의 생성자에서 Subject 레퍼런스를 저장하는데, 나중에 Observer 를 삭제하고 싶을 때 유용하다고 한다. 무슨 말인지 잘 모르겠다 ㅎㅎ

 

* 레퍼런스 타입이란? 자바에서 데이터 타입에는 '기초적인 타입'과 '레퍼런스 타입'이 있다. 기초적인 타입은 우리가 흔히 봐 왔던 int, char, boolean 등이고, 레퍼런스 타입은 클래스 타입/인터페이스 타입/배열 타입/열거 타입 으로 분류할 수 있다.

 

 

 

그런데, Weather-O-Rama 의 CEO 인 자니 허리케인 님이 디스플레이 항목을 추가해 달라고 전화가 왔다고 한다.

체감 온도 디스플레이 항목을 추가할 것이다. 

 

체감 온도란 영어로 heat index 또는 humiture 라고 하고, 기온(T) 과 상대 습도(RH) 를 바탕으로 결정된다고 한다. 

 

heatindex = 어쩌구저쩌구 ..

 

복잡한 식이다. HeatIndexDisplay.java 파일을 만든 다음 heatindex.txt 파일을 복사해 넣으라고 한다.

 

 

 

이러쿵저러쿵 HeatIndexDisplay.java 을 아래와 같이 구현

 

package weatherproject;

public class HeatIndexDisplay implements Observer, DisplayElement {
	private float heatIndex;
	private Subject weatherData;
	
	public HeatIndexDisplay(Subject weatherData) {
		this.weatherData = weatherData;
		weatherData.registerObserver(this);
	}
	
	public void update(float temperature, float humidity, float pressure) {
		float t = temperature;
		float rh = humidity;
		
		heatIndex = (float)
				(
				(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)));
		
		display();
	}
	
	public void display() {
		System.out.println("Heat index is " + heatIndex);
	}
}

 

테스트 클래스에도 이 새로운 항목의 객체를 생성하고 실행한 결과는,

 

 

 

실행된 결과를 보면, 옵저버1 CurrentConditionDisplay 는 온도와 습도만 필요로 한다. 심지어 옵저버2 StatisticsDisplay 는 온도만, 옵저버3 ForecastDisplay 는 기압만을 필요로 한다. 그러나 옵저버들은 온도,습도,기압 세가지를 모두 업데이트 받는다. Subject 가 그냥 다 보내기 때문이다. 그래서 옵저버는 자신이 필요한 정보만을 가져가기를 원했다. 

 

그러면 자바에 내장되어 있는 옵저버 패턴을 이용하면 된다고 한다는데, 다음 시간에 무슨 얘기인지 알아보기로 한다.

 

 

'성장하는 중 입니다? > 디자인패턴' 카테고리의 다른 글

자바 내장 옵저버 패턴  (0) 2021.07.11