by-nc-sa     개발자, DBA가 함께 만들어가는 구루비 지식창고!

2. 옵저버 패턴




1. 옵저버 패턴의 정의

옵저버 패턴(Observer Pattern)에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 자동으로
갱신되는 방식으로 일대다(one-to-many)의존성을 정의 한다.

*한 객체의(주제) 상태가 바뀌면 그 객체에 의존하는 다른객체들(옵저버)에게 연락이 간다.
*주제 이면서 옵저버 일수도 있다.(책에서는 출판사(주제)와 구독자(옵저버)를 예로 들었는데..구독자 이면서 동시에 출판사 일수도 있다.)

1. 옵저버 패턴의 조건.

1) 옵저버 패턴에서 상태를 저장하고 있는 것은 주제 책체다. 옵저버는 이 상태를 사용은 하지만 반드시 가지고 있어야 하는갓은 아니다. 되도록이면 서로간에 결합도는 떨어뜨리고 상태를 통하여 옵저버가 주제에 의존적인 성질을 가지게 한다.

2) 첫째도 느슨한 결합 두째도 느슨한 결합. 아주 일반적인 상식이다.
객체간의 결합도가 높아질수록 수정 유지보수는 아주 힘든 작업이 된다(스트레티지 패턴에서 상속 보다는 위임을 쓰면서 메소드만 가진 객체를 분리 하였듯이(인터페이스)). 그래서 MVC 패턴도 그렇지만(이건 디자인 차원이 아니라 아키텍쳐 수준의 패턴이지만) 일단 객체간의 결합도는 낮추는게 일반론이다.
책에서 설명하는 느슨한 결합의 장점을 뽑으라면 일단.

-옵저버를 언제든지 추가할수 있다. ( )
-새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요없다()
-주제와 옵저버를 서로 독립적으로 재사용할수 있다.
-주제나 옵저버가 바뀌더라도 서로에게 영향을 미치지 않는다.()

위 예제는 여러개로 나누어 했지만 결국 지문 자체에서 전달하고자 하는 내용은 서로 연결되어 있다. 요약하자면
결합도를 낮추어라.(확장하기 용이 하게 수정은 최대한 간단히)

2. 패턴이 적용된 예제.

우선 패턴을 적용할 예제에 대해서 알아보자.
기상모니터링 애플리케이션을 구현 하려 한다. 이 시스템은 다음과 같이 3개의 요소로 이루어져 있고(기상스테이션, WeatherData, 디스플레이 장비)
우리는 WeatherData 를 통해서 여러개(3개 혹은 그이상의 추가 디스플레이장비)의 디스플레이 장비에 데이터를 보여 주게 된다.

위 가상 스테이션 애플리케이션을 옵저버 패턴으로 구현 하기 위해 분석을 하여 보자.
1. 옵저버 패턴의 정의를 떠올리면 one to many 관계 이므로 one = WeatherData 이고 many = Display장치 가된다는걸 알수있다.
2. WeatherData 객체를 주제 객체로 하고 디스플레이 장치를 옵저버로 하는경우 디스플레이장치 에서 자기가 원하는 정보를 얻기 위해 WeatherData객체에 등록을 할수 있는 메소드가 필요한다.
3. 모든 디스플레이 장치는 항목이 다를수 있다. 그러므로 바로 이부분이 공통 인터페이스를 이용해서 처리가 가능 하다. 각 장치마다 구성요서(어떤 장치는 온도 습도 어떤 장치는 날씨)의 형식이 달라도
똑같은 인터페이스를 구현해야만 WeatherData객체에서 가상스테이션에서 언더온 측정값을 보낼수 있다.
4. 즉 모든 디스플레이 장치는 WeatherData에서 호출할수 있는 update()메소드가 필요하다. 그리고 이 update()메소드는 모든 장치들이 구현하는 공통인터페이스에서 정의 되야 한다.

