본문 바로가기
일상,취미

옵저버 패턴의 쓰임새

by 진득한진드기 2024. 4. 21.

옵저버 패턴은 듣기는 한번씩 들었던 패턴의 이름이다.

 

헤드 퍼스트 디자인 패턴에서는 바로 사전적 정의를 알려주는 것보다 메커니즘을 위주로 말하기에 이게 뭐지 싶은 생각이 먼저 들고 시작한다.

 

찾아본 정의가 조금씩 다 달라서 개인적으로 찾아본 사전적 정의는 다음과 같다.

 

 

"In software design and engineering, the observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods"

 

 

subject이름의 객체에서 observers라고 하는 종속적인 리스트를 유지하고, 일반적으로 메서드 중 하나를 호출해서 바뀌는 상태를 자동으로 알리는 패턴이라는 것 같다.

 

말만 들어서는 어지러우니 금방 코드를 봐보자

 

먼저 헤드퍼스트디자인 패턴 책에서는 처음에 이러한 코드를 제공했다.

 

class WeaterData{
public:
	void mesasurmentsChanged(){
            double temp = getTemperature();
       	    double humitity = getHumidity();
	    double pressure = getPressure();
        
           currentConditionsDisplay->update(temp,humidity,pressure);
	   statisticDisplay->update(temp,humidity,pressure);
           forecasDisplay->update(temp,humidity,pressure);
    }
};

 

 

이러한 코드는 디스플레이 객체가 추가될 때마다 update() 메서드를 계속 추가해줘야한다.

이는 전에 봤던 전략패턴에서와 같이 캡슐화가 필요하다.

 

하드 코딩하는건 생각보다 나쁘지 않다고 하는 친구가 있었는데 개인적인 생각으론.....프로젝트 규모가 커지면 골치아파지기 때문에 더욱 견고한 모델링을 기반으로 베이스를 다져놔야 이후에 유지보수할 때 좀 더 편할 것 같다.

 

이제 슬슬 옵저버 패턴을 넣어야한다.

 

옵저버 패턴은 간단하게 신문사 + 구독자 라고 할 수 있다.

 

신문사(Subject)를 구독자(observer)가 구독하는 형식으로 보면된다.

 

구독자들은 특정 주제를 구독하고 있고, 주제 데이터가 바뀌면 갱신 내용을 전달 받는다고 생각하자.

 

옵저버 패턴에서는 느슨한 결합을 유지하는 것이 좋은데, 느슨한 결합은 객체들이 상호작용은 되지만 서로 잘 모르는 관계를 의미한다.

 

굳이 느슨한 결합을 사용하는 이유는 느슨한 결합은 코드를 유지보수할 때 좀 더 유연해진다는 장점이 있다. 이외에도 옵저버 패턴에 사용하는 이유는 다음과 같다.

 

1. 옵저버가 특정 인터페이스를 구현한다는 사실만 알게한다.

2. 옵저버는 언제든지 새로 추가가 가능하다.

3. 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 전혀 없다.

4. 주제와 옵저버는 서로 독립적으로 재사용할 수 있다.

5. 주제나 옵저버가 달라져도 서로에게 영향을 미치지는 않는다.

 

여기서 통용되는 디자인 원칙을 알 수 있는데, 상호작용하는 객체들 사이에는 가능하면 느슨한 결합을 사용해야 한다는 것이다.

 

구독형식이기 때문에 기본적으로 일대다(one-to-many) 의존성을 정의한다.

 

우리가 여기서 볼 기상 가상 시뮬레이션 코드는 

 

WeatherData 클래스가 구독한 모두가 봐야할 데이터 즉, one이 되고 그 데이터들을 보는 구독자들이 display가 된다.

 

크게 설계하면 다음과 같다.

 

 

기상 시뮬레이션을 기반으로 옵저버 패턴의 틀을 구현해보자.

 

우리가 봐야할 주제에 관한 클래슨느 다음과 같다.

 

