옵저버 패턴
디자인 원칙
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
구현보다는 인터페이스에 맞춰서 프로그래밍한다.
상속보다는 구성을 활용한다.
상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
옵저버 패턴(Observer Pattern)은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.
요구사항 및 문제점
습도, 온도, 기압을 측정하는 측정기가 있다고 가정해 보자. WeatherData라는 객체는 측정기에서 데이터를 취득 후 화면에 표시하는 역할을 담당한다. 측정기에서 습도, 온도, 기압이 변하면 WeatherData 객체로 데이터가 전달되고, 자동으로 디스플레이 장비 화면에 표시되는 것이 요구사항이다.

즉 새로운 값이 들어올 때마다 WeatherData 객체의 measurementsChanged() 메서드가 호출되며 디스플레이가 업데이트되어야 한다.
그렇다면 measurementsChanged() 메서드를 어떻게 구현해야 할까? 옵저버 패턴을 적용하지 않는다면 다음과 같을 것이다.
package observer;
import java.util.ArrayList;
import java.util.List;
public class WeatherData implements Subject{
private List<Observer> observerList;
private Double temporature;
private Double humidity;
private Double pressure;
public void measurementsChanged(Double temporature, Double humidity, Double pressure) {
//변화한 부분 업데이트
this.temporature = temporature;
this.humidity = humidity;
this.pressure = pressure;
//디스플레이 갱신한다. 디스플레이가 추가될때마다 해당 코드도 수정해야 한다.
currentConditionDisplay.update(temporature, humidity, pressure);
statisticsDisplay.update(temporature, humidity, pressure);
forecastDisplay.update(temporature, humidity, pressure);
}
}
위와 같이 코드를 작성했을 시 4가지 문제점이 있다.
1. 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩이 이뤄지고 있다.
2. 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
3. 실행 중에 디스플레이 항목을 추가하거나 제거할 수 없다.
4. 바뀌는 부분을 캡슐화하지 않았다.
이 문제를 해결하기 위해 옵저버 패턴을 적용해야 한다.
옵저버 패턴 적용 및 설계
옵저버 패턴이란 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.
그렇다면 어떻게 설계해야 할까? 다음은 옵저버 패턴을 적용한 설계도이다.

구현하기
위 설계도에 맞춰 코드를 구현해 보겠다.
package observer;
public interface Subject {
// Observer를 인자로 받아 각각 옵저버를 등록하고 제거하는 역할을 담당한다.
public void registerObserver(Observer o);
public void removeObserver(Observer o);
// 주제의 상태가 변경되었을 때 모든 옵저버에게 내용을 알릴때 호출되는 메서드이다.
public void notifyObserver();
}
package observer;
public interface Observer {
//기상 정보가 변경되었을 때 옵저버에게 전달되는 상태값들이다.
public void update(Double temperature, Double humidity, Double pressure);
}
인터페이스에 맞춰 구현한 내용은 다음과 같다.
package observer;
import java.util.ArrayList;
import java.util.List;
//Subject 인터페이스 구현
public class WeatherData implements Subject{
//Observer 객체를 저장하는 ArrayList를 추가후 생성자에서 구현
private List<Observer> observerList;
private Double temporature;
private Double humidity;
private Double pressure;
public WeatherData() {
observerList = new ArrayList<>();
temporature = 10.0;
humidity = 10.0;
pressure = 10.0;
}
//옵저버 등록 요청하면 리스트에 추가한다.
@Override
public void registerObserver(Observer o) {
observerList.add(o);
}
@Override
public void removeObserver(Observer o) {
observerList.remove(o);
}
//옵저버 페턴의 핵심 부분이다. 모든 옵저버에게 현재 상태가 변화됐다는 사실을 알려주는 부분이다.
//모두 Observer 인터페이스를 구현한 인스턴스이기 때문에 모두 update() 메서드가 있을 것이다.
//따라서 손쉽게 변화를 알려줄 수 있다.
@Override
public void notifyObserver() {
for(Observer observer : observerList) {
observer.update(temporature, humidity, pressure);
}
}
//측정계로부터 갱신된 새 값을 받으면 옵저버들에게 알려준다.
public void measurementsChanged(Double temporature, Double humidity, Double pressure) {
this.temporature = temporature;
this.humidity = humidity;
this.pressure = pressure;
notifyObserver();
}
public Double getTemporature() {
return temporature;
}
public Double getHumidity() {
return humidity;
}
public Double getPressure() {
return pressure;
}
//setter 메서드 또한 변화가 발생하기 때문에 옵저버를 호출해야 한다.
public void setTemporature(Double temporature) {
this.temporature = temporature;
notifyObserver();
}
public void setHumidity(Double humidity) {
this.humidity = humidity;
notifyObserver();
}
public void setPressure(Double pressure) {
this.pressure = pressure;
notifyObserver();
}
}
package observer;
//WeatherData로부터 변경 사항을 받으려면 Observer를 구현해야 한다.
public class CurrentConditionDisplay implements Observer {
private WeatherData weatherData;
private Double temporature;
private Double humidity;
private Double pressure;
//생성자에 weatherData라는 주제가 전달되면, 그 객체를 디스플레이 옵저버로 등록한다.
public CurrentConditionDisplay(WeatherData weatherData) {
weatherData.registerObserver(this);
this.weatherData = weatherData;
}
//update가 호출되면 새 값을 저장한 후 display()를 호출한다.
//display()는 변화가 일어났을 때 호출하는 값으로 callback함수와 같은 역할을 담당한다.
@Override
public void update(Double temperature, Double humidity, Double pressure) {
this.temporature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
//사용자의 조건에 맞춰 display()를 구현한다.
public void display() {
System.out.println("current temporature : " + temporature + " current humidity : " + humidity + " current pressure : " + pressure);
}
}
이제 작성한 코드를 테스트해 보겠다. 테스트 코드는 다음과 같다.
package observer;
public class Main {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
weatherData.setHumidity(11.1);
weatherData.setPressure(9.5);
weatherData.setTemporature(13.5);
weatherData.measurementsChanged(10.0, 10.0, 10.0);
}
}
[출력된 결괏값]