2.1 가상 스테이션 애플리케이션의 사용자 구현 옵저버 패턴

위의 분석 결과를 적용한 UML을 확인해 보고 코드를 작성 하자.

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

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

public interface DisplayElement {
	public void display();
}
import java.util.*;

public class WeatherData implements Subject {
	private ArrayList observers;
            '
            '
            '
	
	public WeatherData() {
		observers = new ArrayList();
	}
	
	@SuppressWarnings("unchecked")
	public void registerObserver(Observer o) {
		observers.add(o);
	}
	
	public void removeObserver(Observer o) {
		int i = observers.indexOf(o);
		if (i >= 0) {
			observers.remove(i);
		}
	}
	
	public void notifyObservers() {
		for (int i = 0; i < observers.size(); i++) {
			Observer observer = (Observer)observers.get(i);
			observer.update(temperature, humidity, pressure);
		}	
	}

	public void notifyObservers(List observerList) {
		Iterator iter=observerList.iterator();
		while(iter.hasNext()){
			Observer observer = (Observer)iter.next();
			observer.update(temperature, humidity, pressure);			
		}	
	}
		
	public void measurementsChanged() {		
		List observerList = observers.subList(0, 2); 		
		notifyObservers();
//		notifyObservers(observerList);
	}

	
	public void setMeasurements(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}
	
	// other WeatherData methods here
           '
           '
           '	
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {
	private float temperature;
	private float humidity;
	private Subject weatherData;
	
	public CurrentConditionsDisplay(Subject 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() {	... }
}

public class WeatherStation {

	public 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(80, 65, 30.4f);
		weatherData.setMeasurements(82, 70, 29.2f);
		weatherData.setMeasurements(78, 90, 29.2f);
	}
}

2.2 가상 스테이션 애플리케이션의 자바내장 옵저버 패턴

위와 동일한 역활을 하는 프로그램을 이번에는 자바의 내장된 옵저버패턴을 사용해서 구현해 보자.

public class WeatherData extends Observable {
	private float temperature;
	private float humidity;
	private float pressure;
	
	public WeatherData() { }
	
	public void measurementsChanged() {
		setChanged();
		notifyObservers();
	}
	
	public void setMeasurements(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}
	
	public float getTemperature() {...}	
	public float getHumidity() {...}	
	public float getPressure() {return pressure;}
}

public class CurrentConditionsDisplay implements Observer {
	Observable observable;
	private float temperature;
	private float humidity;
	
	public CurrentConditionsDisplay(Observable observable) {
		this.observable = observable;
		observable.addObserver(this);
	}
	
	public void update(Observable obs, Object arg) {
		if (obs instanceof WeatherData) {
			WeatherData weatherData = (WeatherData)obs;
			this.temperature = weatherData.getTemperature();
			this.humidity = weatherData.getHumidity();
			display();
		}
	}
	
	public void display() {...}
}

자바의 내장된 옵저버패턴을 이용하므로서 얻어지는 장점과 단점

장점
1.Subject(주제) 쪽에서 일방적으로 데이터를 보내는 (푸시방식)방식 많이 아니라 Observer(옵저버)쪽에서 데이터를 가져가는 (풀방식)방식을 선택해서 사용할수 있다.

  • 풀 방식의 처리가 좀더 권장되어진다. 이유는 수많은 옵저버가 필요로 하는걸 모두 파악하기란 자원의 낭비가 심하기 때문이다. 풀방식으로 처리된다면 등록된 옵저버중에 필요한 내용만 가져갈수도 있고, 만약 주제가 확장되어 상태가 몇개가 추가된다고 하더라도 옵저버에 갱신된 상태를 전달하기 위한 메소드를 일일히 고칠 필요 없이 게터 메소드 하나만 추가 하고 필요한 옵저버가 필요한 상태만 가지기 처리 할수 있기 때문이다.

2.setChanged 메소드를 통하여 옵저버 메소드들을 갱신 하는 방법에 있어 좀더 유연성을 가질수 있다.(온도가 변경될때 1도 이상 변경될때만 디스플레이 장치에 값이 전달되야 할경우)

3.객체 생성자에 관련 옵저버들을 처리 하기 위한 자료 구조를(스택, 큐 구조 또는 arrayList를 이용한 데이터 구조) 만들 필요가 없다.

