본 포스트는 헤드퍼스트 디자인패턴을 읽고 정리한 글입니다. |
Ch2 옵저버 패턴
Question: 기상 스테이션 구축
현재 기상 조건(온도, 습도, 기압)을 추적하는 WeatherData 객체를 바탕으로 3개의 항목을 화면에 표시하는 애플리케이션을 만들어야 한다.
요구사항
- 3가지 디스플레이에 표시할 항목은 각각 현재 조건, 기상 통계, 기상 예보이다.
- 화면에 표시되는 항목은 WeatherData 객체에서 최신 측정치를 수집할 때마다 실시간으로 갱신된다.
- 날씨 디스플레이는 확장 가능해야 한다.
WeatherData 객체 코드 채우기
public class WeatherData {
// 인스턴스 변수 선언
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay();
StatisticsDisplay statisticsDisplay = new StatisticsDisplay();
ForecastDisplay forecastDisplay = new ForecastDisplay();
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);
}
}
Problem
각 디스플레이에 정보를 갱신하는 부분
- 두 번째 디자인 원칙(구현보다는 인터페이스에 맞춰서 프로그래밍한다.) 위반
- 구체 클래스에 맞추어 코딩한 것으로, 다른 디스플레이 항목을 추가하거나 제거할 수 없다.
- 바뀔 수 있는 부분으로, 분리하여 캡슐화해야 한다.
이 문제를 위한 해답은 옵저버 패턴에 있다.
옵저버 패턴 (Observer Pattern)
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의하는 디자인 패턴이다.
신문사와 구독자, 주제와 옵저버
신문사는 구독 중인 모든 독자에게 신문을 제공한다.
옵저버 패턴에서 신문사는 ‘주제(Subject)’에 해당하고 구독자는 ‘옵저버(Observer)’에 해당하며 일대다 관계가 정의된다.
옵저버는 주제에 등록되며, 주제의 상태가 바뀌면 옵저버에게 정보가 전달된다.
옵저버 패턴의 구조
Subject 인터페이스와 Observer 인터페이스가 들어있는 클래스 디자인으로 구현된다.
구상 클래스가 각각의 인터페이스를 구현하며, Subject 객체는 List로 Observer 객체를 담고 있다.
주제 (Subject)
인터페이스
객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때 사용하는 메서드가 담겨 있다.
객체에서 옵저버로 등록할 때 Subject의 List에 추가되며, 탈퇴할 때는 삭제된다.
구상 클래스
Subject 인터페이스를 구현한다. 상태가 바뀔 때 모든 옵저버에게 연락하는 notifyObservers 메서드를 구현해야 한다.
Subject에서 Observer로 알림을 보낼 때, 순서에 의존하지 말라는 JDK 권고가 있으니 주의하도록 하자.
옵저버 (Observer)
인터페이스
주제의 상태가 바뀌었을 때 호출되는 update 메서드만 존재한다.
구상 클래스
Observer 인터페이스를 구현한다. 각 옵저버는 특정 주제에 등록하여 연락을 받을 수 있다.
디자인 원칙
네 번째, 느슨한 결합
상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
느슨하게 결합하는 디자인을 사용하면 객체 사이의 상호의존성을 최소화할 수 있기 때문에, 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.
옵저버 패턴은 느슨한 결합을 잘 보여주는 예이다.
느슨한 결합 (Loose Coupling)
객체들이 상호작용할 수 있지만, 서로를 잘 모르는 관계를 의미한다.
옵저버 패턴이 느슨한 결합을 만드는 방식
- 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 안다.
- 옵저버는 언제든지 새로 추가할 수 있다.
- 다형성
- 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 없다.
- 주제와 옵저버는 서로 독립적으로 재사용할 수 있다.
- 주제나 옵저버가 달라져도 서로에게 영향을 미치지 않는다.
이 다섯 가지 방식으로 인하여 Subject와 Observer 사이의 결합은 느슨한 결합이다.
옵저버 패턴에 적용된 디자인 원칙
- 첫 번째, 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
- 주제의 상태와 무관하게 옵저버의 개수와 형식을 바꿀 수 있다.
- 두 번째, 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
- 주제와 옵저버 모두 인터페이스를 사용했다. 이를 통해 느슨한 결합를 만들 수 있다.
- 세 번째, 상속보다는 구성을 활용한다.
- 구성을 활용하여 옵저버들을 관리한다. 주제 ↔ 옵저버 : 구성
위에서 언급된 세 가지 디자인 원칙에 대한 설명은 아래 포스트를 통해 확인할 수 있습니다.
Problem Solve
전제
WeatherData 객체는 현재 기상 조건(온도, 습도, 기압)을 추적한다.
요구사항
- 화면에 표시할 3가지 항목은 현재 조건, 기상 통계, 기상 예보이다.
- 화면에 표시되는 항목은 WeatherData 객체에서 최신 측정치를 수집할 때마다 실시간으로 갱신된다.
- 날씨 디스플레이는 확장 가능해야 한다.
위 요구사항에 따라, WeatherData와 WeatherDisplay는 일대다 관계를 가진다. WeatherData : WeatherDisplay = 1 : N
즉, WeatherData가 Subject, WeatherDisplay가 Observer가 된다.
옵저버 패턴의 구조에 따른 요구사항 만족 - Data Push
‘Data Push’라는 표현은 빌드업이니 일단 넘기도록 하자. |
옵저버 패턴의 구조에 따라 주제와 옵저버는 각각 인터페이스를 가진다.
- 옵저버인 Display 구현체들은 서로 다른 방식으로 display한다.
display()
메서드를 가지는Display 인터페이스
를 하나 더 만들고 각 화면(구상 클래스)이 이를 구현하면 1번 요구사항을 만족한다. Observer 인터페이스
는update()
메서드를 가지며, 이는 화면에 표시할 항목을 주제의 notifyObservers()로부터 전달 받은 정보로 갱신하는 메서드이다.- 옵저버는 여러 구현체가 존재할 수 있으며
주제와 다른 구현체의 수정사항 없이 새로운 구현체 생성이 가능
하다는 점에서 3번 요구사항을 만족한다.
구조도
실제 구현 코드
Subject Interface
import observer.Observer;
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
Observer Interface
public interface Observer {
public void update(float temperature, float humidity, float pressure);
}
Display Interface
public interface WeatherDisplay {
public void display();
}
Subject 구상 클래스 - WeatherData
import observer.Observer;
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;
// Subject interface 구현
public WeatherData() {
observers = new ArrayList<Observer>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
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();
}
}
Observer(Display) 구상 클래스 - CurrentConditionsDisplay
import subject.WeatherData;
public class CurrentConditionsDisplay implements Observer, WeatherDisplay {
private float temperature;
private float humidity;
private WeatherData weatherData; // 내가 등록한 Subject
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this); // Subject 의 Observer 로 등록
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
@Override
public void display() {
System.out.println("현재 상태: 온도 " + temperature + "F, 습도 " + humidity + "%");
}
}
프로그램 흐름
- Observer는 weatherData에 자신을 등록한다.
- 측정값이 갱신되면 WeatherData의 상태가 변경되며 measurementChanged()가 호출된다.
- measurementChanged()에서 notifyObservers()을 호출한다.
- notifyObservers()에서 등록된 모든 Observer에 상태가 변경되었음을 알린다(update() 호출).
- Observer는 update 메서드를 통해 데이터를 받고 각 디스플레이 특성에 맞게 display()한다.
실행 결과
import observer.*;
import subject.WeatherData;
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
}
}
Think About
측정치를 직접 전달(update(temperature, humidity, pressure)
로 모든 데이터 전송)하는 게 상태를 갱신하는 가장 간단한 방법일까?
갱신된 상태를 옵저버에게 전달하는 문제를 해결할 수 있는 다른 접근법을 생각해 보자.
코드 수정 - Data Pull
위에서 넘긴 ‘Data Push’와 반대되는 ‘Data Pull’이라는 표현이 등장했다.
이 두 표현은 말 그대로 데이터를 직접 주는 것과 데이터를 자신이 가져오는 것을 의미한다.
Push vs Pull
Push: Subject가 Observer에게 상태를 알리는 방식 - 자신이 알고 있는 모든 데이터를 보낸다.
Pull: Observer가 Subject로부터 상태를 가져오는 방식 - 자신이 필요한 데이터만 가져온다.
위에서는 Subject가 자신이 가지고 있는 모든 데이터를 Observer에 전달했다. 이것을 push라고 하겠다.
하지만, 이러한 방식(모두 전달)을 사용하게 되면, Observer는 불필요한 데이터도 전달 받게 된다.
또, Subject는 자신이 가지는 데이터가 확장되면 갱신하는 메서드도 일일이 고쳐야 한다.
기상 스테이션을 확장하여 수많은 디스플레이를 팔게 되었다고 하자.
수많은 디스플레이가 화면에 표시하는 항목이 모두 다르고, 또, 필요로 하는 데이터의 종류도 다르다면 어떨까?
모든 데이터를 push하는 것보다, 자신이 필요한 데이터는 가져오는 것(pull)이 더 좋을 것이다.
이 방법을 구현하는 것은 어렵지 않다.
Subject에 데이터에 관한 getter 메서드를 만들고, Observer에서는 필요한 데이터를 pull할 때 해당 메서드를 호출하면 된다.
코드를 바꿔보도록 하자.
구조도
수정 코드
WeatherData(Subject)의 notifyObservers 메서드가 Observer의 update 메서드를 인자 없이 호출하도록 수정한다.
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
Observer 인터페이스에서 update 메서드에 매개변수가 없도록 수정한다.
public interface Observer {
public void update();
}
Observer에서 자신이 필요한 데이터만 Subject로부터 pull하도록 구상 클래스의 update 메서드를 수정한다.
// CurrentConditionsDisplay의 update()
public void update() {
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
실행 결과
Data Push에서와 동일한 Main문을 실행하였습니다.
GitHub
전체 실습 코드는 아래 레포지터리의 ch2_OberserPattern 폴더에서 확인할 수 있습니다.