의도
객체 사이에 일 대 다의 의존 관계를 정의해 두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 갱신될 수 있게 만듭니다.
다른 이름
종속자(Dependent), 게시-구독(Publish-subscribe)
활용성
다음 상황 중 어느 한가지에 속하면 감시자 패턴을 사용합니다.
- 어떤 추상 개념이 두 가지 양상을 갖고 하나가 다른 하나에 종속적일 때. 각 양상을 별도의 객체로 캡슐화하여 이들 각각을 재사용할 수 있습니다.
- 한 객체에 가해진 변경으로 다른 객체를 변경해야 하고, 프로그래머들은 얼마나 많은 객체들이 변경되어야 하는지 몰라도 될 때
- 어떤 객체가 다른 객체에 자신의 변화를 통보할 수 있는데, 그 변화에 관심있어 하는 객체들이 누구인지에 대한 가정 없이도 그러한 통보가 될 때.
구조
결과
감시자 패턴을 사용하게 되면 주체 및 감시자 모두를 독립적으로 변형하기 쉽습니다. 감시자를 재사용하지 않고도 주체를 재사용할 수 있고, 주체 없이도 감시자를 재사용할 수 있습니다. 또한 주체나 감시자의 수정 없이도 감시자를 추가할 수 있습니다.
감시자 패턴을 사용함으로써 얻을 수 있는 그 밖의 이익은 다음과 같습니다.
Subject와 Observer 클래스 간에는 추상적인 결합도만이 존재합니다.
주체가 아는 것은 감시자들의 리스트일 뿐입니다. 이 감시자들은 Observer 클래스에 정의된 인터페이스(ConcreteObserver가 아닌!)를 따릅니다. Subject와 Observer 클래스가 강결합되지 않기 때문에, 이들은 시스템의 여러 추상화 계층에 속할 수 있습니다.
브로드캐스트(broadcast) 방식의 교류를 가능하게 합니다.
일반적인 요청과 달리, 감시자 패턴에서 주체가 보내는 통보는 구체적인 수신자를 지정할 필요가 없습니다.
예측하지 못한 정보를 갱신합니다.
감시자는 다른 감시자의 존재를 모르기 때문에 주체를 변경하는 비용이 궁극적으로 어느 정도인지 모릅니다.
구현
주체와 감시자 사이의 종속 메커니즘을 구현하기 위해서는 몇 가지 생각해야 할 것이 있습니다.
주체와 그것의 감시자를 대응시킵니다.
자신이 통보해 주어야 하는 감시자들을 주체가 지속적으로 관리하는 가장 쉬운 방법은 주체에 감시자에 대한 참조자를 저장하는 것입니다. 만약 주체가 많고 감시자가 적다면 모든 주체들마다 감시자에 대한 참조자를 다 갖고 있을 테니 저장 공간의 낭비가 심할 것입니다. 공간을 절약할 수 있는 한 가지 방법은 별도의 탐색용 자료 구조를 두고 둘 간의 대응 관계를 관리하게 하는 방법입니다.
하나 이상의 주체를 감시합니다.
누가 갱신을 촉발시킬 것인가?
값을 갱신 하려면 어느 객체가 Notify() 메서드를 호출해야 할까요? 여기에는 두 가지 선택 사항이 있습니다. (a) Subject 클래스의 상태 변경 후 상태를 지정하는 연산에서 Notify()를 호출합니다. 사용자가 Notify() 메서드를 호출할 필요가 없다는 장점이 있고, 계속되는 연산의 수행으로 여러 번 수정해야 하기 때문에 비효율적이라는 단점이 있습니다. (b) 사용자가 적시에 Notify()를 호출하는 책임을 지도록 해야 합니다. 중간 중간에 불필요한 수정이 일어나지 않는 대신, 사용자에게 수정하게 하는 추가적 행동을 정의해야 합니다. 이렇게 되면 사용자가 Notify()를 호출하는 것을 잊어버릴 때가 많기 때문에 오류가 많이 생길 수 있습니다.
삭제한 주체애 대한 무효 참조자를 계속 유지할 때가 있습니다.
주체의 삭제로 감시자가 무효 참조자를 갖게 되도록 만들면 안 됩니다.
통보 전에 주체의 상태가 자체 일관성을 갖추도록 만들어야 합니다.
감시자별 갱신 프로토콜을 피합니다. (푸쉬 모델 & 풀 모델).
감시자 패턴을 구현하려고 변경이 발생할 때 주체가 추가적인 정보를 브로드캐스트하게 만들 때가 꽤 많습니다. 주체는 Update() 연산의 인자로 이 추가적인 정보를 전달해야 합니다.
극단적으로, 주체가 자신의 변경에 대한 상세한 정보를 감시자에게 전달하는 푸시 모델(push model)을 이용할 수 있습니다. 도 다른 극단적인 예로 주체가 최소한의 정보만을 전달하고 감시자가 다시 상세 정보를 요청해 오는 풀 모델(pull model)을 사용할 수도 있습니다.
풀 모델이 주체가 감시자를 몰라도 된다는 것에 중점을 두었다면, 푸시 모델에서 주체는 감시자의 요청이 무엇인지 알아야 합니다. 푸시 모델을 사용하면 감시자 클래스의 재사용성이 떨어지게 됩니다. 그러나 풀 모델은 Observer 클래스가 Subject와 상관없이 무엇이 변했는지를 확인해야 한다는 점에서 비효율적일 수 있습니다.
자신이 관심 있는 변경이 무엇인지 명확하게 지정합니다.
Subject 클래스에 자신이 관심 있는 이벤트에 대한 감시자를 등록하는 인터페이스를 정의함으로써 갱신의 과정을 좀 더 효율화할 수 있습니다.
void Subject::Attach(Observer*, Aspect& interest);
void Subject::Update(Observer*, Aspect& interest);
복잡한 갱신의 의미 구조를 캡슐화합니다.
주체와 감시자 간에 일어나는 관련성이 복잡한 것이라면, 이들 관련성을 관리하는 별도의 객체를 만들도록 합니다.
Subject와 Observer 클래스를 합칩니다.
의견
Subject를 상속받는 ClockTime 클래스, Widget 클래스와 Observer 클래스를 동시에 상속받는 DigitalClock, AnalogClock 클래스를 생각하면 이해가 쉽다.
참고
에릭 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스. GoF의 디자인 패턴. 프로텍미디어 2015
GoF의 디자인 패턴 (Summary) 시리즈입니다.
- GoF의 디자인 패턴 (Summary) - 1
- GoF의 디자인 패턴 (Summary) - 2. 추상 팩토리(Abstract Factory)
- GoF의 디자인 패턴 (Summary) - 3. 팩토리 메서드(Factory Method)
- GoF의 디자인 패턴 (Summary) - 4. 단일체(Singleton)
- GoF의 디자인 패턴 (Summary) - 5. 적응자(Adapter)
- GoF의 디자인 패턴 (Summary) - 6. 복합체(Composite)
- GoF의 디자인 패턴 (Summary) - 7. 장식자(Decorator)
- GoF의 디자인 패턴 (Summary) - 8. 퍼사드(Facade)
- GoF의 디자인 패턴 (Summary) - 9. 감시자(Observer)
- GoF의 디자인 패턴 (Summary) - 10. 전략(Strategy)
- GoF의 디자인 패턴 (Summary) - 11 템플릿 메서드(Template Method)