  • 일단 스택 큐 언급은 했으나 옵저버 패턴을 이용한 처리에서 연락이 가는 순서에 의존한 처리는 잘못된 처리 방식 이므로 굳이 스택 이나 큐같은 방식이 아니라 set 이나 리스트 혹은 map 과같이 어떤 자료구조를 쓰더라도 상관은 없겠다. 단 순서에 의존한 처리만 하지 않는다면 말이다.

일단 위에 기술한 모든 내용은 사용자가 직접 옵저버 패탠을 구현하여 처리 해도 얼마든지 구현이 가능 하지만 자바 내에서 제공해주는 api를 이용하여 좀더 편해진다는게 사용 이유라면 이유다.

단점
1. 일단 가장치명적인 단점은 Observable은 인터페이스가 아닌 클래스 라서 서브 클래스를 만들어야 하고
2. 다른 수퍼 클래스를 상속받아 확장하고 있는경우 Observable의 기능을 추가 할수 없다. 그러므로 재사용성이 급격이 제약된다.(패턴의 사용이유는 재사용성의 증가인데..목적을 상실했다.)
3. setChanged 메소드가 protected로 선언되어 Observable 클래스를 상속받은 서브 클래스에서만 호출이 가능 하다. 이 이야기는 인스턴스 변수로 사용할수 없다는 이야기다.
(인스턴스 변수가 먼지 모른다고 하면 ... OTL 이다 스트레티지 패턴에 잘 나와 있으니 먼지 찾아 보길 바란다.) 결국 이러한 디자인은 상속보다는 구성을 사용한다는 근본 원칙에도 위배 된다.

그렇지만 이러한 단점을 가지었음에도 Observable을 확장한 클래스를 쓸수 있는 상활이라면 쓰면 좋을 거다.

3. 도난 방지 장치 시스템을 옵저버 패턴으로 구현해 보자.

이번에 구현해볼 내용은 도난 방지 프로그램이다. 지금껏 배운 옵저버팬턴을 적용할 예제를 떠올려 보니 이게 바로 떠올랐다.(사실은 다들 알다시피 내집이 털렸었따. 그래서 이게 바로 떠오르더라 )

도난 방지 프로그램이란건 집에 도둑이 들어 왔을? 경보를 발생하는 시스템이다. 담벼락이나 창문 대문 등에 센서를 달아 도둑을 감지 하면 온 집안의 전등을 켜며 경보음을 울려 도둑이 도망가게 하는 프로그램이다. 레벨은 1부터 10까지 있고 낮은 레벨의 경우 경보음이 작게 높은레벨은 큰 경보음을 내며 불이 켜지는 기능을 한다.

고려되야 할점은 현재의 경보시스템은 집안에 불을 켜고 경보음만 발생 하지만 나중에는 좀더 많은 조치가 취해 질수 있다. 경찰서나 이웃에게 도둑이 왔다는 신호를 보낸다던지 아니면 핸드폰으로 전화연락이 된다던지 하는 기능이 추가 될수도 잇다. 이런 변화를 쉽게 수용하게 프로그램을 설계 해야 한다.(마치 위 기상 스테이션 예제에서 디스플레이 장치가 늘어날 경우를 대비한 확장성을 고려해서 프로그램을 설계해야 한다는 소리다)

3.1 디자인 패턴이 적용이 되지 않은 도난방지 어플리케이션

class SensorSystem { 
	Lighting lighting;
	Buzzer buzzer;	
	
	public SensorSystem() {
		lighting = new Lighting();
		buzzer = new Buzzer();		
	}
	
