티스토리 뷰


오늘은 두 번째 디자인 패턴인 옵저버패턴에 대해 포스팅 하려고 합니다.


1. 옵저버 패턴이란,

옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.

 - 출처 : 위키백과


간단하게 이야기 하면, 어떤 객체의 상태가 변하게 되면, 변한 객체가 그 밑의 관찰자들에게 변했다는 것을 알리는 디자인 패턴의 일종입니다.


헤드퍼스트 책의 예제를 활용하여 (동일하진 않고, 간결한 설명하기 위해 케이스를 줄였습니다) 옵저버패턴을 설명해 보겠습니다.


기상정보를 알리는 응용 프로그램을 만들게 되었습니다. 이 모든 기상정보는 WeatherData라는 객체가 받고 있으며, 받게 되면 실시간으로 갱신을 하고 있습니다. 이 때 실시간으로 갱신되고 있는 이 정보를 가져와 띄워줄 수 있는 어플리케이션을 만들어하는 상황입니다. 디스플레이에 출력해야 할 때 사용하는 정보는 총 세 가지, 온도, 습도, 압력입니다.


우리는 온도, 습도, 압력을 Display 해 주는 창을 구현해야 합니다. 추후 업데이트 된 자료를 해석할 창을 새로 만들어야 한다고 합니다.


물론 일일이 이 세 객체를 WeatherData의 멤버변수로 둬서 업데이트 시 돌아가는 함수에 각 멤버변수마다 값을 업데이트 하게 만들 수는 있지만, 이 세 가지의 정보를 이용해서 새로운 정보를 띄우고 싶을 때마다 코드 변경이 많이 필요해서 여러모로 유지보수가 귀찮아지는 상황이 발생하게 될 것입니다. 


하지만 옵저버 패턴을 사용하게 되면, WeatherData의 옵저버에서 제외하기만 하면 됩니다. 


주체 - 옵저버와의 관계는 유투버와 구독자를 생각하면 제일 편합니다.

 


 1. 유투버의 영상을 일반 유저가 구독하기 버튼을 누름.

 


 2. 구독자는 유투버가 새로운 영상을 업로드 하게 되면 새 영상에 대한 알람을 받음

 3. 구독자가 유투버의 계정의 구독을 끊음.

 4. 새 영상에 대한 알람을 받을 수 없음

 유투버 (Subject)


 구독자 (Observer)


이해가 되셨으면, 유투버와 구독자를 생각하며 위에 주어진 예제를 코드로 구현해보도록 하겠습니다.


2. 구현

 1) 인터페이스

  - 크게 두 가지로 나뉘면 주제 인터페이스, 옵저버 인터페이스가 들어 있는 클래스를 바탕으로 시작합니다.



 - Subject Class

  (1) RegisterObserver :  Observer로서 객체를 등록할 때 쓰이는 함수

  (2) RemoveObserver : Observer 객체의 관찰을 해제 할 때 쓰이는 함수

  (3) NotifyObserver : Observer에게 갱신된 값을 알리는 함수

 - Observer Class

  (1) update : 값이 업데이트 될 때 사용되는 함수



2) Subject 클래스



Subject에게 필요한 것은 위에서 말했다시피 등록, 삭제, 알림 오로지 세 가지 기능입니다. 이 기능을 인터페이스로 선언한 채 이 주제를 상속을 WeatherData에 상속하였습니다.

Subject 클래스를 상속받는 WeatherData  클래스입니다. WeatherData는 WeatherData값을 가지고 있는 기계에서 값을 알아내기 위한 get 함수들이 있습니다. get을 통해 받아온 값이 갱신이되면 NotifyObserver() 함수를 통해 등록된 객체들에게 값이 변경되었다고 알리게 됩니다.

이를 C++ 코드로 구현해 보겠습니다.

<CSubject.cpp>

1
2
3
4
5
6
7
8
9
10
class CSubject
{
public:
    CSubject();
    virtual ~CSubject();
 
    virtual void RegisterObserver(CObserver *observer) = 0;
    virtual void RemoveObserver(CObserver *observer) = 0;
    virtual void NotifyObserver() = 0;
};
cs