weatherData의 값이 바뀔 때마다 currentConditionDisplay의 display() 메서드가 새롭게 호출되는 것을 확인할 수 있다.
PULL 방식으로 변경
현재 코드는 WeatherData가 변경될 때마다 모든 데이터를 옵저버에게 push하는 방법을 사용하고 있다. 즉 주제가 옵저버에게 상태를 알리는 방식을 사용하고 있다. 잘못된 방식은 아니지만 옵저버가 필요한 데이터만 골라서 가져가도록 만드는 방법이 더 권장된다. 이를 pull 방식, 옵저버가 주제로부터 상태를 끌어오는 방식이라고 부른다.
pull 방식이 적용된 코드는 다음과 같다.
옵저버 인터페이스의 update() 메서드에 매개변수가 없도록 변경한다.
package observer;
public interface Observer {
public void update();
}
마찬가지로 옵저버가 인자 없이 호출되도록 WeatherData의 nofifyObserver() 메서드를 변경한다.
package observer;
import java.util.ArrayList;
import java.util.List;
public class WeatherData implements Subject{
private List<Observer> observerList;
private Double temporature;
private Double humidity;
private Double pressure;
public WeatherData() {
observerList = new ArrayList<>();
temporature = 10.0;
humidity = 10.0;
pressure = 10.0;
}
@Override
public void registerObserver(Observer o) {
observerList.add(o);
}
@Override
public void removeObserver(Observer o) {
observerList.remove(o);
}
@Override
public void notifyObserver() {
for(Observer observer : observerList) {
observer.update();
}
}
public void measurementsChanged(Double temporature, Double humidity, Double pressure) {
this.temporature = temporature;
this.humidity = humidity;
this.pressure = pressure;
notifyObserver();
}
public Double getTemporature() {
return temporature;
}
public Double getHumidity() {
return humidity;
}
public Double getPressure() {
return pressure;
}
}
마지막으로 옵저버를 구현한 클래스에서 update() 메서드를 매개변수가 없도록 변경하고 WeatherData의 getter 메서드로 주제의 데이터를 가져오도록 수정한다.
package observer;
public class CurrentConditionDisplay implements Observer {
private WeatherData weatherData;
private Double temporature;
private Double humidity;
private Double pressure;
public CurrentConditionDisplay(WeatherData weatherData) {
weatherData.registerObserver(this);
this.weatherData = weatherData;
}
@Override
public void update() {
this.temporature = weatherData.getTemporature();
this.humidity = weatherData.getHumidity();
this.pressure = weatherData.getPressure();
display();
}
public void display() {
System.out.println("current temporature : " + temporature + " current humidity : " + humidity + " current pressure : " + pressure);
}
}
결론
옵저버 패턴을 이해하기 위해 다음 객체지향 원칙을 알아야 한다.
1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
3. 상속보다는 구성을 활용한다.
4. 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
'디자인패턴' 카테고리의 다른 글
| [헤드퍼스트 디자인패턴] 커맨드 패턴 (0) | 2024.08.17 |
|---|---|
| [헤드퍼스트 디자인패턴] 싱글턴 패턴 (0) | 2024.08.13 |
| [헤드퍼스트 디자인패턴] 팩토리 패턴 (0) | 2024.08.12 |
| [헤드퍼스트 디자인패턴] 데코레이터 패턴 (0) | 2024.08.06 |
| [헤드퍼스트 디자인패턴] 전략 패턴 (0) | 2024.04.14 |