	public void giveTheAlarm(String alarmSourceName, int level) {
		lighting.alarm(alarmSourceName, level);
		buzzer.alarm(alarmSourceName, level);				
	}		
}

class Lighting  {
	public void alarm(String alarmSourceName, int level) {
		System.out.println(level + "만큼 불들을 킨다.");
		System.out.println(alarmSourceName + "쪽의 불들을 반드시 킨다.");
	}
}

class Buzzer  {
	public void alarm( String alarmSourceName, int level) {
		System.out.println(level + "만큼 경보음을 발생한다.");
		System.out.println(alarmSourceName + "쪽의 경보장치 볼륨을 최대한 높인다.");
	}
}


public class ObserverDemoBefore {
	public static void main(String[] args) {
		SensorSystem ss = new SensorSystem(); 

		ss.giveTheAlarm("대문", 5); // "대문"에서 5 수준의 경보가 발생 
	}
}

음 코드는 간결하다. 그렇다 좋다 정말좋은가?? 문제제기를 해보자 스트레티지패턴에서 하던대로 무언가 하나를 추가해 보자. 여기다가 경찰서에 전화하는 기능을 넣어보자
전화하는 기능을 넣게되면 일단 SensorSystem 의 생성자에 변화가 온다. 그리고 속성에도 변화가 온다. 그리고 마지막으로 giveTheAlarm()또한 변경해야 한다.
단지 기능하나를 추가하는데...이런문제가 발생하게 되었다. 다음예제를 통해서 패턴을 적용할경우 어떻게 문제가 풀려나가는지 살펴보자

3.2 디자인 패턴이 적용된 도난방지 어플리케이션

interface AlarmListener {
	public void alarm(SensorSystem sensorSystem);
}
class Lighting implements AlarmListener {
	public void alarm(SensorSystem sensorSystem) {
		System.out.println(sensorSystem.getLevel() + "만큼 불들을 킨다.");
		System.out.println(sensorSystem.getAlarmSourceName() + "쪽의 불들을 반드시 킨다.");
	}
}
class Buzzer implements AlarmListener {
	public void alarm(SensorSystem sensorSystem) {
		System.out.println(sensorSystem.getLevel() + "만큼 경보음을 발생한다.");
		System.out.println(sensorSystem.getAlarmSourceName() + "쪽의 경보장치 볼륨을 최대한 높인다.");
	}
}

class SensorSystem {
	private String alarmSource;
	private int level =0; // 경보의 수준을 나타내는 값. 0부터 10까지의 범위
	private Vector listeners = new Vector();
	
	public void register(AlarmListener al) {
		listeners.addElement(al);
	}
	
	public void giveTheAlarm(String alarmSource,int level) {
		this.alarmSource = alarmSource;
		this.level = level;
		
		notifyObservers();
	}
		
	public void notifyObservers() {		
		for (Enumeration e = listeners.elements(); e.hasMoreElements();)
			((AlarmListener) e.nextElement()).alarm(this);
	}	
	
	public int getLevel() {
		return level;
	}
	
