C++로 Observer Pattern 구현해보기

Kang Chang Hwan·2024년 5월 23일

CS - Design-Pattern

목록 보기
2/6

"Subject(뉴스 발행자)야 내(옵저버)가 필요한 데이터를 너가 업데이트 해주니까 구독좀 할게(registerObserver)"
"Subject: 띠링~ 새로운 신문이 발간됐어!(NotifyObservers)"
"Observer: 오! 고마워 해당 신문 좀 읽어봐야 겠네 ㅋ (update) 그리고 내가 게재하는 블로그에 게재해야겠어(display)"

더 자세히 알아보자.

첫 옵저버 패턴 구현

  1. Subject(옵저버 등록, 삭제 관리 및 옵저버에게 필요한 데이터 제공)
  2. Observer(Subject의 데이터를 받아서 상태 업데이트)

의존 관계 어떻게 표현함?

의존 관계는 의존되는 쪽이 화살표의 끝이다. 즉, 다음과 같다.

"Subject(뉴스 발행자)야 내(옵저버)가 필요한 데이터를 너가 업데이트 해주니까 구독좀 할게(registerObserver)"
"Subject: 띠링~ 새로운 신문이 발간됐어!(NotifyObservers)"
"Observer: 오! 고마워 해당 신문 좀 읽어봐야 겠네 ㅋ (update) 그리고 내가 게재하는 블로그에 게재해야겠어(display)"

해당 그림은 내가 구현한 소스 코드를 그림으로 표현한 것이다.

전략 패턴을 배웠을 때 디자인 패턴의 원칙 중 하나가 인터페이스에 맞춰서 프로그래밍 하라! 였다.

따라서 인터페이스를 구현한 클래스들과 해당 클래스에서도 WeatherData와 CurrentDisplayment를 보면 배열의 요소의 데이터 타입이 CurrentDisplayment와 WeatherData가 아닌 것을 볼 수 있다.

즉, 세부적인 구현 사항은 구현체에 부탁하는 것이다.

위 내용을 어떻게 코드로 구현할 수 있는지 알아보자.

코드로 구현하기

1. Subject.h

#ifndef SUBJECT_H
#define SUBJECT_H
#include "observer_interface.h"


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

#endif

먼저 Subject는 어떤 행동을 하는 지만 알면 된다. 따라서 순수 가상 함수로 각 행동들을 정의했다.

2. Observer & Displayment.h

#ifndef OBSERVER_H
#define OBSERVER_H

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

class Displayment {
public:
  virtual void display() = 0;
};

#endif

마찬가지로 Observer와 displayment가 하는 행동은 옵저버는 Subject를 관찰하다가 데이터가 넘어오면 받아서 가지고 있던 데이터를 갱신하고 Displayment는 이를 사용자에게 보여주는 역할(display)이기 때문에 위와 같이 구현했다.

3. Subject를 구현한 WeatherData class

현재 예시는 기상청에서 데이터를 업데이트해주면 WeatherData(Subject)가 데이터를 받아서 이를 필요로하는 옵저버에게 전달해주고 어떤 옵저버가 이 데이터를 필요로 하는지 알기 위해 옵저버를 등록(register)하거나 데이터가 필요 없는 옵저버를 위해 구독 해제(remove) 기능을 제공한다.

weather_data.h

#ifndef WEATHERDATA_H
#define WEATHERDATA_H

#include "observer_interface.h"
#include "subject.h"
#include <iostream>
#include <vector>
using namespace std;

class WeatherData : public Subject {
private:
  vector<Observer*> observers;
  int temperature;
  int humidity;
  int pressure;
public:
  WeatherData() {
    observers = vector<Observer*>();
  }
  //옵저버 배열의 끝에 Observer 객체를 추가한다. 
  void registerObserver(Observer* o) override { observers.push_back(o); };

  void removeObserver(Observer* o) override ;

  void notifyObservers() override ;

  void measurementsChanged();
  void setMeasurements(int temperature, int humidity, int pressure);
};

#endif

헤더 파일에서는 클래스가 어떻게 생겼는지 "선언(declare)"하는 역할만 하기에 구현 없이 선언만 했다.

1) vector< Observer* > observers

구독자를 관리하려면 어떤 옵저버가 내 구독 명단에 있는지 알아야 하기 때문에 이를 배열로 관리한다.

