이번 글의 주제는 C++ 에서 호출하는 클래스(CCaller) 와 호출 받는 클래스(CCallee) 가 있을 경우 호출 받는 클래스(CCallee)에서 이벤트가 발생 등의 이유로 호출하는 클래스(CCaller)의 어떤 함수를 호출하고자 할 경우 사용할 수 있는 안전한 방법에 대해 얘기하고자 합니다.
일반적인 함수호출은 아래 그림과 같습니다.
오늘 설명 드리고자 하는 내용은 반대로 CCallee 클래스에서 CCaller 클래스의 Method를 종속성을 해하지 않고 호출하는 방법을 알아 보려고 합니다.
CCaller 는 CallSerice() 멤버 함수에서 CCallee 의 인스턴스를 만들고 CCallee::DoSomething() 을 호출합니다.
또한 CallBackMe() 라는 메쏘드를 준비하고 있는데 이 메쏘드는 추후 CCallee 에 의해 이벤트 발생시 불려지는 함수라고 설정했습니다.
// Caller.h #include "Callee.h" class CCaller { public: CCaller(); virtual ~CCaller();
bool CallSerice() { // CCallee 인스턴스를하나만들고 CCallee service; // CCallee 의함수를호출합니다. service.DoSomething();
} bool CallBackMe(const char* lpText) { printf("call back 함수호출됨 %s", lpText); } }; |
CCallee 는 DoSomething() 이라는 메쏘드를 제공합니다. 단순히 외부에서부터 불려지는 일반 함수인데, 이 함수에서 만약 호출한 클래스(CCaller) 의 CallBackMe() 를 호출할려면 어떤 방법이 있을까요?.
// Callee.h class CCallee { public: CCallee(); virtual ~CCallee();
public: bool DoSomething() { // Do Something
// CCaller::CallBackMe() 를호출할수있을까?
} }; |
가장 무식하고 간단하지만 나중에 문제가 되는 방식은 “Caller.h” 를 직접 #include 하고 함수 인자로 호출하는 클래스의 포인터(CCaller*) 를 전달받는 방식입니다.
// Callee.h #include "Caller.h"
class CCallee { public: CCallee(); virtual ~CCallee();
public: // 문제가 되는 코드 - 함수 인자로 CCaller의 포인터를 넘깁니다 bool DoSomething(CCaller* pCaller) { // Do Something
pCaller->CallBackMe();
} }; |
이 방식이 왜 위험할까요?
1. 가장 큰 문제는 CCallee 클래스와 CCaller 가 서로 엉키어서 둘중 하나가 변경될 경우 상호 컴파일이 일어난다는 점입니다. 아주 간단한 프로젝트라면 모르겠지만 대규모 프로젝트에서는 작은 소스 수정하나가 다른 소스, 다른 모듈에 종속될 경우 상호 컴파일 및 재배포의 과정을 거쳐야 하는데 이는 경우에 따라 큰 짐이 될 수가 있습니다.
2. DoSomething() 함수는 이제 CCaller* 함수만 인자로 받을 수 있게 됩니다. 만약 CCaller 가 아닌 다른 클래스에서 DoSomething() 함수를 호출하고자 한다면 어떻게야 할까요?.
DoSomething(CAnother*) 와 같은 별도 함수를 만들어야 할까요?. 서비스를 받고자 하는 클래스가 늘때마다 비슷한 함수를 또 만들어야 하고 서로간의 상호 참조도 늘어나게 되어 종속성이 심화됩니다.
3. ‘2’번의 이유로 DoSomething() 함수가 범용적으로 사용될 수 없게 되고 두 클래스의 의존도로 인해 하나의 클래스를 라이브러리화 시키기가 힘들어 집니다.
위 방법의 문제들을 해결하면서 우아하게 CallBackMe() 함수를 호출하는 방식은 다음과 같습니다.
우선 CCallee 가 만들어 내는 이벤트를 정의할 ICallerSink 인터페이스를 선언합니다.
일반적으로 인터페이스는 소멸자를 제외한 순수 가상함수로만 이루어진 클래스입니다.
interface ICallerSink { virtual ~ICallerSink(){}; virtual bool CallBackMe(const char* lpText) = 0; }; |
인터페이스정의가 끝나면 이 이벤트를 받을 클래스인 CCaller 클래스에게 해당 인터페이스를 상속받고 구현하도록 코드를 수정합니다.
// Caller.h #include "Callee.h" // 이벤트 수신을 위해 ICallerSink 인터페이스를 상속받습니다. class CCaller : public ICallerSink { public: CCaller(); virtual ~CCaller();
bool CallSerice() { // CCallee 인스턴스를하나만들고 CCallee service; // CCallee 의함수를호출합니다. service.DoSomething(this);
}
// ICallerSink::CallBackMe() 을구현합니다. virtual bool CallBackMe(const char* lpText) { printf("call back 함수호출됨 - %s", lpText); } }; |
이제, 실제 이벤트를 발생할 클래스를 조금 수정합니다. 이벤트를 발생하기 위해서는 이벤트를 전달받아 처리하는 ICallerSink 인터페이스를 상속받은 클래스를 알아야 하기 때문에 DoSomething 함수의 인자로 ICallerSink 인터페이스 포인터를 전달받아 처리하도록 아래와 같이 수정했습니다.
// Callee.h interface ICallerSink { virtual ~ICallerSink(){}; virtual bool CallBackMe(const char* lpText) = 0; };
class CCallee { public: CCallee(); virtual ~CCallee();
public: // ICallerSink 이벤트포인트를전달받습니다. bool DoSomething(ICallerSink* pSink) { // Do Something
// 가상함수를통해실제로는CCaller::CallBackMe() 을호출하게됩니다. pSink->CallBackMe(“이벤트 발생”);
} }; |
이와 같이 인터페이스를 하나 두어 구현할 경우 장점은
1. CCallee 클래스는 CCaller 클래스에 대한 어떤 참조도 하지 않습니다. 당연히 CCaller 클래스 헤더의 변경등이 있더라도 CCallee 클래스가 컴파일 될 이유가 없습니다.
2. CCallee::DoSomething 함수는 ICallerSink 인터페이스를 상속받는 어떠한 클래스에게도 서비스를 제공할 수 있습니다. 물론 호출하는 클래스가 ICallerSink 를 구현해야 한다는 부담은 아주 조금 있지만, 종속성을 제거하고 향후 유지보수를 쉽게 하기 위해서는 이러한 방식이 최선입니다.
사실 이러한 인터페이스를 통한 쌍방향 함수 호출방식은 COM 관련 작업을 해 보신 분이라면 누구든 아는 방법입니다. (Developer’s Workshop To COM and ATL 3.0 539Page)
그리고 이와 같은 인터페이스 구현 방식을 조금 확장하여 모든 공통라이브러리 클래스를 인터페이스 포인터만을 통하여 생성, 사용할 수 있게 함으로써 큰 프로젝트의 종속성을 획기적으로 줄일 수 있게 됩니다. (이에 대한 내용도 향후 기회가 된다면 작성하겠습니다)
'개발 > C C++' 카테고리의 다른 글
[C/C++] enum, 보다 나은 enum (19) | 2008.07.21 |
---|---|
싱글톤 클래스 (5) | 2008.07.01 |
[C++]STL Container 조합하기 (5) | 2007.11.12 |
[C/C++]유용한 #pragma directive (9) | 2007.09.05 |
[C/C++]#define 매크로 팁 (16) | 2007.06.29 |
C++ 성능이야기 – MFC CString 인자로 사용하기 (4) | 2007.04.27 |
댓글