	public String getAlarmSourceName() {
		return alarmSource;
	}
	
}

import java.util.Vector;
import java.util.Enumeration;

public class ObserverDemoAfter {
	public static void main(String[] args) {
		SensorSystem ss = new SensorSystem();
		
		ss.register(new Buzzer());
		ss.register(new Lighting());		
		
		ss.giveTheAlarm("대문", 5); // "대문"쪽에서 센서가 반응하였으며, 경보수준을 5로 정한다.
	}
}

음.. 아름답다. 위코드에 경찰서로 전화 하는 기능을 추가해 본다고 하자. 그냥

class CopCall implements AlarmListener {
	public void alarm(SensorSystem sensorSystem) {
		System.out.println(sensorSystem.getLevel() + "만큼 경보발생 경찰에 알린다.");
	}
}

메인 코드는 다음에 
ss.register(new Buzzer());
ss.register(new Lighting());		
ss.register(new CopCall()); // 이거 한줄만 추가 하면 된다.

이처럼 만들면 된다. 물론 이코드 또한 개선의 여지가 있다 무엇인지 알아 보면 다음과 같다.
일단 따로 상속받을 SuperClass가 없으므로 자바내장옵저버패턴을 이용하면 훨씬 깔끔한 코드를 얻을수 있다. 그장점으로는 다음이 있겠다.
1. 풀방식의 처리가 가능해 진다.(무언지 모르겠으면 다시 처음부터 꼼꼼히 읽어 보자.)
2. setChanged 메소드를 통해서 일정수준 이상의 경고가 아니면 경찰서에는 연락안할수도 있게 좀더 유연하게 처리 할수 있다.
3. 이 예제에서처럼 Vector를 이용한 처리를 할필요가 없게된다(자료구조가 필요없다)

이 예제는 직접작성해 보기 바란다. 정말 쉽다

위 도난 예제는 다음과 같이 처리 할수도 있다

package headfirst.observer.thief;
public interface Subject {
	public void registerAlarmListener(AlarmListener o);
	public void removeAlarmListener(AlarmListener o);
	public void notifyAlarmListener();
}

public interface AlarmListener {
	public void alarm(String alarmSource, int level);
}

import java.util.Enumeration;
import java.util.Vector;

public class SensorSystem implements Subject {
	
	private String alarmSource;
	private int level =0; // 경보의 수준을 나타내는 값. 0부터 10까지의 범위
	private Vector<AlarmListener> listeners;
	
	public SensorSystem(){
		listeners = new Vector<AlarmListener>();
	}
	
	public void registerAlarmListener(AlarmListener al) {
		listeners.addElement(al);
		//listeners.add(al) 리턴이(boolean) 있다 즉 add메소드를 사용함이 좀더 유연성 있게 프로그램이 가능 하다.
	}

	public void removeAlarmListener(AlarmListener al) {
		int i = listeners.indexOf(al);
		if (i >= 0) {
			listeners.remove(al);
		}
	}	

	public void notifyAlarmListener() {		
		for (Enumeration e = listeners.elements(); e.hasMoreElements();)
			((AlarmListener) e.nextElement()).alarm(alarmSource, level);

		//Enumeration enums=listeners.elements();		
		//while(enums.hasMoreElements()){
		//	((AlarmListener) e.nextElement()).alarm(alarmSource, level);
		//}		
	}	

	public void giveTheAlarm() {		
		notifyAlarmListener();
	}

	public void setTheAlarm(String alarmSource,int level) {		
		this.alarmSource = alarmSource;
		this.level = level;
		giveTheAlarm();
	}
		
	
	public int getLevel() {
		return level;
	}
	
	public String getAlarmSourceName() {
		return alarmSource;
	}
}

public class Lighting implements AlarmListener {

	@SuppressWarnings("unused")
	private Subject sensorSystem; 

	public Lighting(Subject sensorSystem){
		this.sensorSystem = sensorSystem;
		sensorSystem.registerAlarmListener(this);	
	}

	public void alarm(String alarmSource, int level) {
		System.out.println(alarmSource + "만큼 불들을 킨다.");
		System.out.println(level + "쪽의 불들을 반드시 킨다.");
	}
}

public class Buzzer implements AlarmListener {

	@SuppressWarnings("unused")
	private Subject sensorSystem; 

	public Buzzer(Subject sensorSystem){
		this.sensorSystem = sensorSystem;
		sensorSystem.registerAlarmListener(this);	
	}

	public void alarm(String alarmSource, int level) {
		System.out.println(level + "만큼 경보음을 발생한다.");
		System.out.println(alarmSource + "쪽의 경보장치 볼륨을 최대한 높인다.");
	}

}

문서에 대하여

  • 이 문서를 다른 블로그나 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^^

문서정보

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.