헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 2. 객체들에게 연락 돌리기 - 옵저버 패턴
기상 모니터링 애플리케이션 알아보기
- 이 시스템은 기상 스테이션(실제 기상 정보를 수집하는 장비), WeatherData 객체(기상 스테이션으로부터 오는 정보를 추적하는 객체), 사용자에게 현재 기상 조건을 보여 주는 디스플레이 장비 3가지 요소로 이루어진다고 가정한다.
- WeatherData 객체는 물리 기상 스테이션과 통신해서 갱신된 기상 데이터를 가져온다.
- 디스플레이를 업데이트하려면 우선 WeatherData 객체를 고쳐야 한다.
WeatherData 클래스 살펴보기
| WeatherData |
| -------------------- |
| getTemperature() |
| getHumidity() |
| getPressure() |
| measurementChanged() |
/*
* 온도, 습도, 기압 값 등 기상 관측값을 새로 받을 때마다 메서드가 호출됩니다.
*/
public void measurementsChanged() {
// 코드가 들어갈 자리
}
구현 목표
- 디스플레이를 구현하고 새로운 값이 들어올 때마다, 즉
measurementsChanged()
메서드가 호출될 때마다 WeatherData에서 디스플레이를 업데이트해야 한다는 사실을 파악했다. - 조금 더 자세히 따져보자.
- WeatherData 클래스에는 3가지 측정값 (온도, 습도, 기압)의 Getter 메서드가 존재한다.
- 새로운 기상 측정 데이터가 들어올 때마다
measurementsChanged()
메서드가 호출된다. (어떤 식으로 호출되는지 모르며, 그냥 메서드가 호출된다는 사실만 알고 있다.) - 기상 데이터를 사용하는 디스플레이 요소 3가지를 구현해야 한다. 하나는 현재 조건 디스플레이, 다른 하나는 기상 통계 디스플레이, 마지막은 기상 예보 디스플레이이다. WeatherData에서 새로운 측정값이 들어올 때마다 디스플레이를 갱신해야 한다.
- 디스플레이를 업데이트하도록
measurementsChanged()
메서드에 코드를 추가해야 한다.
추가 목표
- 소프트웨어 개발에서 바뀌지 않는 단 하나: 변화를 생각해야 한다.
- 나중에 기상 스테이션이 성공하면 디스플레이가 더 늘어날 수도 있고, 디스플레이를 추가할 수 있는 마켓플레이스가 만들어질지도 모른다.
- 확장 기능을 추가해본다면?
확장성
- 지금은 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);
}
// 기타 메서드
}
원칙적으로 추가 코드 살펴보기
currentConditionDisplay.update(temp, humidity, pressure);
statisticDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
- 위 부분은 바뀔 수 있는 부분이므로 캡슐화해야 한다.
- 디스플레이 항목과 데이터를 주고받는 데 공통된 인터페이스를 사용하고 있는 것으로 보인다.
- 모두 온도, 습도, 기압 값을 받아들이는
update()
메서드를 가지고 있으므로
- 모두 온도, 습도, 기압 값을 받아들이는
- 실행 중에 디스플레이를 더하거나 빼려면 어떻게 해야 할까?
- 디스플레이 항목과 데이터를 주고받는 데 공통된 인터페이스를 사용하고 있는 것으로 보인다.
옵저버 패턴 이해하기
- 신문 구독 메커니즘을 제대로 이해한다면 옵저버 패턴을 쉽게 이해할 수 있다.
- 신문사 + 구독자 = 옵저버 패턴
- 신문사를 주제(subject), 구독자를 옵저버(observer)
- 주제 객체
- 주제에서 중요한 데이터를 관리한다.
- 주제 데이터가 바뀌면 옵저버 객체에게 그 소식이 전해진다.
- 데이터가 바뀌면 새로운 데이터 값이 어떤 방법으로든 옵저버 객체에게 전달된다.
- 옵저버 객체
- 옵저버 객체들은 주제를 구독하고 있으며 (주제 객체에 등록되어 있으며) 주제 데이터가 바뀌면 갱신 내용을 전달받는다.
옵저버 패턴의 정의
- 옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 (One-to-Many) 의존성을 정의한다.
- 주제 객체와 옵저버 객체로 일대다 관계가 정의된다.
- 옵저버 패턴은 여러 가지 방법으로 구현할 수 있지만, 보통 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인으로 구현한다.
옵저버 패턴의 구조
- Subject (인터페이스)
- 주제를 나타내는 Subject 인터페이스
- 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때는 이 인터페이스 안에 있는 메서드를 사용한다.
| Subject | | ------------------ | | registerObserver() | | removeObserver() | | notifyObserver() |
- Observer (인터페이스)
- 옵저버가 될 가능성이 있는 객체는 반드시 Observer 인터페이스를 구현해야 한다.
- 이 인터페이스에는 주제의 상태가 바뀌었을 때 호출되는
update()
메서드밖에 없다. - 각 주제마다 여러 개의 옵저버가 있을 수 있다.
| Observer | | -------- | | update() |
- ConcreteObserver
- Observer 인터페이스만 구현한다면 무엇이든 옵저버 클래스가 될 수 있다.
- 각 옵저버는 특정 주제에 등록해서 연락할 수 있다.
| ConcreteObserver | | --------------------- | | update() | | // 기타 옵저버용 메서드 |
- ConcreteSubject
- 주제 역할을 하는 구상 클래스에는 항상 Subject 인터페이스를 구현해야 한다.
- 주제 클래스에는 등록 및 해지용 메서드와 상태가 바뀔 때마다 모든 옵저버에게 연락하는
notifyObserver()
메서드로 구현해야 한다. - 주제 클래스에는 상태를 설정하고 알아내는 Setter/Getter 메서드가 들어있을 수 있다.
| ConcreteSubject | | ------------------------ | | registerObserver() {...} | | removeObserver() {...} | | notifyObserver() {...} | | getState() | | setState() |
느슨한 결합의 위력
- 느슨한 결합 (Loose Coupling)은 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계를 의미한다.
- 옵저버 패턴은 느슨한 결합을 보여주는 대표적인 예시이다.
- 옵저버 패턴에서 어떻게 느슨한 결합을 만들까?
주제는 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현한다는 사실만 압니다
- 옵저버의 구상 클래스가 무엇인지, 옵저버가 무엇을 하는지는 알 필요 없다.
옵저버는 언제든지 새로 추가할 수 있다.
- 주제는 Observer 인터페이스를 구현하는 객체의 목록에만 의존하므로 언제든지 새로운 옵저버를 추가할 수 있다.
- 실행 중에 하나의 옵저버를 다른 옵저버로 바꿔도 주제는 계속해서 다른 옵저버에 데이터를 보낼 수 있다.
- 마찬가지로 아무 때나 옵저버를 제거해도 된다.
새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 전혀 없다.
- 옵저버가 되어야 하는 새로운 구상 클래스가 생겼을 때도, 새로운 클래스 형식을 받아들일 수 있도록 주제를 바꿔야 할 필요 없다.
- 새로운 클래스에서 Observer 인터페이스를 구현하고 옵저버로 등록하기만 해도 된다.
주제와 옵저버는 서로 독립적으로 재사용할 수 있다.
- 주제나 옵저버를 다른 용도로 활용할 일이 있다고 해도 손쉽게 재사용할 수 있다.
주제나 옵저버가 달라져도 서로에게 영향을 미치지 않는다.
- 주제나 옵저버 인터페이스를 구현한다는 조건만 만족한다면 어떻게 고쳐도 문제가 생기지 않는다.
디자인 원칙 4
- 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
- 느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.
- 객체 사이의 상호의존성을 최소화할 수 있기 때문
기상 스테이션 설계하기
기상 스테이션 프로젝트에 옵저버 패턴 적용하기
- 옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 일대다 의존성을 정의한다.
- WeatherData 클래스가 일(one)에 해당하고, 디스플레이 요소는 다(many)에 해당한다.
- WeatherData 객체가 주제가 되고 디스플레이 요소가 옵저버가 되면 디스플레이에서 원하는 정보를 얻기 위해 WeatherData 객체에 등록한다.
- 모든 디스플레이 요소가 다를 수 있으므로 공통적인 인터페이스를 사용해야 한다.
- 모든 디스플레이에 WeatherData에서 호출할 수 있는
update()
메서드가 있어야 한다. update()
는 공통 인터페이스에서 정의해야 한다.
- 모든 디스플레이에 WeatherData에서 호출할 수 있는
기상 스테이션 설계 만들어보기
- WeatherData는 Subject 인터페이스를 구현한다.
| WeatherData | | --------------------- | | registerObserver() | | removeObserver() | | notifyObserver() | | | | getTemperature() | | getHumidity() | | getPressure() | | measurementsChanged() |
- 모든 가상 구성 요소에 Observer 인터페이스를 구현한다.
- 이 인터페이스는 주제에서 옵저버에게 갱신된 정보를 전달하는 방법을 제공한다.
| <<Interface>> | | Observer | | -------------- | | update() |
- 모든 디스플레이 요소에 구현 인터페이스를 하나 더 만든다.
- 디스플레이 항목에는
display()
메서드만 구현한다.| <<Interface>> | | DisplayElement | | -------------- | | display() |
- 디스플레이 항목들을 각각 구현한다.
| CurrentConditionDisplay | | ------------------------- | | update() | | display() | | { // 측정값을 화면에 표시 } |
| StatisticsDisplay | | ------------------------------ | | update() | | display() | | { // 평균, 최저, 최고값 표시 } |
| ForecastDisplay | | ----------------- | | update() | | display() | | { // 기상 예보 } |
기상 스테이션 구현하기
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
public interface DisplayElement {
public void display();
}
}
Subject 인터페이스 구현체
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<Observer>();
}
/*
* 옵저버가 등록을 요청하면 추가해주는 메서드
*/
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
/*
* 옵저버가 탈퇴를 요청하면 빼주는 메서드
*/
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
/*
* 가장 중요한 부분
* 모든 옵저버에게 상태 변화를 알려주는 부분
* 모두 Observer 인터페이스를 구현하여 update() 메서드가 있는 객체들이므로 손쉽게 상태 변화를 알려줄 수 있다.
*/
@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();
}
// 기타 WeatherData 메서드
}
디스플레이 요소 구현체
public class CurrentConditionDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData;
/*
* 생성자에 WeatherData 주제가 전달되며 그 객체를 써서 디스플레이를 옵저버로 등록한다.
* 주제 레퍼런스를 미리 저장하면 나중에 옵저버 목록에서 탈퇴할 때 유용하게 사용할 수 있다.
*/
public CurrentConditionDisplay(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("현재 상태: 온도 %f F, 습도 %f %".formatted(temperature, humidity));
}
}
update() 메서드에서 display() 메서드를 호출하는 방법이 최선일까?
- 위 예제에서는 값이 바뀔 때마다 display()를 호출해주는 방법이 괜찮아보이지만 최선의 방법은 아니다.
- 데이터를 화면에 표시하는 더 좋은 방법은 모델-뷰-컨트롤러 패턴에서 더 자세히 알아볼 것
라이브러리 속 옵저버 패턴 알아보기
스윙 라이브러리
- 스윙 툴킷 중 하나인 JButton 클래스
- JButton의 슈퍼클래스인 AbstractButton을 찾아보면 리스너를 추가하고 제거하는 메서드가 잔뜩 들어있다.
- 이 메서드들은 스윙 구성 요소에서 일어나는 다양한 유형의 이벤트를 감시하는 옵저버를 추가하거나 제거하는 역할을 한다.
- (스윙 라이브러리에서는 리스너라고 함)
자바빈
- 자바빈에 있는 옵저버 패턴이 궁금하다면
PropertyChangeListener
인터페이스를 확인해보자.
자바의 Observable 클래스?
- 자바에는 옵저버 패턴용 Observable 클래스 (주제 클래스)와 Observer 인터페이스가 존재했다.
- Observable 클래스는 직접 코드를 작성하지 않아도 옵저버를 추가하고 삭제하고 옵저버에게 알림을 보내는 메서드를 제공했다.
- Observer 인터페이스는
update()
메서드를 포함하여 앞서 만들었던 것과 유사한 인터페이스를 제공했다.
- 이 클래스들은 자바 9 이후로 쓰이지 않는다.
- 각자 코드에서 더 강력한 기능을 스스로 구현하는 게 낫겠다고 생각하는 사람들이 늘면서 사라지게 되었다.
풀 (pull) 방식으로 코드 바꾸기
푸시를 풀로 바꾸는 건 좋은 생각이다
- 사실 주제가 옵저버로 데이터를 보내는 푸시(push)를 사용하거나 옵저버가 주제로부터 데이터를 당겨오는 풀(pull)을 사용하는 방법 중 하나를 선택하는 일은 구현 방법의 문제로 볼 수 있다.
- 대체로 옵저버가 필요한 데이터를 골라서 가져가도록 만드는 방법이 더 좋다.
- 값이 변했다는 알림을 옵저버가 받았을 때 주제에 있는 Getter 메서드를 호출해 필요한 값을 당겨오도록 하자.
1. 주제에서 알림 보내기
- 옵저버의
update
메서드를 인자 없이 호출하도록 WeatherData의notifyObservers()
메서드를 수정한다.public void notifyObservers() { for (Observer observer : observers) { observer.update(); } }
2. 옵저버에서 알림 받기
- Observer 인터페이스에서
update()
메서드에 매개변수가 없도록 서명을 수정해준다.public interface Observer { public void update(); }
update()
메서드의 서명을 바꾸고 WeatherData의 Getter 메서드로 주제의 기상 데이터를 가져오도록 각 Observer 구현 클래스를 수정한다.CurrentConditionsDisplay
클래스 코드는 다음과 같이 수정한다.public void update() { this.temperature = weatherData.getTemperature(); this.humidity = weatherData.getHumidity(); display(); }