디자인 패턴/GoF

[디자인 패턴] 행동 패턴 (5) 감시자 Observer

로파이 2021. 2. 4. 14:34

감시자 (Observer) 패턴

 

1. 의도

객체 사이에 一 대 多의 의존 관계를 정의하여 어떤 객체의 상태가 변할 때 그 객체를 관찰하는 감시자에게 변화를 통보하고 변화된 상태를 알 수 있게 한다.

 

2. 활용

 

감시자 패턴은 서로 의존적인 두 클래스가 일관성을 갖도록 하는 것이다. 일관성을 가지면서 두 클래스의 결합도를 높이지 않아 각 클래스의 재사용성을 높이는데 목적이 있다.

 

- Subject-Observer 예시

의존적인 관계를 가지는 두 클래스르 Subject-Observer라고 할 수 있고 이를 설명하기 위해 데이터와 그래프 자료를 예로 들 수 있다.

Subject-Observer

  • 데이터 자료형을 스프레드 시트나 그래프로 나타내는 GUI 객체들이 있는데 이들은 모두 실제 데이터에 의존적인 모습을 보인다.
  • 데이터가 변경되면 그에 따른 스프레드 시트의 엔트리에서 데이터를 업데이트 해야하고 바 차트와 원형 다이어그램에도 반영을 해야한다. 
  • Subject는 데이터 자체라고 할 수 있고 Observer는 Subject의 상태를 계속 관찰해야하는 GUI 객체들이다.
  • 두 상호작용을 통해 두 클래스의 일관성을 유지하는 데 Notify Subject가 자신의 상태가 변경되었을 때 관련있는 GUI 객체들에게 알리고 Notify를 받은 GUI 객체들은 변화된 상태를 알기위해 Request를 한다.

다음 상황에서 감시자 패턴을 사용할 수 있다.

  • 두 추상 개념이 다른 클래스 계통을 가지고 한 클래스가 다른 하나에 종속적일 때
  • 한 객체의 변경으로 다른 객체를 변경해야하고 이 변경 과정이 자동으로 일어나게 하고 싶을 때
  • 상태 변화 종류에 상관없이 통보가 될 때 (누군가는 필요하다고 가정하고 통보가 되어야한다.)

3. 참여자

 

행동 패턴 - 감시자

  • Subject: 감시자들을 알고 있는 주체, 임의 개수의 감시자 객체는 주체를 감시할 수 있다.
  • Observer: 주체에 생긴 변화에 관심 있고 통보를 통해 자신의 상태를 갱신하는데 필요한 인터페이스를 정의한다.
  • ConcreteSubject: ConcreteObserver 객체에게 알려주어야 하는 상태를 저장한다.
  • ConcreteObserver: ConcreteSubject 객체에 대한 참조자를 관리한다. 주체의 상태와 일관성을 유지해야 하는 상태를 저장한다.

4. 협력 방법

  • ConcreteSubject는 자신의 상태가 변경될 때마다 ConcreteObserver에게 통보를 한다.
  • ConcreteObserver는 통보를 받고 ConcreteSubject에게 상태를 질의하여 자신의 상태와 일치시킨다.

하나의 주체와 두 감시자의 협력 관계 예시

협력 관계 예시

Observer가 SetState()를 통해 Subject의 상태 변경을 유발시키고 Subject는 자신의 상태 변경을 두 Observer에게 알린다. Update()를 통해 알림을 받은 옵저버는 GetState()를 통해 Subject의 상태를 자신의 상태와 일치시킨다.

위의 예시에서는 Observer 역시 Subject의 상태 변경에 관여할 수 있는 것을 가정한다.

 

5. 결과

 

  1) Subject와 Observer 클래스 간에는 추상적인 결합만 존재

  • 주체는 감시자 클래스의 리스트를 보관할 뿐 어떤 ConcreteObserver를 가지고 있는지 알 필요가 없다. 주체 클래스의 메서드, Notify()는 감시자 인터페이스, Update()를 사용하기만 하면 된다.

  2) 브로드캐스트 방식의 교류를 가능하게 한다.

  • 어떤 감시자가 통보를 받을 지 고려하지 않고 자신과 관련만 있다면 통보를 하게 된다. ConcreteObserver가 통보를 받고 변경 정보를 받을지 선택할 수 있다.

  3) 예측하지 못한 정보를 갱신한다.

  • 감시자가 주체의 상태 변경을 촉발할 때 얼마나 큰 변경 비용이 발생하는지 모른다. 주체와 종속적인 다른 감시자들 역시 변경이 연쇄적으로 일어나고 상태 변경을 촉발한 감시자는 이 비용을 모르기 때문에 불필요한 갱신이 이루어질 수 있다.

