참고: Head First Design Patterns
날씨를 모니터링하는 어플리케이션이 있다. 어플리케이션은 아래 3가지 부분으로 구분할 수 있다.
이 중, WeatherData 객체를 이용하여 다음 3가지 화면을 Display에 업데이트하는 기능을 구현해야 한다.
class WeatherData {
getTemperature()
getHumidity()
getPressure()
measurementsChanged()
}
WeatherData
는 3가지 정보에 대한 getter
메소드와 measurementsChanged()
메소드를 가진다. 그 중 measurementsChanged() 메소드는 측정치에 업데이트가 생길 때 마다 호출할 메소드이다.
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);
}
위와 같이 getter
로 데이터를 받아온 후, Display
객체의 upate()
메소드에게 전달하도록 구현할 수 있겠다.
위의 코드는 과연 최선일까? 앞서 Strategy Pattern을 공부하면서 배웠던 디자인 원칙들을 되새기면서 문제점들을 분석해보자.
※ [디자인 패턴] 1. the Strategy Pattern 보러 가기
앞에서 동적으로 구현체가 결정되도록 프로그래밍 하는 법을 배운 것이 무색하게 Display
의 구현체들을 각각 선언하여 직접 접근하고있다.
똑같은 Display
인터페이스의 구현체의, 똑같은 update()
메소드에게, 똑같은 매개변수를 전달하는 코드가 3줄이나 된다. 구현할 화면이 많아진다면 중복되는 코드도 더 많아질 것이다.
정리하자면 캡슐화의 부재라고 할 수 있다.
유튜버와 구독자 관계를 생각해보자. 유튜버가 영상을 올리면 구독자에게 알림이 보내진다. 구독자는 알림을 받고, 구독중인 유튜버의 새로운 영상이 올라왔다는 것을 알 수 있다.
이제, 유튜버를 서브젝트(Subject), 구독자를 옵저버(Observer)에 대입해보자. 옵저버는 서브젝트를 '구독'중이다. 마치 유튜버의 구독자처럼 서브젝트에 변화가 생길 때 마다 옵저버는 그 내용을 전달받는다. 당연히 구독자는 여려명일 수 있다.
구독자는 원한다면 언제든지 구독 해지를 할 수 있으며, 새로운 유튜버를 구독할 수도 있다. 마찬가지로 옵저버도 유연하게 서브젝트를 구독 - 해지할 수 있다. 이런 옵저버와 서브젝트의 관계가 옵저버 패턴의 핵심이다.
따라서, 옵저버 패턴은 아래과 같이 정의한다.
어떤 객체의 상태가 바뀔 때, 그 객체에 의존하는 모든 객체들이 자동으로 업데이트 되도록 하는 객체들 간의 1:N 의존성
여러 가지 구현 방법이 있지만, 대부분 서브젝트와 옵저버 인터페이스를 이용해 구현한다.
interface Subject {
registerObserver()
removeObserver()
notifyObservers()
}
interface Observer {
update()
}
위 코드에서 알 수 있드시 옵저버의 등록, 제거, 상태 변화 알려주기 모두 서브젝트 인터페이스를 통해서 진행한다. 옵저버 인터페이스는 단지 상태 변화 정보를 받아 업데이트하는 update()
메소드 단 하나만을 가진다.
class ConcreteSubject implements Subject {
register ...
remove ...
notify ...
getState()
setState()
}
class ConcreteObserver implements Observer {
update()
}
서브젝트 인터페이스의 구현체는 인터페이스에 선언된 메소드 외에도 '상태'에 대한 getter
와 setter
를 가질 수 있다. 옵저버의 구현체는 update()
외에는 정해진 형식은 크게 없다. 왜냐하면, 옵저버 인터페이스를 구현한 그 어떤 객체도 옵저버가 될 수 있기 때문이다.
두 객체가 '느슨하게 결합되어있다'라는 말은 상호작용이 가능하나, 서로의 정보에 대해선 잘 모른다는 의미이다. 옵저버 패턴은 서브젝트와 옵저버가 느슨하게 결합되어있는 관계라고 볼 수 있다.
서브젝트가 옵저버에 대해 아는 것은 '오브젝트 인터페이스를 구현하였구나'가 전부이다. 그 외의 정보는 필요하지 않다. 또한, 새로운 옵저버를 언제든 추가할 수 있으며 기존 옵저버들과 다른 '타입'의 옵저버 객체를 서브젝트의 수정 없이 등록할 수 있다.
이 느슨한 결합 덕분에 서브젝트와 옵저버는 독립적으로 재사용이 가능하고, 어느 한쪽의 수정은 다른 쪽에 전혀 영향을 주지 않는다.
여기서 오늘의 디자인 원칙을 배울 수 있다.
상호작용하는 두 객체들 사이에서는 느슨한 결합을 지향하라.
자, 다시 날씨 어플리케이션으로 돌아가보자. 옵저버 패턴은 데이터를 관리할 '1'과 데이터 변경을 업데이트 받을 'N'의 결합이라고 하지 않았던가? 그렇다면, 우리의 WeatherData
객체가 1, Display
가 N이라고 볼 수 있겠구나!
class WeatherData implements Subject {
registerObserver()
removeObserver()
notifyObservers()
getTemperature()
getHumidity()
getPressure()
measurementsChanged()
}
class CurrentConditionsDisplay implements Observer, Display {
update()
display()
}
class StatisticsDisplay implements Observer, Display {
update()
display()
}
class ForecastDisplay implements Observer, Display {
update()
display()
}
위와 같은 구조로 설계할 수 있겠다. Display
인터페이스는 display()
메소드만을 갖는다.
이제 WeatherData
클래스를 구체적으로 어떻게 구현할 수 있을지 살펴보자. 서브젝트이기 때문에 옵저버를 관리할 자료구조를 가져야 할 것이다.
Java가 제공하는 ArrayList를 이용해 선언해보자.
private List<Observer> observers;
public WeatherData() { // 생성자
observers = new ArrayList<>();
}
다음으로 register
, remove
, notify
메소드를 구현해보자.
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 = observers.get(i);
observer.update(temperature, humidity, pressure);
}
}
register
와 remove
는 ArrayList가 제공하는 메소드로 간단하게 구현하였다. notify
역시 크게 어렵지 않은데, 서브젝트에 등록된 모든 옵저버들에게 새로운 데이터를 update
해주면 그만이다.
자, 이제 드디어 measurementsChanged()
메소드를 구현할 차례이다. 과연 옵저버 패턴을 적용한 이 메소드 어떤 모습일까?
public void measurementsChanged() {
notifyObservers();
}
구현할 내용이 없다!
measurementsChanged()
메소드의 요구사항은 데이터가 업데이트 될 때, Display
에게 값을 넘겨주는 것이지 않았나? 그런데, WeatherData
는 서브젝트 객체이기에 이미 notify
해주는 메소드를 가진다. 따라서, 그저 notifyObservers()
를 호출해주면 그만이다.
이렇게 우리의 서브젝트 객체 WeatherData
의 구현이 완료되었다. 우리의 옵저버 Display
는 어떻게 구현할까?
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
Display
의 구현체는 서브젝트, 즉 WeatherData
를 인스턴스로 가지며 생성과 동시에 register
한다.
옵저버 인터페이스의 유일한 메소드 update()
를 구현해보자.
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
서브젝트가 넘겨준 정보를 받아 저장하고 display()
메소드를 호출하여 화면에 이쁘게 보여주면 Display
의 구현도 끝이다.
다시 강조하지만, 서브젝트 객체는 notify
할 대상이 누구인지, 무얼 하는지 알지 못 할 뿐더러 알 필요도 없다.
이 점을 명심하고 옵저버 패턴을 적용하기 전의 measurementsChanged()
메소드를 다시 보면 느껴지는 점이 많을 것이다. 가령, 더 많은 종류의 Display
를 만들어야 한다고 생각해보자.
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);
// Display 추가
anotherDisplay.update(temp, humidity, pressure);
andAnotherDisplay.update(temp, humidity, pressure);
moreDisplay.update(temp, humidity, pressure);
andMoreDisplay.update(temp, humidity, pressure);
andMoreAndMoreDisplay.update(temp, humidity, pressure);
...
}
Display
가 하나 추가 될 때 마다 코드를 수정해야 한다. 심지어, 모두 똑같은 로직의 코드다.
반면, 똑같이 더 많은 Display
를 추가해야 하는 상황에서 우리의 '서브젝트' WeatherData
의 코드는 어떻게 바뀔까?
public void measurementsChanged() {
notifyObservers();
}
당연하게도 코드를 수정할 필요가 없다. 새로 만들 Display
가 무엇이던, 무엇을 하던 옵저버이기만 하면 객체 생성과 동시에 WeatherData
라는 서브젝트에 register
되고, 그 순간부터 모든 업데이트는 서브젝트가 알아서 해주기 때문이다.
이러한 객체지향 설계는 다른 디자인 패턴들과 함께 더 자세히 다루겠지만, 아주 중요한 객체지향 설계 원칙을 관통한다.
The 'Open - Closed' Principle (OCP) :
코드의 수정에 있어서는 닫혀있되, 프로그램의 확장에 있어서는 열려있도록 프로그래밍하라.
우리는 옵저버 패턴을 이용해 코드를 매번 수정하지 않아도 되도록 구현하여 Display 종류의 확장에 있어서 유연한 날씨 어플리케이션을 만들었다.
Java는 java.util.Observer
, java.util.Observable
을 통해 옵저버 패턴을 built-in으로 제공한다.
직접 구현한 옵저버 패턴과의 차이점이라면, 서브젝트 객체는 Subject
인터페이스 대신 Observable
클래스를 상속하는 방식으로 사용하며, 더 다양한 기능을 제공한다.
정확하다. 이것이 Java가 제공하는 Observable
의 가장 큰 단점이다. 인터페이스가 아니기 때문에 이미 다른 수퍼클래스를 상속중인 객체는 Observable
의 서브클래스가 되지 못한다.
또한, 인터페이스가 아닌 클래스이기 때문에 제공하는 메소드의 커스텀이 자유롭지 못하다. 추가로, Observer
이 들어있는 API는 'Protected'로 선언되어 있다. 즉, 다른 패키지의 클래스들은 접근할 수 없다.