이번 글의 주제는 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)

 

그리고 이와 같은 인터페이스 구현 방식을 조금 확장하여 모든 공통라이브러리 클래스를 인터페이스 포인터만을 통하여 생성, 사용할 수 있게 함으로써 큰 프로젝트의 종속성을 획기적으로 줄일 수 있게 됩니다. (이에 대한 내용도 향후 기회가 된다면 작성하겠습니다)


  1. BlogIcon 이아우 2007.07.15 06:07 신고

    좋은 글 잘 보았습니다. :)

  2. 박준학 2007.08.07 23:32 신고

    잘 읽고 갑니다.
    java rmi 기술과 유사해보이네요.
    물론 여기에서 비롯되었겟죠. ^^

    • BlogIcon esstory 2007.08.08 07:57 신고

      자바쪽은 잘 몰라 어떤 내용인지 모르지만, refactoring 이나, 디자인 패턴책은 자바관련 책이 더 많은걸로 보아 대중적으로는 자바쪽에 더 많은 좋은 기술들이 알려지고 있는거 같습니다. 저도 자바를 공부해야 할텐데 맨날 생각만 하고 못하고 있습니다. ^^ㅣ
      리플 감사합니다.

  3. BlogIcon 대갈장군 2008.10.22 02:18 신고

    요즘 MSDN에서 COM 관련 글을 조금씩 읽어보고 있는데 COM의 인터페이스 개념이 이글을 보니 조금은 알것 같습니다.

    한가지 궁금한 것이 있는데 service.DoSomthing(this)가 가능한 원리가 궁금합니다.

    this는 물론 CCaller 자신을 말하는 포인터라는 건 알지만 이 포인터가 this가 상속한 ICallerSink 인터페이스를 가리킬 수도 있다는 것이 선뜻 이해가 안갑니다.

    분명히 어떤 원리가 있을것 같아서 이렇게 질문드립니다. ^^

  4. BlogIcon esstory 2008.10.22 07:59 신고

    CCaller 클래스 선언부에서 다음처럼

    class CCaller : public ICallerSink

    ICallerSink 를 상속받도록 선언했기 때문에 this(클래스 자신을 가리키는 포인터)는 자기 자신일 수도 있고, 자신이 상속받은 여러 클래스 들 중 하나로 캐스팅 될 수 있습니다. 다중 상속받을 때도 this 는 상속받은 클래스 중 하나로 캐스팅 가능합니다. COM 에서는 모든 클래스가 IUnknown 을 상속 받도록 되어 있고 역시 (IUnknown*)this 식으로 IUnknown* 으로 this 를 캐스팅 가능합니다.^^

    • BlogIcon 대갈장군 2008.10.22 21:43 신고

      아. 그렇군요. 공통 되는 IUnknown을 상속하기 때문에. :) 답변 감사합니다. ^^

  5. BlogIcon 철이 2010.12.31 16:23 신고

    정말로 유용한 강의 인것 같습니다. ^^

  6. BlogIcon 철이 2010.12.31 16:45 신고

    이런 종류의 공부를 하고 싶은데; 어떤 책을 참고 하면 되죠;

    항상 개발을 하다 보면, 이런 유익한 정보를알게 되면, 소스 코드가 깔금 할텐데 ㅠ

    맨날 소스 코드가 드러워 집니다. ㅠ

    • BlogIcon esstory 2010.12.31 16:59 신고

      소스는 언제나 더러워지는 운명인가 봅니다.
      그래도 몇 개월에 한번씩 리팩토링 해 주면 그나마 괜찮고, 아니면 몇년 후 재개발하는게 차리리 낫고 그러더라구요.
      C++ 책은 요즘 얼마 없습니다
      제 서재 중 C++ 책으로
      http://eslife.tistory.com/293
      이나
      EFFECTIVE C++ 같은 책이 괜찮았던거 같습니다 :)

  7. 제임스뒨 2011.06.23 16:19 신고

    잘 봤습니다. 좋은 팁 하나 알았네요 ^^*

+ Recent posts