선언만 해 놓는 추상클래스를 통해 Subject라면 반드시 필요한 세 함수를 선언하였습니다. 구현은 이 주제를 상속받는 곳에서 하게 됩니다.

 

<CWeatherData.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CWeatherData : public CSubject
{
public:
    CWeatherData();
    ~CWeatherData();
    
    void SetMeasureChanged(float humidity, int pressure, float temperature);
    void RegisterObserver(CObserver *observer);
    void MeasurementChanged();
    void RemoveObserver(CObserver *observer);
 
private:
    float mHumidity;
    int mPressure;
    float mTemperature;
    std::vector<CObserver *> mObserver;
    void NotifyObserver();
};
 
cs

WeatherData는 Subject 객체이므로 위의 인터페이스에 선언된 세 함수 (RegisterObserver, RemoveObserver, NotifyObserver) 에 대해 정의하였습니다.

그리고 구독하고 있는 옵저버들에게 알리기 위해 포인터들을 저장하는 벡터(mObserver)도 멤버 변수도 선언하였습니다.

그리고 WeatherData가 가지고 있어야 하는 세 데이터(mHumidity, mPressure, mTemperature)도 정의되어 있습니다.


그럼 제가 만든 함수들을 하나하나 확인해 보도록 하겠습니다.


 - Subject 구현부

<CWeatherData.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CWeatherData::RegisterObserver(CObserver * observer)
{
    mObserver.push_back(observer);
}
 
void CWeatherData::NotifyObserver()
{
    std::vector<CObserver *>::iterator iter = mObserver.begin();
    for (; iter != mObserver.end(); iter++)
    {
        CObserver *observer = *iter;
        observer->update(mTemperature, mHumidity, mPressure);
    }
}
 
void CWeatherData::RemoveObserver(CObserver *observer)
{
    std::vector<CObserver *>::iterator iter;
    iter = find(mObserver.begin(), mObserver.end(), observer);
    if (iter != mObserver.end())
    {
        mObserver.erase(iter);
    }
}
cs


WeatherData의 멤버변수로 선언된 벡터에 옵저버들을 등록합니다(RegisterObserver). 그리고 등록을 해제하고 싶다면 해당하는 옵저버 객체를 삭제하면 됩니다(RemoveObserver). 그리고 Observer에게 변경사항을 알리는 함수(NotifyObserver)를 통해, 등록된 Observer 리스트를 돌면서 Observer에게 받은 값을 업데이트 하도록 구현하였습니다.


- WeatherData의 구현부

1
2
3
4
5
6
7
8
9
10
11
12
13
void CWeatherData::SetMeasureChanged(float humidity, int pressure, float temperature)
{
    mHumidity = humidity;
    mPressure = pressure;
    mTemperature = temperature;
    MeasurementChanged();
}
 
void CWeatherData::MeasurementChanged()
{
    NotifyObserver();
}
 
cs

값을 가지고 갈 때는 get을 통해 들고가게 만들었습니다.

그리고 업데이트를 받았을 때 알리는 것이 바로 옵저버 패턴의 핵심이기 때문에, 값이 바뀌게 되면(SetMeasureChanged), 값이 바뀌었다고 알리는 함수(MeasurementChanged)를 부르고, 이 함수가 옵저버들에게 변화를 알리게(NotifyObserver) 됩니다.


3) Observer 클래스

- Observer는 Subject로부터 Update가 실행됩니다.

- Observer를 상속받은 CurrentCondition을 보게 되면 Display에 필요한 데이터인 온도, 습도, 압력을 가지고 있고, Observer의 Subject인 weatherData를 멤버 변수로 가지고 있습니다.


<CObserver.h>

1
2
3
4
5
6
7
class CObserver
{
public:
    CObserver();
    ~CObserver();
    virtual void update(float temperature, float humidity, int pressure) = 0;
};
cs


Observer 코드도 간단합니다. Update는 구현부에서 구현할 예정이므로 추상클래스에서 구현해 놓았습니다.


왜 멤버변수를 가지고 있는가, 궁금할텐데, 그건 CObserver를 상속받은 CurrentCondition을 확인하면 알 수 있습니다. 


