-
디자인 패턴 스터디 기록 (2) - 옵저버 패턴✏️ 스터디 모음집/디자인 패턴 스터디 2022. 10. 27.
옵저버 패턴이란?
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고,
자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의하는 방식
옵저버 패턴의 구조
Subject 객체
Subject가 알고 있는 것(멤버 변수, 프로퍼티)
→ Observer들이 관심있어하는 타깃 인 상태값을 가지고 있음
→ Observer들을 저장 할 컨테이너를 가지고 있음
Subject 하는 일(멤버 함수, 메소드)
→ Observer를 컨테이너에 추가
→ Observer를 컨테이너에 삭제
→ 상태값이 바뀌었을때 컨테이너를 순회하며 등록된 옵저버들에게 알리기
→ 상태값에 대한 getter와 setter가 있을 수 있다.
Observer 객체
Observer가 알고 있는 것(멤버 변수, 프로퍼티)
→ 없다.
Observer 하는 일(멤버 함수, 메소드)
→ 알림을 받았을때 수행 할 일이 정해져있다.
책에 나온 예시 따라해보기 :
다음과 같은 상황을 생각해보자
단순히 생각하면 이런 구조를 떠 올릴 수 있다.
public class WeatherData { 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가 추가되면 여기도 또 추가 해야함. 😓 } }
⇒ 만약 새로운 종류의 디스플레이가 추가되면 WeatherData 클래스 코드 에도 그 디스플레이를 업데이트 하는 코드를 심어줘야 한다.
⇒ 옵저버 패턴을 적용해보자
public interface Subject { public void registerObserver(Observer o); public void removeObserver(Observer o); public void notifyObservers(); } public interface Observer { void update(float temperature, float humidity, float pressure); } public interface DisplayElement { public void display(); }
- Subject 구성
package ObserverPattern; public interface Subject { public void registerObserver(Observer o); public void removeObserver(Observer o); public void notifyObservers(); }
package ObserverPattern; import java.util.ArrayList; import java.util.List; public class WeatherData implements Subject { private float temperature; // Subject가 알고 있는 것 1 : 타깃이 되는 상태값 들 private float humidity; private float pressure; private List<Observer> observers; // Subject가 알고 있는 것 2 : 옵저버 담을 컨테이너 public WeatherData() { observers = new ArrayList<Observer>(); } @Override // Subject가 하는 일 1 : 옵저버를 컨테이너에 등록 public void registerObserver(Observer o) { observers.add(o); } @Override // Subject가 하는 일 2 : 옵저버를 컨테이너에서 뺌 public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers() { // Subject가 하는 일 3 : 컨테이너를 순회 하며 옵저버에게 알리기 for(Observer observer: observers) { observer.update(temperature, humidity, pressure); // 상태값들을 Object에게 push 해준다 } } public void measurementsChanged() { notifyObservers(); } // 그 외 상태값을 다루기위한 getter setter public void setWeatherData(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; measurementsChanged(); } public float getTemperature() { return this.temperature; } public float getHumidity() { return this.humidity; } public float getPressure() { return this.pressure; } }
- Observer 구성
package ObserverPattern; public interface Observer { void update(float temperature, float humidity, float pressure); // Observer 하는일 : 알림을 받았을때 수행 할 일이 정해져있다. }
package ObserverPattern; public interface DisplayElement { // 이건 그냥 println 표시를 위한 인터페이스 public void display(); }
package ObserverPattern.display; import ObserverPattern.DisplayElement; import ObserverPattern.Observer; public class CurrentConditionsDisplay implements Observer, DisplayElement { private float temperature; private float humidity; @Override // Observer 추상 메서드 구현 public void update(float temperature, float humidity, float pressure) { // Subject로 부터 push 받은 데이터 가지고 각자의 일 수행 this.temperature = temperature; this.humidity = humidity; display(); } @Override // DisplayElement 추상 메서드 구현 public void display() { System.out.println("현재 상태: 온도 "+temperature+"F, 습도 "+humidity+"%"); } }
- 실행 부분
public static void main(String[] args) { WeatherData weatherData = new WeatherData(); // Subject 인스턴트 생성 Observer currentConditionsDisplay = new CurrentConditionsDisplay(); // Observer 인스턴트 생성 Observer statisticsDisplay = new StatisticsDisplay(); // Observer 인스턴트 생성 weatherData.registerObserver(currentConditionsDisplay); // Observer를 Subject에 등록 weatherData.setWeatherData(3, 5, 7); System.out.println("통계 디스플레이를 추가합니다."); weatherData.registerObserver(statisticsDisplay); // Observer 를 Subject에 등록 System.out.println("기상 데이터가 업데이트 됩니다."); weatherData.setWeatherData(20, 30, 80); System.out.println("현재 상태 디스플레이를 제거합니다."); weatherData.removeObserver(currentConditionsDisplay); // Observer 를 Subject에서 제거 weatherData.setWeatherData(25, 30, 80); }
옵저버 데이터 방식의 푸시(push) vs 풀(pull)
💡 지금 만들어 놓은 WeatherData 디자인은 하나의 데이터만 갱신해도 되는 상황에서도 update 메소드에 모든 데이터를 보내도록 되어 있습니다. 만약 풍속 같은 새로운 데이터가 추가되면 대부분의 update 메소드에서 새로 추가된 그 풍속 데이터를 쓰지 않더라도 모든 update 메소드를 바꿔야 한다. 대체로 옵저버가 필요한 데이터만 골라오도록 하는 pull 방법이 더 나은 방법!
풀방식으로 바꿔보기
// Observer.java public interface Observer { void update(); // (1) Observer에서는 이제 알림 받을때 인수 안 받음 } // WeatherData.java @Override public void notifyObservers() { for(Observer observer: observers) { observer.update(); // (2) Observer가 인수 받았던 부분 삭제 } }
package ObserverPattern.display; import ObserverPattern.DisplayElement; import ObserverPattern.Observer; import ObserverPattern.WeatherData; public class CurrentConditionsDisplay implements Observer, DisplayElement { private float temperature; private float humidity; private WeatherData weatherData; // (3) 옵저버도 이제 서브젝트를 알고 있음! -> 서브젝트도 옵저버를 알고, 옵저버도 서브젝트를 아는 상황이 되었음. public CurrentConditionsDisplay(WeatherData weatherData) { this.weatherData = weatherData; } @Override public void update() { this.temperature = weatherData.getTemperature(); // (4) update가 호출 될 때 옵저버가 알고있는 서브젝트에서 직접 꺼내온다. this.humidity = weatherData.getHumidity(); display(); } @Override public void display() { System.out.println("현재 상태: 온도 "+temperature+"F, 습도 "+humidity+"%"); } }
🤔 옵저버도 이제 서브젝트를 알고 있음!
-> 서브젝트도 옵저버를 알고, 옵저버도 서브젝트를 아는 상황이 되었음.
→ 순환참조가 일어나는 상황인데 과연 이 방식이 좋은 방식일까?
JS로 옵저버 패턴 구현해보기
옵저버 패턴을 적용해서 유저가 버튼이나 스위치를 조작하는 이벤트를 다른 UI가 구독하고 해지하고 할수 있도록 구조를 짜보자!
- Subject 구성
// Observable.js class Observable { constructor() { // 이 예제에서는 Subject가 직접 상태값을 가지고 있지 않고있다. this.observers = []; // Subject가 알고 있는 것 2 : Observer 담을 컨테이너 } subscribe(f) { this.observers.push(f); // Subject가 하는 일 1 : Observer를 컨테이너에 등록 } unsubscribe(f) { this.observers = this.observers.filter(subscriber => subscriber !== f); // Subject가 하는 일 2 : Observer를 컨테이너에서 삭제 } notify(data) { this.observers.forEach(observer => observer(data)); // Subject가 하는 일 3 : Observer들에게 알림 } } export default new Observable();
- Observer 구성
// App.js // Observer들은 그냥 Subject로 부터 data를 받으면 일을 수행하는 함수로 구성했다. function logger(data) { console.log(`${Date.now()} ${data}`); } function toastify(data) { toast(data, { position: toast.POSITION.BOTTOM_RIGHT, closeButton: false, autoClose: 2000 }); }
- 실행 부분
import React from "react"; import { Button, Switch, FormControlLabel } from "@material-ui/core"; import { ToastContainer, toast } from "react-toastify"; import observable from "./Observable"; function handleClick() { observable.notify("User clicked button!"); // (2) 버튼이 클릭 되었을때 Subject를 통해 등록된 Observer 들에게 데이터를 전달함. } function handleToggle() { observable.notify("User toggled switch!"); } function logger(data) { console.log(`${Date.now()} ${data}`); } function toastify(data) { toast(data, { position: toast.POSITION.BOTTOM_RIGHT, closeButton: false, autoClose: 2000 }); } observable.subscribe(logger); // (1) Subject에 옵저버를 등록하는 부분 observable.subscribe(toastify); export default function App() { return ( <div className="App"> <Button variant="contained" onClick={handleClick}> Click me! </Button> <FormControlLabel control={<Switch name="" onChange={handleToggle} />} label="Toggle me!" /> <ToastContainer /> </div> ); }
직접 실행해 보기 :
https://codesandbox.io/embed/quizzical-sinoussi-md8k5
ES5 버전으로 옵저버 패턴 구현해보기
옵저버 패턴의 특징을 정리 해보면…
더보기✋ 1 대 다 관계
- 하나의 Subject가 여러개의 Object를 알고 있다.
✋ 느슨한 결합
✋ 출판-구독 패턴이랑 비숫하지만 다름.
CF.
옵저버 패턴 VS 출판-구독 패턴
가장 큰 차이점은 중간에 Message Broker 또는 Event Bus 가 존재하는지 여부입니다.
Observer패턴은 Observer와 Subject가 서로를 인지하지만 Pub-Sub패턴의 경우 서로를 전혀 몰라도 상관없습니다.
Observer패턴의 경우 Subject에 Observer를 등록하고 Subject가 직접 Observer에 직접 알려주어야 합니다.
Pub-Sub패턴의 경우 Publisher가 Subscriber의 위치나 존재를 알 필요없이 Message Queue와 같은 Broker역활을 하는 중간지점에 메시지를 던져 놓기만 하면 됩니다.
반대로 Subscriber 역시 Publisher의 위치나 존재를 알 필요없이 Broker에 할당된 작업만 모니터링하다 할당 받아 작업하면 되기 때문에 Publisher와 Subscriber가 서로 알 필요가 없습니다.
https://jistol.github.io/software engineering/2018/04/11/observer-pubsub-pattern/
'✏️ 스터디 모음집 > 디자인 패턴 스터디' 카테고리의 다른 글
디자인 패턴 스터디 기록 (6) - 싱글톤 패턴 (0) 2022.11.02 디자인 패턴 스터디 기록 (5) - 팩토리 패턴, 의존성 역전 (0) 2022.11.02 디자인 패턴 스터디 기록 (4) - 데코레이터 패턴 (0) 2022.11.02 디자인 패턴 스터디 기록 (3) - 객체 간의 강한 결합 VS 느슨한 결합 (0) 2022.10.27 디자인 패턴 스터디 기록 (1) (2) 2022.10.27