뭔가 재미있는 일이 생겼을 때 모르고 지나치면 슬프겠죠?
뭔가 중요한 일이 일어났을 때 객체에게 새 소식을 알려 줄 수 있는 패턴이 하나 있습니다. 바로 옵저버 패턴입니다. 자주 사용되는 패턴 중 하나로 엄청나게 쓸모가 있습니다.
2장에서는 일대다 관계나 느슨한 결합같이 옵저버 패턴에 관한 내용을 두루두루 배워 볼 겁니다.
이 시스템은
WeatherData
객체(기상 스테이션으로부터 오는 정보를 추적하는 객체)이 세 가지 요소로 이루어진다.
각각에 대한 요구사항은 아래와 같다.
WeatherData
객체는 현재 기상 조건인 온도
, 습도
, 기압
을 추적한다.현재 조건
,기상 통계
,기상 예보
를 표시한다.WeatherData
객체에서 최신 측정치를 수집할 때 마다 실시간으로 갱신된다.WeatherData
클래스의 메소드는 아래와 같이 구성되어 있다.
이 세가지 메소드는 각각 최근에 측정된 온도, 습도, 기압 값을 리턴하는 메소드다.
이 메소드는 WeatherData
에서 갱신된 값을 가져올 때 마다 호출된다.
이 메소드에 디스플레이를 업데이트하는 코드를 넣어 현재 조건
,기상 통계
,기상 예보
를 보여줄 수 있도록 해보자.
WeatherData
클래스에는 3가지 측정값(온도, 습도, 기압)의 게터 메소드가 있다.measurementsChanged()
메소드가 호출된다.현재 조건
,기상 통계
,기상 예보
) 를 구현해야 한다.measurementsChanged()
메소드에 코드를 추가해야 한다.public class WeatherData{
// 인스턴스 변수 선언
public void measurementsChanged(){
// WeatherData에 있는 게터 메소드를 호출해서 최신 측정값을 가져오고
// 각 값을 적당한 변수에 저장한다.
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
// 각 디스플레이를 갱신한다.
// 최신 측정값을 전달하면서 각 디스플레이 항목의 update 메소드를 호출한다.
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
// 기타 메소드
}
위 코드의 특징은 아래와 같다.
해당 특징들이 드러나는 부분은 전체 코드 중 아래 부분이다.
currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
바뀌어야 하는 부분이지만 캡슐화되어 있지 않으며, 구체적인 구현에 맞춰 코딩했으므로 프로그램을 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없다.
이러한 단점들을 어떻게 보완할 수 있을까?
바로 오늘의 주제인 옵저버 패턴
을 적용해볼 때다.
신문이나 잡지를 구독하는 매커니즘에 대해 생각해보자.
여기서 한 가지 결론을 얻을 수 있다.
💡 신문사(subject) + 구독자(observer) = 옵저버 패턴
위 문장에 대해 자세히 알아보도록 하자.
주제 객체
가 되는 신문사는 중요한 데이터를 관리한다.
만약 주제 데이터가 바뀌면 옵저버 객체
에게 그 소식이 전해지며, 새로운 데이터 값은 어떤 방법으로든 옵저버 객체에 전달된다.
옵저버 객체들은 주제를 구독하고 있으며(주제 객체에 등록되어 있으며) 주제 데이터가 바뀌면 갱신 내용을 전달받는다.
옵저버가 아닌 객체는 주제 데이터가 바뀌어도 아무 연락도 받지 못한다.
int
값에 정말 관심이 많았기 때문이다!새 객체도 이제 정식 옵저버가 되었다.
새 객체는 매우 신난 상태다! 구독자 목록에 이름을 올리고 다음에 전달될 int
값을 애타게 기다리고 있다!
주제 값이 바뀌었다.
이제 새 객체를 비롯한 모든 옵저버가 주제 값이 바뀌었다는 연락을 받게 되었다!
기존 옵저버 객체중 하나가 옵저버 목록에서 탈퇴하고 싶다는 요청을 한다.
배가 부른 한 객체는 한참 전부터 int
값을 받아왔으나 이제 지겨워졌는지 옵저버를 그만두기로 하고 해지 요청을 한다.
이제 그 객체는 빠졌다.
주제가 요청을 받아들여 해당 객체를 옵저버 집합에서 제거했다.
주제에 새로운 int
값이 들어왔다.
모든 옵저버가 값이 바뀌었다는 연락을 받는다. 하지만 탈퇴한 객체는 더이상 연락을 받지 못하기에 새로운 int
값이 무엇인지 알 수 없다. 새로운 int
값이 무엇인지 알고 싶다면 다시 옵저버 등록 요청을 하면 된다.
이 흐름을 쭉 읽어보면 마치 IT 기업 헤드헌터(주제)
에게 개발자(일반 객체)
가 본인의 포트폴리오를 어필하여 리크루팅 목록(옵저버)
에 이름을 올려두고, 채용 기회가 있을 때마다 헤드헌터(주제)
가 개발자(옵저버의 객체)
에게 이를 알리는 것과 비슷한 맥락이라고 볼 수 있을 것이다.
옵저버 패턴이 무엇인지 어느정도 이해했다면 이제 정확한 정의를 알아보자.
옵저버 패턴(ObserverPattern)은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.
옵저버 패턴은 일련의 객체 사이에서 일대다 관계
를 정의하기 때문에 한 객체의 상태가 변경되면 그 객체에 의존하는 모든 객체에 연락이 간다.
즉, 옵저버는 주제에 딸려 있으며 주제의 상태가 바뀌면 옵저버에게 정보가 전달된다.
옵저버는 데이터가 변경되었을 때 주제에서 갱신해주기를 기다리는 입장이기에 의존성
을 가진다고 할 수 있으며, 이런 방법을 사용하면 여러 객체가 동일한 데이터를 제어하는 방법보다 더 깔끔한 객체지향 디자인을 만들 수 있다.
옵저버 패턴은 여러 가지 방법으로 구현할 수 있지만, 보통은 주제 인터페이스
와 옵저버 인터페이스
가 들어있는 클래스 디자인으로 구현한다.
느슨한 결합에 대해 다루기에 앞서 바구니를 예시로 들어 한번 생각해보자.
단단하게 엮인 바구니와 느슨하게 엮인 바구니가 있다. 둘 중 어느 것이 덜 찢어지거나 부서질까? 바로 느슨하게 엮여 유연한 바구니다.
소프트웨어에서도 마찬가지다. 객체가 서로 덜 단단하게 결합되어 있다면 객체들이 부서질 확률이 낮아진다.
모든 생명체가 다른 생명체에 의존하는 것처럼 모든 객체는 다른 객체에 의존한다. 따라서 완전히 의존하지 않을 수는 없지만, 느슨한 결합을 위해 의존은 하되 다른 객체의 세세한 부분까지 다 알 필요는 없는 것이다.
다른 객체를 잘 모르면 변화에 더 잘 대응할 수 있는 디자인을 만들 수 있다. 덜 단단하게 짠 바구니처럼 말이다.
느슨한 결합(Loose Coupling)은 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계를 의미한다.
앞서 바구니를 예시로 든 것 처럼, 느슨한 결합을 활용하면 유연성이 아주 좋아진다. 옵저버 패턴은 이 느슨한 결합을 보여주는 훌륭한 예시다.
상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 객체지향 시스템을 구축할 수 있다. 객체 사이의 상호의존성을 최소화할 수 있기 때문이다.
pubilc interface Subject{
// Observer를 인자로 받아 옵저버를 등록하거나 제거함
public void registerObserver(Observer o);
public void removeObserver(Observer o);
// 주제의 상태가 변경되었을 때 모든 옵저버에게 변경 내용을 알림
public void notifyObservers();
}
// 모든 옵저버 클래스에서 구현해야 함
// 따라서 모든 옵저버는 update() 메소드를 구현해야 함
public interface Observer{
public void update(float temp, float humidity, float pressure);
}
// 디스플레이 항목을 화면에 표시해야 할 때 메소드를 호출함
public interface DisplayElement{
public void display();
}
public class WeatherData implements Subject { // WeahterData에서 Subject 인터페이스를 구현
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<Observer>(); // 생성자가 a의 객체 생성
}
// 여기서부터 notifyObservers() 까지는 Subject 인터페이스를 구현하는 부분
// 옵저버가 등록을 요청하면 목록 맨 뒤에 추가
public void registerObserver(Observer o) {
observers.add(o);
}
// 옵저버가 탈퇴를 요청하면 목록에서 뺌
public void removeObserver(Observer o) {
observers.remove(o);
}
// ✨중요✨ 모든 옵저버에게 상태 변화를 알려주는 부분
// 모두 Observer 인터페이스를 구현하는, update() 메소드가 있는 객체들이므로 손쉽게 상태 변화를 알려줄 수 있음
public void notifyObservers() {
for (Observer observer : observers) {
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(); // 실제 장비에서 기상 데이터를 가져오는 대신 메소드를 활용해 디스플레이 항목을 테스트
}
// 기타 WeatherData 메소드
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
}
// WeatherData 객체로부터 변경 사항을 받으려면 Observer를 구현해야
// 모든 디스플레이 항목에서 DisplayElement를 구현하기로 했으므로 이것도 구현
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData;
// 생성자에 weatherData라는 주제가 전달되며, 그 객체를 써서 디스플레이를 옵저버로 등록
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
// update()가 호출되면 온도와 습도를 저장하고 display()를 호출
public void update(float temperature, float humidity, float pressure) {
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(); // WeatherData 객체를 생성
// 3개의 디스플레이를 생성하고 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);
}
}
옵저버를 구현하는 방식에는 크게 두 가지가 있다.
지금 만들어 놓은 WeatherData 디자인은 전자의 방식으로, 하나의 데이터만 갱신해도 되는 상황에서도 update() 메소드에 모든 데이터를 보내도록 되어 있다.
그래도 별 문제는 없지만, 풍속 같은 새로운 데이터 값을 추가한다면 대부분의 update() 메소드에서 풍속 데이터를 쓰지 않더라도 모든 디스플레이에 있는 update()의 값을 바꿔야 할 것이다.
이러한 이유 등으로, 대체로 옵저버가 필요한 데이터를 골라서 가져가도록(Pull) 만드는 방법이 더 좋다.
주제가 자신의 데이터에 관한 게터 메소드를 가지게 만들고, 필요한 데이터를 당겨올 때 해당 메소드를 호출할 수 있도록 옵저버를 고쳐주면 위와 같은 방법으로 구현이 가능하다.
-> 옵저버 패턴에서 변하는 것은 주제의 상태와 옵저버의 개수, 형식이다. 옵저버에서는 주제를 바꾸지 않고도 주제의 상태에 의존하는 객체들을 바꿀 수 있다. 나중에 바뀔 것을 대비해 두면 편하게 작업할 수 있다.
-> 주제와 옵저버에서 모두 인터페이스를 사용한다.
주제는 Subject 인터페이스로 Observer 인터페이스를 구현하는 객체들의 등록과 탈퇴를 관리하고, 그런 객체들에게 연락을 돌린다. 이렇게 하면 느슨한 결합도 만들 수 있다.
-> 옵저버 패턴에서는 구성을 활용해서 옵저버들을 관리한다. 주제와 옵저버 사이의 관계는 상속이 아니라 구성이다. 게다가 실행 중에 구성되는 방식을 사용하므로 더욱 좋다.
본 포스팅에 쓰인 이미지와 내용의 모든 출처는 책 '헤드 퍼스트 디자인 패턴' 에 있습니다.