여기서 Observer의 데이터 타입을 포인터로 선언한 이유는 Observer는 객체를 찍어낼 수 없기 때문에 런타임에 형성되는 Observer type의 객체의 주소를 관리하는 것이다. 즉, upcasting을 활용해야 함. >> Observer를 구현한 클래스! 타입의 객체를 런타임 때 동적으로 참조해서 실제 구현을 사용할 예정.

2) 기상청에서 받는 온도, 습도, 기압

3) 생성자에서 빈 배열로 초기화

초기화하지 않으면 에러가 발생하는지는 실험해봐야 한다.

4) 옵저버 등록, 해제 함수

각 함수의 인자는 옵저버 포인터(옵저버 타입의 객체의 주소)를 받아서 배열에 추가하거나 삭제한다.

5) 기상 데이터 관련 처리 함수

weather_data.cpp

#include "observer_interface.h"
#include "weather_data.h"
#include <iostream>
#include <algorithm>
using namespace std;

void WeatherData::removeObserver(Observer* o)  {
  // 삭제할 Observer 객체의 인덱스를 찾아낸다.
  for (int i = 0; i < observers.size(); i++) {
    if(observers[i] == o) {
  // 해당 index에 있는 observer객체를 제외.
      observers.erase(observers.begin() + i);
      break;
      }
    }
  }

void WeatherData::notifyObservers() {
  // 배열을 순회하며 배열에 있는 옵저버에게
  for (int i = 0; i < observers.size(); i++ )
  // 옵저버의 update 메서드에 온도, 습도, 기압을 넘겨줘 값을 알린다.
    observers[i]->update(temperature, humidity, pressure);
  }

void WeatherData::measurementsChanged() {
  //기상청에서 기상 관련 데이터 측정에 업데이트가 생겼을 때 옵저버에게 알려준다.
  notifyObservers();
}

void WeatherData::setMeasurements(int temperature, int humidity, int pressure) {
    this->temperature = temperature;
    this->humidity = humidity;
    this->pressure = pressure;
    measurementsChanged();
}

4. Observer와 Displayment를 구현한 CurrentDisplayment 클래스

current_display.h

#ifndef CURRENTDISPLAY_H
#define CURRENTDISPLAY_H

#include "observer_interface.h"
#include "subject.h"

class CurrentConditionDisplay : public Observer, Displayment {
private:
  int temperature;
  int humidity;
  Subject* weatherData;
public:
  CurrentConditionDisplay(Subject* initialWeatherData);
  void update(int temperature, int humidity, int pressure) override ;
  void display() override ;
};
#endif

옵저버는 기상데이터를 구독해서 받고 온도와 습도만 표시하기 때문에 필요한 데이터만 프로퍼티로 선언했다.

Subject* weatherData;

Design 원칙 2! Composition 활용하기

특이한 점을 보면 포인터 변수인 weatherData를 선언한 건데, 다음 2가지를 위해 선언되었다.

"나 기상청 데이터 좀 구독하고 싶은데 좀 등록해줄래?(Subject의 registerObserver 메서드 필요)"
"나 기상청 데이터가 필요 없어졌어... 구독 취소 좀 할게(Subject 인터페이스의 removeObserver 메서드 필요)!"

즉, interface를 구현하는 게 아니라 subject를 참조하는 변수를 선언함(Composition)으로써 더 유연하게 활용할 수 있는 것이다.

current_display.cpp

#include "current_display.h"
#include <iostream>
using namespace std;

CurrentConditionDisplay::CurrentConditionDisplay(Subject* initialWeatherData) {
    weatherData = initialWeatherData;
    weatherData->registerObserver(this);
}

void CurrentConditionDisplay::update(int temperature, int humidity, int pressure) {
    this->temperature = temperature;
    this->humidity = humidity;
    display();
}

void CurrentConditionDisplay::display() {
    cout << "현재 상태: " << temperature << "F degree and 습도 " << humidity << "%" << endl;
}

6.사용자에게 표시하기!

#include <iostream>
#include "weather_data.h"
#include "current_display.h"

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

  CurrentConditionDisplay currenDisplay = CurrentConditionDisplay(&weatherData);

  weatherData.setMeasurements(80, 65, 30);
  weatherData.setMeasurements(82, 70, 29);
  weatherData.setMeasurements(78, 90, 29);

  return 0;
}

Stack에 WeatherData 객체를 생성하고 currentDisplay 객체의 생성자의 인자로 WeatherData 객체의 주소를 넘겨 currentDisplay 객체가 해당 객체를 관찰(observe)하게 한다.

