입문에 해당하는 생성 패턴 몇개와 디자인 패턴이 무었이라는 것만 추상적으로 알고 있다가, 큰 맘 먹고 쭌과 함께 'GoF의 디자인 패턴' 책을 가지고 디자인 패턴을 쭉 공부하였다. 전부 마스터 하지는 못하고 마지막 행위 패턴들 중 일부를 남겨둔채 중단하였지만, 정말 유익한 경험이었다.
이번에 실제 프로젝트에서 해결해야할 문제가 발생하였고, 공부했던 행위 패턴중에 이 문제를 해결할 적당한 패턴이 있었음을 기억하였다. 그리고 그 패턴을 실제 적용하였다. 결과는 대 만족이었다.
문제는 다음과 같았다.
* 여러 객체들이 서로의 상태에 따라 반응을 해야 한다. 그러나, 각 객체의 동작이 영향을 주는 객체가 어떤건 하나, 어떤건 다수 등등 각각 제각각이며, 적용되어야 하는 결과도 다들 틀리다.
책은 "GoF의 디자인패턴", "Java 언어로 배우는 디자인 패턴 입문", "C# 객체지향 언어로 배우는 디자인패턴" 등등을 추천합니다.
실전은 이랬다.
(1) 상호 작용하는 객체는 ToolBar, Tabbed Window에 들어가는 3종류의 View Class (그중 하나는 Instance가 2개 이상존재하며, 똑같이 동기화 되어야 한다. Single톤 패턴 사용은 불가하다), Device 제어 판넬
(2) 두개의 서로 다른 창이 있다. 하나는 Docking Window로서 Tabbed Window 중 하나이다. 다른 한 창은 Modaless의 Dialog창이다.
각 창은 똑 같은 (1의) View(ListView)를 가지며, 이 창이 동시에 존재하는 경우도 종종 생긴다. 동시에 존재할 경우 각 창은 상대편에서 일어나는 동작(선택된 아이템)을 똑 같이 적용하여야 한다. 그리고 그 결과는 연관되어 있는 다른 Frame이나 툴바 객체들에 전달되어야 한다.
그리고 적용한 Mediator의 대략 코드는 다음과 같다. (이후 많이 변경되었다. Colleague는 생성과 해제 시점에 자동으로 Mediator에 자신을 등록/해제한다. Mediator, Colleague둘다 아래 코드보다는 많이 확장되었다. 기존 각 동작에 대한 연관 객체의 메시징을 통한 상호 전달을 Mediator방식으로 전환하였다.)
장점 : 각 객체는 자신이 관심있는 동작에 대한 인터페이스만을 구현하고, 인터페이스 내에서 거의 자신만의 상태 체크와 적용만을 하면 된다. 이전에 비해 필요한 이쪽 저쪽 객체의 상태체크가 현격히 줄었다. 거의 자신의 상태만 집중하면 된다.
단점 : 등록 Colleague에 전달되는 동작이 우선순위를 가지고 Colleague에 전달되어야 할때는 사용할 수 없다. (아직 그런 문제에 직면하지는 않았다.) Recursive Storm을 예상해 볼 수 있다. Colleague에 전달된 동작이 같은 동작 Echo나 다른 동작을 야기하여 혹시나 무한 루프에 빠질 수 있는 개연성이 충분히 있는 것으로 보인다.
class CxMonsterMediator; class CxMonsterMediatorColleague { public: CxMonsterMediatorColleague() {} virtual ~CxMonsterMediatorColleague() {}
protected: virtual void SetItemState(unsigned long hostaddr, bool bSelected) {} virtual void ApplyItemState(unsigned long hostaddr, bool bSelected) {} virtual void ApplyItemDblClk(unsigned long hostaddr) {}
void CxMonsterMediator::DoSetItemState(unsigned long hostaddr, bool bSelected) { for(X_MONSTER_COLLEAGUE_LIST::iterator it = m_stColleagueList.begin(); it != m_stColleagueList.end(); it++) { CxMonsterMediatorColleague *pColleague = *it; if(pColleague) pColleague->SetItemState(hostaddr,bSelected); } }
void CxMonsterMediator::DoApplyItemState(unsigned long hostaddr, bool bSelected) { for(X_MONSTER_COLLEAGUE_LIST::iterator it = m_stColleagueList.begin(); it != m_stColleagueList.end(); it++) { CxMonsterMediatorColleague *pColleague = *it; if(pColleague) pColleague->ApplyItemState(hostaddr,bSelected); } }
void CxMonsterMediator::DoApplyItemDblClk(unsigned long hostaddr) { for(X_MONSTER_COLLEAGUE_LIST::iterator it = m_stColleagueList.begin(); it != m_stColleagueList.end(); it++) { CxMonsterMediatorColleague *pColleague = *it; if(pColleague) pColleague->ApplyItemDblClk(hostaddr); } }
class CxMonster : public CListView, public CxMonsterMediatorColleague { ... CxMonsterMediator *m_pMediator; void SetMediator(CxMonsterMediator *pMediator);
protected: virtual void SetItemState(unsigned long hostaddr, bool bSelected); ... };
class CxMonsterFrame : public CFrameWnd, public CxMonsterMediatorColleague { ... CxMonsterMediator m_Mediator;
// Implementation protected: virtual void ApplyItemState(unsigned long hostaddr, bool bSelected); virtual void ApplyItemDblClk(unsigned long hostaddr); ... };
대략 코드이다. 실제 사용을 자세히 남기지 못하는 것이 아쉽다. 핵심은 다음과 같다.
객체는 자신에게 발생한 동작을 해당 동작을 관할하는 Mediator에게 알리고 Mediator는 이를 다시 자신에게 등록된 Colleague 전체 객체에 알려 해당 행위에 맞는 각각의 Colleague 객체 고유의 동작이 이루어지도록 한다. 이때 각 Colleague 객체는 추상 Colleague의 전체 동작들 중 자신이 할 수 있는 고유 동작들만 정의 함으로서 자신에게 적용될 행위만을 구현하면 된다.
위의 실전에서 Mediator 패턴을 사용하여 해결한 핵심 부분중 하나를 예를 들면, CxMonster라는 thumbnail 이미지를 갖는 ListView 객체가 있는데, 아이템 선택 변경에 따라 툴바의 상태가 변한다. 물론 반대의 경우도 있다. 그런데 이 객체는 특이하게 여러창에 동시에 뿌려질 때가 있다. View의 속성상 부득이 하게 Instance가 두개 이상 생겨야 하지만 이루어지는 동작에 따른 Item선택 및 내부 상태가 똑같이 유지되어야 한다.
이의 해결을 위해 각각의 CxMonster Instance는 하나의 CxMonsterMediator Instance에 각각에서 일어나는 행위를 알리고 CxMonsterMediator는 다시 이 행위를 두 CxMonster Instance를 포함한 전체 Colleague에게 전파한다.
이렇게 함으로서 CxMonster Instance는 같은 클래스의 여러 Instance에 대한 복잡한 참조 필요없이 모든 Instance가 동기화 되었다. 물론 다른 방법으로도 충분히 가능하지만 한마디로 자신의 동작과 상태에만 관심을 가져도 동기화는 자동으로 이루어지는 느낌을 받았다.