헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.
옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
실제 기상 정보를 수집하는 장비인 기상 스테이션과 기상 스테이션으로부터 오는 데이터를 추적하는 객체인 WeatherData, 그리고 사용자에게 현재 기상 조건을 보여주는 디스플레이, 세 요소로 이루어진다.
WeatherData 객체에서는 기상 스테이션으로부터 데이터를 가져올 수 있다. 데이터를 가져온 후에는 디스플레이 장비에 세 가지 항목을 표시할 수 있다.
WeatherData 객체를 사용하여 디스플레이 장비에서 위의 3가지 요소를 갱신해 가면서 보여주는 애플리케이션을 만들어보자
measurementsChanged
메소드를 현재 조건, 기상 통꼐, 기상 예측 3가지 디스플레이를 갱신할 수 있도록 구현해야 한다.public class WeatherData {
// 인스턴스 변수 선언
public void measurementsChanged() {
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
}
위의 코드의 문제는 무엇일까??
구체적인 구현에 맞춰서 코딩되어 있기 때문에 코드를 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없고 디스플레이에 항목들을 갱신하여 업데이트하는 부분들은 바뀔 수 있는 부분이므로 캡슐화해야 한다.
이제 옵저버 패턴에 대해서 알아보자
쉽게 생각해서 신문 구독 메커니즘과 비슷한다. 즉 출판사 + 구독자 = 옵저버 패턴
인 것
출판사를 주제 or 주체(subject), 구독자를 옵저버(observer)라고 부른다.
옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
Subject 인터페이스
ConcreteSubject
Observer 인터페이스
ConcreteObserver
옵저버 패턴에서 상태를 저장하고 지배하는 것은 subject 객체이다. 따라서 상태가 들어있는 객체는 하나만 존재하고, 옵저버는 반드시 상태를 가지고 있어야 하는 것은 아니기 때문에 옵저버는 여러 개가 있을 수 있으며 subject 객체에서 상태가 바뀌었다는 알려주기를 기다리는, subject에 의존적인 성질을 가진다.
따라서 하나의 subject와 여러개의 observer가 연관된 일대다 관계가 성립하고 해당 의존을 통해 여러 객체에서 동일한 데이터를 제어하도록 할 수 있다.
추가로 옵저버 패턴의 장점은 subject와 observer가 느슨하게 결합되어 있는 디자인을 제공하는 것이다. 두 객체가 느슨하게 결합되어 있다는 것은, 해당 객체들이 상호작용을 하지만 서로에 대해 잘 모른다는 것을 의미한다.
subject가 observer에 대해 아는 것은 해당 옵저버가 observer 인터페이스를 구현한다는 것 뿐이다
observer는 언제든지 추가 가능하다
새로운 형식의 observer를 추가하려고 할 때도 subject를 변경할 필요가 없다.
subject와 observer는 서로 독립적으로 재사용할 수 있다.
subject와 observer에 변경이 생겨도 서로에게 영향이 미치지 않는다.
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObserver();
}
public interface Observer {
void update(float temperature, float humidity, float pressure);
}
public interface DisplayElement {
void display();
}
import java.util.ArrayList;
import java.util.List;
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
this.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 notifyObserver() {
observers.forEach(observer -> observer.update(temperature, humidity, pressure));
}
public void measurementsChanged() {
notifyObserver();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
@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");
}
}
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay conditionsDisplay = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(70, 60, 30.4f);
weatherData.setMeasurements(55, 50, 15.4f);
weatherData.setMeasurements(90, 70, 40.4f);
}
}
현재는 subject의 상태가 변경될 때마다 observer에게 알려주고(push) 있다. 옵저버 입장에서는 필요한 상황에서만 주체의 상태를 가져오는 방식(pull)이 더 편할 수도 있지 않을까??
이처럼 옵저버 패턴은 PUSH 방식과 PULL 방식으로 구분할 수 있다.
PUSH 방식
: 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식
PULL 방식
: 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식
또한 자바에서 몇 가지 API를 통해 자체적으로 옵저버 패턴을 지원한다. 일반적으로 java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스이다. 해당 내장 클래스들은 푸시 방식과 풀 방식 모두 가능하다.
그렇다면 자바 내장 클래스를 이용하여 풀 방식으로 수정해보자
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import observer.after.Observer;
import observer.after.Subject;
public class WeatherDataObservable extends Observable {
private float temperature;
private float humidity;
private float pressure;
public WeatherDataObservable() {
}
public void measurementsChanged() {
setChanged();
notifyObservers(); // pull 방식, push 방식인 경우 notifyObservers(Object arg);
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// pull 방식이므로 옵저버가 주체 객체의 상태를 알아야하므로 필요하다.
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
import java.util.Observable;
import java.util.Observer;
import observer.after.DisplayElement;
import observer.after2.WeatherDataObservable;
public class CurrentConditionsDisplayObserver implements Observer, DisplayElement {
private float temperature;
private float humidity;
Observable observable;
public CurrentConditionsDisplayObserver(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
@Override
public void update(Observable o, Object arg) {
if(o instanceof WeatherDataObservable) {
WeatherDataObservable weatherData = (WeatherDataObservable) o;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
@Override
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
Observer와 Observable은 Java SE 9 버전부터 Deprecated 되었다. 그 이유는 무엇일까?
java.beans
패키지가 제공하고 있다.java.util.concurrent
패키지의 concurrent 자료 구조들 중 하나를 골라 쓰는 편이 낫다.Flow
API를 쓰기를 권한다.Observable의 문제는 헤드 퍼스트 디자인 패턴에서도 지적하고 있다