그리고 기상청에서 기상 정보가 업데이트 된다면 setMeasurements 메서드를 활용해 온도, 습도, 기압 값을 인자로 넘기고 측정 값이 바꼈다는 알림을 구독자들에게 보낸다.(meaurementsChanged( ) > notifyObservers( ) )

이렇게 간단하게 옵저버 패턴을 구현해봤다.

정리

  1. 구독자가 유튜브 영상이 재밌어서 크리에이터(subject)를 구독한다.
  2. 크리에이터의 새로운 영상이 올라오면(setChanged) 자동으로 구독자들에게 알림을 보낸다. "새로운 영상이 올라왔으니까 봐주세요!"
  3. 구독자들은 해당 새로운 영상을 통해 새로운 정보를 얻고(update) 그 정보를 활용해 무엇인가를 한다.(display)
  4. 재미가 없다고 느낀 구독자들은 구독을 해제한다.(remove) 더 이상 해당 크리에이터의 정보를 받을 수 없다.

Observable Class를 상속한 옵저버 패턴 구현

자바에서는 Observable이라는 클래스를 제공하는데 이를 상속하면 자체적으로 구현하지 않아도 옵저버 등록, 제거, 변경 알림을 처리해준다. 코드를 통해 자세히 알아보자.

C++에는 Observable이라는 library가 존재하는지 찾아봤지만 당장 사용가능한 게 보이지 않아 해당 클래스를 자체 구현하게 됐다.

변경된 사항 중 핵심은 Subject가 데이터 변경 시 알림과 함께 옵저버에게 전달하는 것이 아니라 옵저버가 데이터를 끌어다(pull)쓰게 됐다는 점이다.

Observable(Subject) class

subject.h

#ifndef SUBJECT_H
#define SUBJECT_H

#include "observer_interface.h"
#include "weather_object.h"
#include <vector>
using namespace std;

class Subject {
private:
  vector<Observer*> observers;
  bool changed;
public:
  Subject();
  void registerObserver(Observer* o);
  void removeObserver(Observer* o);
  void notifyObservers();
  void setChanged() { changed = true; }
};

#endif

인터페이스에서 클래스로 선언이 바뀌면서 달라진 점은 추상 메서드가 전부 일반 메서드가 됐다는 점이다.

그리고 Observable에서 옵저버의 등록과 제거를 관리하기 때문에 옵저버를 관리하는 배열이 프로퍼티로 추가됐다.

마지막으론 setChanged메서드인데 변경이 됐을 때만 알림이 가도록 명시적인 장치를 걸어주기 위해 bool 변수인 changed가 추가됐다.

subject.cpp

#include "subject.h"

// Subject 초기화
Subject::Subject() {
    observers = vector<Observer*>();
}

// 구독자 등록
void Subject::registerObserver(Observer* o) { 
  observers.push_back(o); 
}

// 구독 취소
void Subject::removeObserver(Observer* o) {
  for (int i = 0; i < observers.size(); i++) {
    if(observers[i] == o) {
  // 해당 index에 있는 observer객체 구독을 취소.
      observers.erase(observers.begin() + i);
      break;
    }
  }
}

// 구독 알림
void Subject::notifyObservers() {
// Subject의 데이터에 변화가 있다면 
  if (changed) {
// 배열을 순회하며 배열에 있는 옵저버에게
  for (int i = 0; i < observers.size(); i++ ) {
    // 옵저버의 update 메서드에 온도, 습도, 기압을 넘겨줘 값을 알림.
    observers[i]->update();
    }
    // 상태 변화에 대한 알림이 갔으므로 변경 상태를 false로 변경.
    changed = false;
  }
}

가장 애를 먹은 지점은 알림을 보내는 것이었다. 데이터 전송 없이 알림만 가도록 하는 것이 생소했기 때문인 것 같다.

changed 변수에 들은 bool 값이 true일 때만 알림을 구독하고 있는 옵저버에게 알림이 가도록 구현했으며 알림이 가면 데이터의 변경 상태를 false로 바꿔줬다.

Observable을 상속한 class (WeatherData)

Weather_data.h

#ifndef WEATHERDATA_H
#define WEATHERDATA_H

#include "observer_interface.h"
#include "subject.h"
#include "weather_object.h"

class WeatherData : public Subject {
private:
  WeatherObject weatherObj;
public:
  WeatherData() {}

  void measurementsChanged() {
    //기상청에서 기상 관련 데이터 측정에 업데이트가 생겼을 때 옵저버에게 알려준다.
    setChanged();
    notifyObservers();
  };
  void setMeasurements(int temperature, int humidity, int pressure);