Subject 클래스

class Subject {
public:
	virtual void registerObserver(Observer *o);
    virtual void removeObserver(Observer o);
    virtual void notifyObservers();
};

class Observer {
public:
	virtual void update(double temp,double humidity, double pressure);
};

class Display {
public:
	virtual void display();
};

 

 

우리가 구독할 WeatherData 클래스는 어떻게 작성해야할까

 

먼저 Subject클래스를 보고 스스로 생각한 구조는 observers 라는 구독자들의 리스트가 존재하고 정형자료구조를 통해 바뀐 데이터들을 구독자들에게 알려줘야한다. 또 구독 취소를 하게되면 해당 구독자를 구독자 리스트에서 제거할 수 있어야한다.

 

이런 요구사항을 가지고 WeatherData 클래스를 작성하면 다음과 같이 작성할 수 있다.

 

WeatherData 클래스

class WeatherData : public Subject{
public:
    WeatherData() {
        observers = new vector<Observer *>();
    }
    ~WeatherData() {
        delete observers;
    }
    double get_temp(){
        return temp;
    }
    double get_hum(){
        return humidity;
    }
    double get_pre(){
        return pressure;
    }
    void registerObservers(Observer *o) override {
        observers->push_back(o);
    }
    void removeObservers(Observer *o) override {
        observers->erase(remove(observers->begin(), observers->end(), o), observers->end());
    }
    void notifyObservers() override {
        for(int i = 0;i<observers->size();i++){
            (*observers)[i]->update(temp,humidity,pressure);
        }
    }
    void measurementsChanged(){
        notifyObservers();
    }
    void setMeasurements(double temp, double humidity, double pressure) {
        this->temp = temp;
        this->humidity = humidity;
        this->pressure = pressure;
        measurementsChanged();
    }
private:
    vector<Observer *> *observers;
    double temp;
    double humidity;
    double pressure;
};

 

 

구독하는 구독자에 대한 코드는 어떻게 작성해야할까

 

구독하는 주제가 있다는 것은 당연히 그것을 구독하는 사람이 있어야 유효한 존재가 될 수 있다.

 

구독자는 주제에 대해 구독이 되어야하며, 구독한 주제에 대한 데이터세팅이 이루어지면 구독자들은 해당 데이터들에 대해 전부 알아야한다.

그렇게 되려면 해당 구독자의 인스턴스를 선언하게 되면 구독자 리스트안에 들어가게 되고 update를 하게되면 데이터가 변환되어야하고, 그것에 대한 정보를 보여주는 display 메서드로 이루어져있을 것이다.

 

이는 쉽다.

위에서 전체적인 모델링은 끝내놨으니 이에 맞춰주면 된다.

class CurrentConditionsDisplay : public Observer, public DisplayElement {
public :
    CurrentConditionsDisplay(WeatherData *weatherData) {
        this->weatherData = weatherData;
        weatherData->registerObservers(this);
    }
    void display() override {
        cout << "현재 온도 " << temp  << "F 습도 " << humidity << "%\n";
    }
    void update(double temp, double humidity, double pressure) override {
        this->temp = temp;
        this->humidity = humidity;
        this->pressure = pressure;
        display();
    }
private:
    double temp;
    double humidity;
    double pressure;
    WeatherData* weatherData;
};

 

 

전체 기본적인 틀은 완성 되었으니 한번 코드를 테스트 해보자.

 

int main() {
	WeatherData* weatherData = new WeatherData();

    CurrentConditionsDisplay* currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);

    weatherData->setMeasurements(80.0,65.0,30.4f);
    weatherData->setMeasurements(82.0,70.0,29.2f);
    weatherData->setMeasurements(78.0,90.0,29.2f);
    delete weatherData;
    delete currentConditionsDisplay;
	return 0;
}

 

 

 

데이터가 업데이트되고 잘 반영 되는 것을 확인할 수 있다.

 

