[Design Pattern] Observer Pattern
헤드퍼스트 디자인 패턴 자바 책을 읽고 정리한 글입니다.
옵저버 패턴이란?
객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 상태를 알리고 내용이 갱신되는 방식이다. 일대다(one-to-many)의 의존방식을 사용한다.
주제 : 상태 값을 가지고있는 객체
옵저버 : 상태가 변경되면 주제로부터 알림을 받는 객체
→ 옵저버는 주제에 의존하며 주제의 상태가 바뀌면 옵저버에게 연락이 간다. 연락 방법에 따라 옵저버의 값이 갱신될 수 있다.
기상정보 애플리케이션 예시
개요
WeatherData객체를 사용하여 현재 날씨, 기상 통계, 기상 예측 세 항목을 디스플레이 장비에서 갱신하면서 보여주는 애플리케이션을 만들어보자.
조건
1. WeatherData객체 (주제)
: 기상 스테이션으로부터 기상 정보 데이터를 취득한다.
세 가지 측정값(온도, 습도, 기압)을 알아내기 위한 getter메서드가 있다.
- getTemperature()
- getHumidity()
- getPressure()
새로운 기상 데이터가 나올 때마다 갱신 메서드를 호출한다.
- measurementsChanged()
2. Display (옵저버)
: 기상데이터를 활용해서 세 개의 디스플레이를 구현해야 한다.
- 현재 날씨 표시 (현재 기온, 현재 습도, 현재 기압)
- 기상 통계(평균 기온, 최저 기온, 최고 기온)
- 기상 예보
WeatherData에 새로운 측정값이 들어올 때마다 디스플레이가 갱신되어야 한다.
3. 시스템의 확장 가능성
: 일단은 세 가지의 디스플레이만 구현하지만 추후에 다른 개발자들이 별도의 디스플레이 항목을 만들 수 있도록 해야 하며 사용자들이 애플리케이션에 맘대로 디스플레이 항목을 추가, 제거 할 수 있도록 확장 가능성을 염두하고 개발해야 한다.
코드 예시
public class WeatherData {
// 인스턴스 변수 선언...
public void measurementsChanged(){
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
// 디스플레이 갱신
currentConditionDisplay.update(temp, humidity, pressure);
statisticDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
...
}
위 코드의 문제점
- 디스플레이 갱신하는 부분의 코드 → 구체적인 구현에 맞춰서 코딩했기 때문에 나중에 디스플레이가 추가 or 제거될 경우에는 WeatherData 프로그램을 고치지 않을 수가 없다.
- 즉 디스플레이 갱신하는 코드는 변경될 수 있기 때문에 캡슐화해야 한다.
위의 코드를 옵저버 패턴으로 코드를 바꾸기 전에 옵저버 패턴이 뭔지 더 자세히 알아보자…
옵저버 패턴의 일대다(one-to-many) 관계 성립
- 옵저버 패턴에서 상태를 저장하고 관리하는 것은 주제 객체이다.
- 때문에 상태를 가지고 있는 객체는 하나만 있을 수 있다.
- 옵저버는 여러 개가 있을 수 있다. → 주제로부터 상태를 기다리기 때문에 주제에 의존적이다.
- 옵저버들이 하나의 주제에 의존성을 가짐으로써 객체지향 디자인을 만들 수 있다.
옵저버 패턴에서 느슨한 결합이란?
느슨한 결합 : 두 객체가 상호작용을 하지만 서로의 내부 구현에 대해 잘 모르는 것을 말한다.
- 주제가 옵저버에 대해 아는 것은 오직 옵저버가 특정 인터페이스를 구현한다는 것
- 옵저버는 언제든지 새로 추가될 수 있음 (반대로 언제든지 제거될 수 있다)
- 주제와 옵저버는 서로 독립적으로 재사용이 가능하다. (만약 단단한 결합이었다면 사용에 제약이 생길 것이다.)
- 주제나 옵저버가 바뀌어도 서로에게 영향을 미치지 않는다.
옵저버 패턴으로 기상 애플리케이션 코드 개선
먼저 데이터들을 취득하고 상태를 관리하는 WeatherData객체는 주제 객체가 된다.
WeatherData로부터 갱신되는 기상 정보 데이타를 받아 화면에 뿌려주는 Display객체들은 옵저버 객체에 해당한다.
기상 정보 데이터들을 디스플레이에 어떻게 전달할 것인가?
→ WeatherData 객체로부터 상태 값을 전달받는 객체라는 것을 등록해야 한다.
모든 디스플레이의 항목이 다를 수 있다는 것을 기억하자
→ 디스플레이의 구성 요소 형식이 달라도 모든 display 객체들이 공통 인터페이스를 구현하면 WeatherData객체에서 측정값을 전달할 수 있다.
→ update()가 정의된 공통 인터페이스를 모든 디스플레이(옵저버)들이 구현해야 WeatherData로부터 상태를 전달받을 수 있다.
클래스 다이어그램
- WeatherData는 Subject(옵저버를 등록, 제거, 상태 알림 기능)을 구현하여 옵저버들을 관리하고 상태값을 전달할 수 있다. 그 외 온도, 습도, 기온 값들을 가져오는 메소드와 기상대이터가 갱신될 때 마다 호출하는 메서드가 정의되어있다.
- 3개의 디스플레이 객체는 기상 데이터를 업데이트하는 인터페이스(Observer)와 화면에 뿌려주는 기능이 있는 인터페이스(DisplayElement)를 구현한다.
코드로 구현
public class WeatherData implements Subject {
private ArrayList observers;
private float humidity;
private float pressure;
// 생성자에서 Observer 객체 생성
public WeatherData() {
obserbers = new ArrayList();
}
// Observer 추가
public void registerObserver(Observer o) {
observers.add(o);
}
// Observer 삭제
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if(i>=0){
observers.remove(i);
}
}
// 상태에 대해 Observer들에게 알리는 메소드
// Observer들은 모두 공통 인터페이스를 구현하고 있기 때문에 update()를 통해서 쉽게 알릴 수 있다
public void notifyObservers() {
for (int i=0; i<observers.size(); i++){
Observer observer = (Observer)observers.get(i);
observer.update(temperature, humitity, pressure);
}
}
// 데이터가 갱신되면 Observer들에게 알림
public void measurementsChanged(){
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure){
this.premperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
...
}
디스플레이 코드
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData;
// 디스플레이 객체 생성 시 옵저버로 등록
public CurrentConditionsDisplay(WeatherData WeatherData){
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(){
this.temperature = temperature;
this.humidity = humidity;
display();
}
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 currentDp = new CurrentConditionsDisplay(weather);
// 새 측정값 들어온 것 처럼 여러번 호출
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 20.2f);
}
}
지금까지 옵저버 패턴을 적용한 기상 애플리케이션을 직접 구현해보았다.
자바 내장 기능인 java.util.Observable 클래스와 java.util.Observer인터페이스를 이용해서 옵저버 객체를 만들수도 있다.
하지만 이 기능을 사용할 때에는 염두해주어야 할 단점이 존재한다.
- Observable은 인터페이스가 아닌 클래스이기 때문에 서브클래스를 만들어야 하는데, 자바는 다중 상속이 되지 않기 때문에 다른 클래스를 확장하고 있는 객체는 옵저버 객체가 될 수 없다 → 재사용성에 제약이 생김
- Observable 인터페이스라는 것이 없기 때문에 자바에 내장된 Observer API와 잘 맞는 클래스를 직접 구현하는 것이 불가능하다. → 멀티스레드로 구현한다거나 하는 일이 불가능해진다.
- Observable의 핵심 메서드인 setChanged() 메서드를 외부에서 호출할 수 없다. protected로 선언되어있기 때문이다. → 상속보다는 구성을 사용한다는 디자인 패턴에 위배됨
옵저버 패턴에서 사용되는 디자인 원칙
1. 변경되는 부분을 찾아내서 변경되지 않는 부분으로부터 분리시킨다.
→ 옵저버 패턴에서 변화하는 것 : 주제의 상태, 옵저버의 개수, 형식
옵저버 패턴에서는 주제의 코드를 바꾸지 않고도 주제의 상태에 의존하는 객체들을 바꿀 수 있다.
2. 특정 구현이 아니라 인터페이스에 맞춰서 프로그래밍한다.
→ Subject와 Observer 모두 인터페이스를 사용한다.
Subject인터페이스를 통해서 Observer인터페이스를 구현한 객체들을 관리한다.
3. 상속보다는 구성(implement)을 활용한다.
→ 옵저버 패턴에서는 구성을 활용하여 옵저버들을 관리한다. 주제와 옵저버 사이의 관계는 상속이 아니라 구성에 의해서 이루어진다.