  // 옵저버에서 필요할 때 데이터를 끌어다 씀(Pull)
  int getTemperature() { return weatherObj.temperature; }
  int getHumidity() { return weatherObj.humidity; }
  int getPressure() { return weatherObj.pressure; }
};

#endif

변경된 점은 setChanged 메서드를 상속받아 기상청의 정보가 업데이트 되면 해당 메서드를 호출한다는 점이다.

Weather_data.cpp

#include "weather_data.h"
using namespace std;

void WeatherData::setMeasurements(int temperature, int humidity, int pressure) {
    this->weatherObj.temperature = temperature;
    this->weatherObj.humidity = humidity;
    this->weatherObj.pressure = pressure;
    measurementsChanged();
}

크게 달라진 점은 없고 weatherObj라는 구조체에서 온도, 습도, 기압을 관리한다는 점이다.

3. Observer 구현(CurrentDisplay)

current_display.h

#ifndef CURRENTDISPLAY_H
#define CURRENTDISPLAY_H

#include "observer_interface.h"
#include "subject.h"
#include "weather_data.h"

class CurrentConditionDisplay : public Observer, Displayment {
private:
  int temperature;
  int humidity;
  WeatherData* observable;
public:
  CurrentConditionDisplay(WeatherData* subject);
  void update() override ;
  void display() override ;
  double calculateHeatIndex(double temperature, double humidity);
};

#endif

이렇게 바꿔보니 문제가 있었다. 바로 upcasting을 이용하지 못한다는 점이었다. Observable을 상속해서 사용하다보니 Subject라는 인터페이스를 활용해서 코드를 작성하기 매우 까다로웠다.

그 이유는 다음 코드에서 살펴보자.

current_display.cpp

#include "current_display.h"
#include "weather_data.h"
#include <iostream>
using namespace std;

CurrentConditionDisplay::CurrentConditionDisplay(WeatherData* subject) {
    observable = subject;
    observable->registerObserver(this);
}

void CurrentConditionDisplay::update() {
  temperature = observable->getTemperature();
  humidity = observable->getHumidity();
  display();
}

void CurrentConditionDisplay::display() {
    cout << "현재 상태: " << temperature << "F degree and 습도 " << humidity << "%" << endl;
    cout << "체감 온도 (Heat Index): " << this->calculateHeatIndex(temperature, humidity) << "°C" << endl;
}

여기서 중요한 것은 update 메서드다. 옵저버가 Subject를 관찰하다가 알림이 발생하면 관리하던 데이터를 최신으로 업데이트하는데 여기서 핵심은 PULL이었다.

즉, "옵저버인 내가 데이터를 끌어다 쓸거야!" 인데 WeatherData(Subject)에 정의했던 Getter 메서드를 통해 데이터를 끌어오는 것이다.

하지만 그러기 위해서는 해당 클래스에 정의된 Subject의 getter method를 사용해야 한다.

그런데...

이를 상속받다 보니 해당 클래스의 생성자에 Subject* subject를 받아서 observable에 할당했더니 Subject(Observable)의 기능만 사용할 수 있고 Subject를 상속한 WeatherData의 getter를 사용할 수 없었던 것이다.

그래서 downCasting을 시도했는데 해당 Syntax를 잘 이해하지 못해서 해결하지 못했다. 따라서 인터페이스(Subject)를 활용하지 못하고 WeatherData를 사용해 문제를 해결할 수 밖에 없었다.

생성자에서 WeatherData가 observable에 할당되기 때문에 해당 주소에 접근하면 getter를 통해 온도와 습도를 끌어올 수 있었다.

결론

그래서 책(헤드 퍼스트 디자인)에서도 해당 디자인은 인터페이스를 활용할 수 없어서(구현에 의존해야 함) 좋지 않은 디자인이라고 했다.

옵저버 패턴 정의

옵저버 패턴은:

  • 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 알림이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

새로운 객체 지향 원칙: Loose Coupling

상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다. 인터페이스를 사용하게 되면 상호작용하는 객체 사이에 "최소한으로만" 구현 사항을 알 수 있다. 예를 들어, 옵저버는 Subject가 등록, 해제, 알림이라는 행동만 알고 내부 구현 사항은 모른다. Subject는 옵저버가 데이터를 업데이트하고 display한다는 것만 안다.

이렇게 느슨한 결합을 유지하면 유연하게 구조를 바꿀 수 있다는 장점이 있다!

profile
아쉬움 없이 살자. 모든 순간을 100%로!

0개의 댓글