감시자 (Observer) 패턴
1. 의도
객체 사이에 一 대 多의 의존 관계를 정의하여 어떤 객체의 상태가 변할 때 그 객체를 관찰하는 감시자에게 변화를 통보하고 변화된 상태를 알 수 있게 한다.
2. 활용
감시자 패턴은 서로 의존적인 두 클래스가 일관성을 갖도록 하는 것이다. 일관성을 가지면서 두 클래스의 결합도를 높이지 않아 각 클래스의 재사용성을 높이는데 목적이 있다.
- 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의 두 서브 클래스를 정의할 수 있다면,
- 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;
}
'디자인 패턴 > GoF' 카테고리의 다른 글
[디자인 패턴] 행동 패턴 (7) 중재자 Mediator (0) | 2021.03.23 |
---|---|
[디자인 패턴] 행동 패턴 (6) 전략 Strategy (0) | 2021.03.13 |
[디자인 패턴] 행동 패턴 (4) 상태 State (0) | 2021.02.03 |
[디자인 패턴] 행동 패턴 (3) 해석자 Interpreter (0) | 2021.02.02 |
[디자인 패턴] 행동 패턴 (2) 명령 Command (0) | 2021.02.02 |