본문 바로가기
개발/C C++

[C++]인터페이스를 이용하여 상호참조와 종속성을 최소화하는 방법

by esstory 2007. 7. 15.


이번 글의 주제는 C++ 에서 호출하는 클래스(CCaller) 와 호출 받는 클래스(CCallee) 가 있을 경우 호출 받는 클래스(CCallee)에서 이벤트가 발생 등의 이유로 호출하는 클래스(CCaller)의 어떤 함수를 호출하고자 할 경우 사용할 수 있는 안전한 방법에 대해 얘기하고자 합니다.

 

일반적인 함수호출은 아래 그림과 같습니다.

사용자 삽입 이미지

 
오늘 설명 드리고자 하는 내용은 반대로 CCallee 클래스에서 CCaller 클래스의 Method를 종속성을 해하지 않고 호출하는 방법을 알아 보려고 합니다.

사용자 삽입 이미지

설명을 진행하기 위해 간단하게 CCaller CCallee 를 만들었습니다.

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

댓글