다른 구독자를 만들어서 데이터를 추가해보자.

 

class StatusticsDisplay : public Observer, public DisplayElement {
public:
    StatusticsDisplay(WeatherData* weatherData) {
        this->weatherData = weatherData;
        weatherData->registerObservers(this);
    }
    void display() override {
        cout << "평균/최고/최저 온도 " << avgtemp  << " / " << maxtemp << " / " << mintemp << '\n';
    }
    void update(double temp, double humidty, double pressure) override {
        if(mintemp > temp){
            mintemp = temp;
        }
        if(maxtemp < temp){
            maxtemp = temp;
        }
        ++count;
        totaltemp += temp;
        avgtemp = totaltemp / count;
        display();
    }
private:
    double mintemp = 100000;
    double avgtemp;
    double maxtemp = -100000;
    double totaltemp;
    WeatherData* weatherData;
    int count = 0;
};
//자유 형식 인터페이스
class ThirdPartyDisplay : public Observer, public DisplayElement {
public:
     ThirdPartyDisplay(WeatherData* weatherData) {
         this->weatherData = weatherData;
         weatherData->registerObservers(this);
     }
     void update(double temp, double humidty, double pressure) override {

     }
     void display() override {

     }

};
class ForecastDisplay : public Observer, public DisplayElement {
public: 
    ForecastDisplay(WeatherData* weatherData) {
        this->weatherData = weatherData;
        weatherData->registerObservers(this);
    }
    void update(double temp, double humidty, double pressure) override {
        display();
        count++;
    }
    void display() override {
        switch(count){
        case 0:
            cout << "날씨가 좋아지고 있습니다\n";
            break;
        case 1:
            cout << "쌀쌀하며 비가 올 거 같습니다\n";
            break;
        case 2:
            cout << "지금과 비슷할 것 같습니다.\n";
            break;
        default:
            break;
        }
    }
private:
    WeatherData* weatherData;
    int condition[3];
    int count = 0;
};
int main() {
    WeatherData* weatherData = new WeatherData();

    CurrentConditionsDisplay* currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
    StatusticsDisplay* statusticsDisplay = new StatusticsDisplay(weatherData);
    ForecastDisplay* forecastDisplay = new ForecastDisplay(weatherData);

    weatherData->setMeasurements(80.0,65.0,30.4f);
    weatherData->setMeasurements(82.0,70.0,29.2f);
    weatherData->setMeasurements(78.0,90.0,29.2f);
    delete weatherData;
    delete currentConditionsDisplay;
    delete statusticsDisplay;
    delete forecastDisplay;
    return 0;
}

 

 

참으로 StatusticDisplay 클래스와 ForecastDisplay와 ThirdPartyDisplay는 직접 짠거이기에.... 가독성이 좋지 않을 수 도 있는점을 이해해주길....ㅎㅎ

 

 

각자 오버라이딩을 통해 동작은 다르지만 구독한 곳의 데이터를 이용하여 보여주고자 하는 잘 가공해서 보여주는 것을 알 수 있다.

 

이 처럼 옵저버 패턴파트는 우리는 2가지의 중요한 점을 배웠다.

 

첫번째는 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야하는 것과 

두번째는 옵저버 패턴은 한 객체의 상태가 바뀌면 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 가지는 디자인패턴이라는 것이다.

 

전략패턴을 완벽히 이해했다면 생각보다 원리를 쉽게 파악할 수 있는 패턴이었다.

 

상기해보면 옵저버 패턴은 특정 주제에 대해 구독자들이 구독하는 시스템과 같고, 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 내용이 갱신되는 방식이다.

 

위 시뮬레이션에서는 WeatherData에 대한 데이터가 추가되면 그를 구독한 구독자들은 변경되는 데이터를 확인할 수 있는 상황을 볼 수있었다. 

 

다음엔 데코레이션 패턴을 알아보려고 한다.