옵저버(Observer) 패턴
은 JDK에서 가장 많이 쓰이는 패턴 가운데 하나 입니다. 그만큼 정말 유용한 패턴입니다. 정리하기가 쉽지 않아 보이지만.. 최대한 정리를 해보겠습니다.
옵저버 패턴의 예시로 신문 구독하는 것을 들 수 있습니다. 출판사는 주제(subject)
, 구독자를 옵저버(observer)
라고 부르겠습니다.
그림으로 나타내면 위와 같습니다.
- 주제 객체에서 일부 데이터를 관리합니다.
- 주제의 데이터가 달라지면 옵저버한테 그 소식이 전해집니다.
- 데이터가 바뀌면 새로운 데이터 값이 옵저버들에게 전달됩니다.
옵저버 패턴(Observer Pattern)에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의합니다.
일대다 관계
는 주제와 옵저버에 의해 정의됩니다. 주제 하나에 옵저버 여러 개를 가지고 있는 것을 볼 수 있습니다. 위에서 주제는 출판사, 옵저버는 구독자라고 했습니다.
출판사의 어떤 특정한 것이 바뀌면 구독자들에게 알려야 하는 것과 같은 상황입니다. 이렇게 옵저버 패턴은 주제(Subject)
인터페이스와 옵저버(Observer)
인터페이스가 들어있는 클래스 디자인을 바탕으로 합니다.
옵저버 패턴의 클래스 다이어그램은 위와 같습니다.
Subject: 주제를 나타나는 Subject 인터페이스 입니다. 객체에서 옵저버를 등록, 삭제, 알림을 하고 싶을 때 인터페이스에 있는 메소드를 사용하게 됩니다.
ConcreteSubject: 주제 역할을 하는 구상 클래스에서는 항상 Subject 인터페이스를 구현해야 합니다.
ConcreteObserver : Observer 인터페이스만 구현한다면 무엇이든 옵저버 클래스가 될 수 있습니다.
옵저버 패턴에서는 주제(Subject)
와 옵저버(Observer)
가 느슨하게 결합되어 있는 객체 디자인을 제공합니다.
- 주제가 옵저버에 대해서 아는 것은 옵저버가 특정 인터페이스를 구현한다는 것입니다.
- 옵저버는 언제든지 새로 추가할 수 있습니다. (신문 구독을 하는 경우)
- 새로운 옵저버를 추가하려 할 때도 주제를 전혀 변경하지 않아도 됩니다.
- 주제와 옵저버는 서로 독립적으로 재사용할 수 있습니다.
- 주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지는 않습니다.
이렇게 느슨하게 결합
하는 디자인을 사용하면 변경 사항이 생겨도 쉽게 처리할 수 있는 유연함을 가질 수 있습니다.
날씨 데이터를 가지고 있는 회사와 데이터를 연동하여 여러 디스플레이에 날씨 데이터를 출력해줘야 하는 일이 생겼다고 가정하겠습니다.
getTemperature(): 온도
getHumidity(): 습도
getPressure(): 기압
measurementsChanged(): 기상 관측값이 갱신될 때마다 알려주기 위한 메소드
WetherData
클래스에 위와 같은 소스 파일이 날씨 데이터 회사로부터 도착했습니다.
그래서 이것을 기반으로 기상 스테이션
을 설계해보겠습니다.
Subject: 주제 역할을 하는 인터페이스 입니다.
Observer: 주제 객체에서 옵저버한테 갱신된 정보를 전달할 수 있는 update() 메소드를 제공합니다.
DisplayElement: 모든 디스플레이 항목에서 display()를 구현하게 하기 위한 인터페이스 입니다.
CurrentConditions: WetherData 객체로 부터 얻은 현재 측정값들을 화면에 보여줍니다.
StaticsDisplay: 측정치의 최소/평균/최대값을 추천하고 화면에 보여줍니다.
ForecastDisplay: 기상 예보를 화면에 보여줍니다.
public interface Subject {
// Observer를 인자로 받고 주제에 옵저버를 등록
public void registerObserver(Observer o);
// Observer를 인자로 받고 주제에 옵저버를 삭제
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();
}
그리고 위에서 본 WeatherData
클래스를 Subject 인터페이스를 구현해서 아래와 같이 만들었습니다.
public class WeatherData implements Subject {
private List observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}
@Override
public void notifyObservers() {
for (int i = 0; i < observers.size(); ++i) {
Observer observer = (Observer)observers.get(i);
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 class CurrentConditionDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
// 디스플레이에 옵저버를 등록합니다.
public CurrentConditionDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
// 기온과 습도를 저장하고 display()를 호출합니다.
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
// 화면에 출력합니다.
@Override
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
주제와 옵저버 사이에는 내가 데이터를 보내는 방식(푸시 방식)
과 그 쪽에서 데이터를 가져가는 방식(풀 방식)
이 있습니다.
이번에는 자바에 내장된 옵저버 패턴에 대해서 알아볼텐데, 여기에는 두 가지 방식이 존재합니다.
자바에 내장된 java.util.Observer 인터페이스
와 java.util.Observable 클래스
를 사용하면 위와 같이 클래스 다이어그램을 만들 수 있습니다.
여기서 하나 볼 점은 Observable
이 인터페이스가 아니라 클래스이기 때문에 WeatherData
는 implements가 아닌 extends를 해야한다는 점입니다.
import java.util.Observable;
// extends를 하고 있음
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();
}
// 풀 방식을 할 때 옵저버들이 데이터를 가져가기 위해 getter 메소드들을 작성
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
그러면 위와 같이 WeatherData
클래스를 변경하면 됩니다.
import java.util.Observable;
import java.util.Observer;
public class CurrentConditionDisplay implements Observer, DisplayElement {
Observable observable;
private float temperature;
private float humidity;
// 디스플레이에 옵저버를 등록합니다.
public CurrentConditionDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
/**
* 옵저버가 연락을 받는 방법
* @param obs : 연락을 보내는 주제 객체
* @param arg: notifyObservers() 메소드에서 인자로 전달된 데이터 객체, 데이터 객체가 지정되지 않는 경우에 널이 됨
*/
@Override
public void update(Observable obs, Object arg) {
if (obs instanceof WeatherData) {
WeatherData weatherData = (WeatherData)obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
@Override
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
주제 객체가 옵저버 객체들한테 연락을 돌릴 때 순서가 달라졌다고 결과가 달리지면 안됩니다. 순서가 바뀌었다고 결과가 다르다면 느슨한 결합
이라고 할 수 없습니다.
-
- Observable이 클래스기 때문에 서브클래스를 만들어야 한다는 점이 문제가 됩니다. 이미 다른 수퍼클래스를 확장하고 있는 클래스에 Observable의 기능을 추가할 순 없습니다. 그래서
재사용성
의 문제가 생깁니다.
- Observable이 클래스기 때문에 서브클래스를 만들어야 한다는 점이 문제가 됩니다. 이미 다른 수퍼클래스를 확장하고 있는 클래스에 Observable의 기능을 추가할 순 없습니다. 그래서
-
- Observable 클래스를 보면 메소드의 접근 지정자가
protected
로 선언되어 있음을 알 수 있습니다. 그러면 Observable을 상속받은 클래스에서만 호출할 수 있다는 큰 단점이 있습니다.
- Observable 클래스를 보면 메소드의 접근 지정자가