6. 구현

 

  1) 주체와 감시자를 대응시킨다.

  • 주체가 감시자에 대한 참조자를 저장하는 것으로 통보 대상을 관리한다.
  • 만약 주체가 많고 감시자가 적다면 모든 주체가 참조자를 가지므로 저장 공간 낭비가 심할 것이다. 공간을 절약할 수 있는 한 가지 방법은 별도의 탐색용 자료구조를 가지게 하는 것이다.
  • 탐색용 자료구조(ex. 해시 테이블)는 주체-감시자 쌍의 대응 관계를 관리하고 이 자료구조를 통해 주체는 필요한 감시자의 참조자를 얻는다.

 

  2) 하나 이상의 주체를 감시한다.

  • 한 감시자에 여러 주체가 대응될 때, 감시자는 어떤 주체에서 통보가 전달되는 지 알아야한다.
  • 이는 Update(Subject* s)등으로 어떤 주체로 부터 통보가 되었는지 매개변수로 받도록한다.

 

  3) 누가 갱신을 촉발시킬 것인가

  • 두 클래스가 일관성을 유지하기 위해 Notify() 통보 메커니즘에 의존할 수 밖에 없다. 그렇다면 어느 객체가 Notify()를 유발해야 할 까

    a) Subject 클래스의 상태 변경과 관련된 연산 내부에서 Notify()를 호출하도록 한다.

  • 사용자가 Notify()를 직접 호출할 필요는 없지만 상태 변경이 많을 경우 Notify()가 지속적으로 일어나는 비효율성이 발생한다.

    b) 사용자가 적시에 Notify()를 호출하도록 한다.

  • 일련의 상태 변경이 완료된 후 한꺼번에 통보하는 방식으로 효율적이지만 사용자가 어느 시기에 Notify()를 호출해야할 것인지 잘 고려해야한다.

 

  4) 삭제한 주체에 대한 dangling 참조자를 계속 유지할 때가 있다.

  • 이미 삭제되어 실체가 없는 무효 참조자를 피하기 위해 주체가 감시자에게 삭제된 내용을 알려주어 주체에 대한 참조자를 삭제하라고 알려주는 방법을 사용할 수 있다.

 

  5) 통보 전에 주체의 상태가 자체 일관성을 갖추도록 만들어야 한다.

  • 통보 전에 변경 연산이 완전히 완료된 후 통보가 되도록 보장해야한다. 

 

  6) 감시자별 갱신 프로토콜을 피한다.

  • 푸시 모델: 변경에 대한 모든 정보를 감시자에게 전달한다. 감시자에게 전달해야하는 정보를 정의해야하는 데 이는 통보의 유연함이 낮아지고 클래스 재사용성이 떨어진다.
  • 풀 모델: 변경에 대한 최소 정보를 전달하고 감시자가 다시 상세 정보를 요청하도록 한다.

 

  7) 자신의 어떤 상태에 관심 있는지를 명시한다.

  • 특정 상태에만 관심이 있는 감시자가 있을 수 있는데 이를 주체에 관심-감시자 쌍을 등록하고 Update()시 해당 감시자에 맞는 관심을 정보로 전달한다.

 

  8) 복잡한 갱신의 의미 구조를 캡슐화한다.

  • 주체와 감시자 간의 관계를 정리하여 관리하는 별도의 객체를 둔다.
  • 이러한 객체를 ChangeManager라고 한다면, 이 객체의 목적은 감시자의 Update()가 최소한으로, 각 감시자마다 한 번만 되도록한다. 여러 주체의 상태가 변경되고 이 주체의 감시자에게 딱 한번만 통보되도록 한다.

 

ChangeManager는 다음 역할을 가져야한다.

  (a) 주체와 감시자를 매핑하고 이를 유지하는 인터페이스를 정의해야한다.

  (b) 특별한 갱신 전략을 정의해야한다.

  (c) 주체에게 요청이 있을 때 모든 독립적 감시자들을 수정해야한다.

 

ChangeManager

위 클래스 다이어그램에서 보듯이 ChangeManager의 두 서브 클래스를 정의할 수 있다면,

  • Register()와 Unregiser()를 통해 주체-감시자 매핑을 유지하고
  • SimpleChangeManager는 가장 기본적인 통보 방식으로 Notify()에서 단순히 모든 주체에 대해 각 주체에 관련된 모든 감시자가 통보 받도록한다.
  • DAGChangeManager는 주체-감시자 관계를 방향 그래프로 보고 한 주체와 관련있는 감시자만 통보 되도록 한다.

ChangeManager는 중재자 패턴으로 볼 수 있고 단일체로 구현할 수 있다.

 

  9) Subject와 Observer 클래스를 합친다.

  • 두 클래스를 분리할 수 없는 경우 사용한다.

 

예제 코드

 

시시각각 변하는 시간을 기록하는 ClockTimer를 주체로 두고 이 시간 변경을 일치시켜 시간을 표시하는 디지털 시계와 아날로그 시계를 감시자로 둘 수 있다.

 

