ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디자인 패턴 스터디 기록 (2) - 옵저버 패턴
    ✏️ 스터디 모음집/디자인 패턴 스터디 2022. 10. 27.

    옵저버 패턴이란?

    한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고,

    자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의하는 방식

     

    새해가 되면 친구 목록에 저장 된 지인들에게 새해 인사를 돌리며 자신의 근황 같은 것을 전하시나요? 그럼 당신은 Subject인 것 입니다.

     

    옵저버 패턴의 구조

     

    Subject 객체 와 Observer객체로 구성됨

     

    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가 구독하고 해지하고 할수 있도록 구조를 짜보자!

    1. 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();
    

     

    1. 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
      });
    }
    

     

    1. 실행 부분
    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 버전으로 옵저버 패턴 구현해보기

    Design Patterns - JavaScript

     

    옵저버 패턴의 특징을 정리 해보면…

    더보기

    ✋ 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/

     

    댓글

GitHub: https://github.com/Yesung-Han