<CCurrentCondition.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CCurrentCondition::CCurrentCondition(CSubject * weatherData)
{
    mWeatherData = weatherData;
    mWeatherData->RegisterObserver(this);
}
 
CCurrentCondition::~CCurrentCondition()
{
}
 
void CCurrentCondition::update(float temperature, float humidity, int pressure)
{
    mTemperature = temperature;
    mHumidity = humidity;
    mPressure = pressure;
    display();
}
 
void CCurrentCondition::display()
{
    printf("현재 압력 : %d\n", mPressure);
    printf("현재 온도 : %f\n", mTemperature);
    printf("현재 습도 : %f\n", mHumidity);
}
 
cs

옵저버들의 WeatherData는 오직 하나입니다. 이 하나의 WeatherData를 멤버 변수로 둬서, 생성자에서 자기 자신을 WeatherData에 등록해 주면 준비가 끝이 납니다. 생성하면서 이 옵저버는 구독을 누른 것과 같은 행위를 하였습니다.


Update 함수도 구현이 되어 있습니다.

주체에서 Update 함수를 적용했을 때 주체의 temperature, humidity, pressure의 값을 옵저버에게 매개변수로 보내 주었었습니다.

옵저버는 이 값을 받아 본인의 멤버변수에 업데이트를 하고, 자료가 업데이트 됐으니 Display를 합니다. 물론 간지나게 툴로 보이게 할 순 있지만, 굳이... 필요하진 않으니 우리의 친구 cmd 창에 Display하는 것으로 만족하겠습니다.


3. 출력

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
    CWeatherData *weather = new CWeatherData();
    CCurrentCondition *condition = new CCurrentCondition(weather);
    CCurrentCondition *condition2 = new CCurrentCondition(weather);
 
    weather->SetMeasureChanged(7.0f, 215.0f);
 
    printf("Observer 삭제\n");
 
    weather->RemoveObserver(condition2);
 
    weather->SetMeasureChanged(5.0f, 113.0f);
 
    return 0;
}
cs


하나 밖에 없는 주체를 선언하고, 두 화면에 디스플레이 하기 위해 옵저버 객체를 두 개 선언하였습니다. 물론 다른 클래스를 정의해서 weatherData의 옵저버 리스트 안에 넣는 것도 가능합니다.


weatherData의 값이 변경되게 되면, Notify->Update->display의 과정을 거쳐 모든 옵저버들이 cmd 창에 온도가 바뀌었다고 출력하게 됩니다.


그리고, 한 옵저버의 등록을 해제하고, 다시 온도를 바꾸게 되면, 하나의 객체만 display를 하게 될 것입니다.



옵저버 객체 삭제 전에는 두 옵저버가 업데이트 내용을 display했고, 한 옵저버를 등록 해제 한 뒤엔 한 옵저버만 변경값을 출력하고 있습니다.

 

3. 결론

처음 구현했을 때 참 신기하다고 생각했던 패턴이 바로 이 옵저버 패턴입니다. 글을 마치기 전에 간단히 정리를 하겠습니다.

옵저버 패턴은 일대 다로 변경시 소식을 받을 수 있는 패턴입니다.

 

이 옵저버패턴의 장점은,

1. 옵저버를 언제든지 새로 추가할 수 있다.

 - 새로운 구현이 필요할 때도 Observer만 상속받아 구현하게 되면 해당하는 주체에 대해 모두 적용 가능합니다.

2. 새 옵저버를 적용할 때 주제를 변경할 필요가 없다.

 - 주제 변경 없이 새 옵저버를 만들기만 하면 모두 적용되는 디자인 패턴입니다.

3. 주제, 옵저버 간의 독립적인 재사용이 가능하다.

4. 옵저버, 주제가 바뀌더라도 서로에게 영향을 미치지 않는다.

 

각 디자인패턴이 여러 매력이 있겠지만, 옵저버 패턴도 장점이 다양한 패턴이라고 생각합니다. 다음 포스팅은 데코레이터 패턴으로 찾아뵙겠습니다. 부족한 글 읽어주셔서 감사합니다. 부족한 사람이라, 언제나 지적해 주시면 감사하겠습니다.

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함