Observer.h

매개변수로 Subject 인스턴스를 받아 다수의 Subject가 하나의 감시자를 가질 수 있다. 예시에서는 하나의 주체만 관리한다.

class Observer
{
public:
	virtual ~Observer();
	// 전달받은 theChangedSubject가 관찰 대상이라면 자신의 상태를 업데이트 합니다.
	virtual void Update(class Subject* thsChangedSubject) = 0;
protected:
	Observer();
};

 

Subject.h

자신과 관련된 감시자들을 List로 관리하고 이에 추가하고 제거하는 인터페이스를 제공한다. Subject는 Notify()에서 감시자들이 자신의 상태와 일치시도록 Update()를 호출한다.

#include "Observer.h"
#include "List.h"
#include "ListIterator.h"
class Subject
{
public:
	virtual ~Subject();
	virtual void Attach(Observer*);
	virtual void Detach(Observer*);
	virtual void Notify();
protected:
	Subject();
private:
	// 자신을 관찰하는 모든 객체를 리스트로 관리
	List<Observer*>* _observers;
};
Subject::~Subject()
{
}
// 관찰자 추가
void Subject::Attach(Observer* o)
{
	_observers->Append(o);
}
// 관찰자 삭제
void Subject::Detach(Observer* o)
{
	_observers->Remove(o);
}

// 모든 관찰자에게 자신의 변경을 알림
void Subject::Notify()
{
	ListIterator<Observer*> i(_observers);

	for (i.First(); !i.IsDone(); i.Next())
	{
		i.CurrentItem()->Update(this);
	}
}

Subject::Subject()
	:
	_observers(new List<Observer*>())
{
}

ClockTimer.h

Subject의 구체 클래스로 관찰 대상으로써의 역할을 수행한다. 자신의 상태에 접근할 수 있는 메서드를 제공한다.

시간이 흐른 뒤 Tick()에서 이를 자신의 변화를 감시자들에게 알리게된다. 

class ClockTimer : public Subject
{
public:
	ClockTimer();

	virtual int GetHour();
	virtual int GetMinute();
	virtual int GetSecond();

	void Tick();
};

void ClockTimer::Tick()
{
	// 자신을 관찰하는 모든 관찰자에게 변화를 알려야 한다.
	// 시간 상태를 변경
	Notify();
}

 

DigitalClock.h / AnalogClock.h

ClockTimer를 관찰하는 구체 클래스이고 시간이 달라짐에 따라 자신의 시각을 표시하기 위해 Update() 함수를 호출한다.

생성자에서 연관된 주체를 참조자로 관리하고 이 주체(ClockTimer)로 부터 통보가 되었을 때 다시 화면에 시계를 표시하도록 한다. 이 클래스들은 기본적으로 Widget 추상 클래스를 상속하여 시간을 나타내는 GUI 객체를 의미한다.

Widget()의 Draw() 인터페이스는 화면에 시계를 표시하는 연산이다.

#include "Widget.h"
#include "Observer.h"
#include "ClockTimer.h"

// 다중 상속을 통한 다중 기능 구현
class DigitalClock : public Widget, public Observer
{
public:
	DigitalClock(ClockTimer*);
	virtual ~DigitalClock();

	// 감시자 연산 오버라이드
	virtual void Update(Subject*);
	// 위젯 연산 오버라이드
	virtual void Draw();
private:
	ClockTimer* _subject;
};
#include "DigitalClock.h"

DigitalClock::DigitalClock(ClockTimer* s)
{
	_subject = s;

	// add observer to my subject 
	_subject->Attach(this);
}

DigitalClock::~DigitalClock()
{
	// delete observer from my subject
	_subject->Detach(this);
}

void DigitalClock::Update(Subject* theChangedSubject)
{
	// check the notficiation is from my subject
	if (theChangedSubject == _subject)
	{
		Draw();
	}
}

void DigitalClock::Draw()
{
	// Obtain new values from my subject
	int hour = _subject->GetHour();
	int minute = _subject->GetMinute();
	int second = _subject->GetSecond();

	// Draw this widget
}

AnalogClock.h 

#include "Widget.h"
#include "Observer.h"
#include "ClockTimer.h"

class AnalogClock : public Widget, public Observer
{
public:
	AnalogClock(ClockTimer*);
	virtual void Update(Subject*);
	virtual void Draw();
	// ...
private:
	ClockTimer* _subject;
};

 

사용자 프로그램 코드

#include "AnalogClock.h"
#include "DigitalClock.h"
#include "ClockTimer.h"

int main()
{
	ClockTimer* timer = new ClockTimer;
	AnalogClock* analogClock = new AnalogClock(timer);
	DigitalClock* digitalClock = new DigitalClock(timer);

	return 0;
}