home > working > C/C++ > Source, Tip

 


Name  
   조규남 
Subject  
   [강좌] How to use C++ in MFC No.2 Virtual Method
[2차] Virtual Method

Virtual Method 또는 Virtual Function 우리 말로는 가상 함수라고 한다. C에 Pointer가 있다면 C++에는 Virtual Method가 있다. 이놈도 참 이해 하기 힘들다. 책을 보면 다형성 어쩌구 저쩌구 하면서 설명이 나와있는데 나의 짧은 머리로는 도저히 이해가 안간다. 다형성이 뭐가 어쩐다는건가? 도저히 이해 할 수 없을 뿐이다. 그래서 나같이 머리 짧은 사람들을 위해서 쉽게 설명해주겠다.
이름을 보자 "가상 " 그래 가상의 함수라는 뜻이다. 뭔소리인가? 쉽게 얘기해서 함수가 제대로 만들어 있지 않아도 된다는 뜻이다. 코드를 작성 할 때 함수 선언만 하고 구현을 하지 않아도 된다는 뜻이다. 우선은 이렇게만 기억하도록 하자 가상 함수는 가짜 함수이다. 즉 형태는 없구 개념만이 존재하는 함수이다. 이런 함수가 왜 필요할까? 유식한 말로 다형성을 위해서이다. 설명하는 나도 다형성이라는 유식한 말에 머리가 아파온다.
가상함수가 왜 필요하냐 한마디로 얘기를 하면 함수의 선언은 부모에서 하고 구현은 부모를 상속 받은 자식 클래스에서 하면 된다. 이게 무슨 의미가 있을까? 가상함수를 사용해서 부모가 함수를 선언해 놓으면 그 부모를 상속 받은 모든 클래스에서 그 함수를 호출 할 수 있다. 구현이 되어 있다면 구현된 코드대로 돌아 갈 것이고, 그렇지 않다고 한다면 아무일 도 없을 것이다. 이렇게 함으로써 어떤 이점이 있을까? 가상 함수를 사용한다면 우리는 부모 클래스를 통해서 자식이 구현해 놓은 가상 함수에 접근 할 수 있다. 한 부모 클래스에서 파생된 자식 클래스는 각자 입맛에 맞게 가상 함수를 구현을 할 수 있고, 우리는 코드에서 각 자식이 구현한 가상함수를 부모를 통해서 호출 할 수 있다는 것이다. 코드로 살펴 보자

class CVParent
{
public:
        virtual void func1()=0;
}

class CVChild : public CVParent
{
public:
        virtual void func1();
};

void CVChild::func1()
{
        printf("CVChild's body\n");
}

class CVOtherChild : public CVParent
{
public:
        virtual void func1();
};

void CVOtherChild::func1()
{
        printf("CVOtherChild's body\n");
}

이런 Class 선언이 있다. 우리는 CVChild와 CVOtherChild가 CVParent의 func1()을 Override 한 것을 볼 수 있다. 하지만 이렇게 보면 Member function의 Override와 별로 달라 보이지 않는다. 사실 다르지 않다. 다음 코드를 보자

static void VirtualCall(CVParnt *pVTest)
{
        pVTest->func1();
}

static void VirsualCallTest()
{
        CVChild                child1;
        CVOtherChild        child2;

        VirtualCall(&child1);        // 첫번째 호출
        VirtualCall(&child2);        // 두번째 호출
}

이렇게 하면 첫번째 호출과 두번째 호출에서 각각 CVChild와 CVOtherChild의 Override 된 func1()이 호출된다. 주목할 점은 VirtualCall이라는 함수는 CVParent의 포인터를 인자로 받았다는 것이다. 자식은 부모로 Type casting 될 수 있다. 이렇게 되면 VirtualCall 이라는 함수는 부모의 Class 명세를 보고 두개의 자식 클래스 객체에서 func1()일라는 함수를 호출 할 수 있다. 또 CVParent는 func1()에 대한 구현을 하지 않았지만 자기 자신 또는 외부에서 func1()을 호출 할 수 있다는 것이다.
하지만 아직까지 아리송 하다. CVChild와 CVOtherChild를 인자로 받는 함수를 두개 만들면 되지 왜 가상 함수라는 걸 구지 사용하는지 이해 할 수 없다. 필자도 처음 배울때 이해 할 수 없었다. 강사도 설명해 주지 않았다. -_-; 요지는 부모도 func1()이라는 함수의 선언을 가지고 있기 때문에 부모 Class를 통해서 자식들의 func1()을 호출 할 수 있다는 것이다. 그렇게 되면 VirtualCall 함수에서는 자식의 명세를 전혀 알 필요가 없다. VirtualCall이라는 함수에서는 CVParent의 func1()이라는 함수의 선언만 알고 있으면 되는 것이다.
하지만 그래도 이해가 안갈 것이다. 이해가 된다면 이제 그만 하산을 하도록 어서 Ctrl + W 를 누르도록
여기서 시간이라는 개념을 넣어 보자! CVParent와 VirtualCall을 작성한 사람은 메인 프로그래머 이다. 그가 처음 CVParent와 VirtualCall을 만들었다. 그리고 몇 개월 후 신입 프로그래머가 들어 왔다. 그는 CVChild를 만들었다. 하지만 VirtualCall을 다시 짤 필요가 없다. 왜냐하면 func1()의 선언을 VirtualCall이 이미 알고 있기 때문이다. 그리고 더 많은 시간이 흘러서 다른 신입 프로그래머가 CVOtherChild Class를 만들었다. 그 역시 VirtualCall을 다시 짤 필요가 없다. 이렇게 프로젝트가 완성이 된다. 이 프로젝트가 코드로 계속 보전이 된 것이 아니라 DLL등의 바이너리로 되어 있었다면 Virtual 함수 없이는 불가능 했을 것이다.
이해를 돕고자 빵집을 예를 들어서 설명해 보겠다. 맛있다 빵집의 주인아저씨는 빵을 참 잘 만든다. 그리고 그 아저씨에게는 후계자 아들이 있어야 하는건 당연한 수순이다. ^^; 어느날 외국으로 여행가는 사람이 찾아와서 10년뒤 다시 돌아와서 이집 빵을 먹고 싶다구 말을 했다. 10년이면 사회적 문화도 바뀔 수 있고 심지어 언어도 바뀔수 도 있기 때문에 미리 약속을 받아 놓는 것이다. 주인 아저씨는 10년 후에 와서 코끼리 코를 해서 바닥에서 10번 돌면 빵을 준다구 했다. 코끼리 코를 해서 바닥에서 10번 도는 것은 바로 Virtual 함수의 선언이다. 외국으로 여행가는 사람이 지금 코끼리 코를 해서 바닥에서 10번을 돈다구 해도 아저씨는 빵을 주지 않을 것이다. 아직 구현이 되지 않았기 때문이다. 이것이 바로 순수가상 함수 pure virtual function 이다. 여행자는 여행을 갔고 주인아저씨는 후계자 아들에게 훗날 어떤 사람이 와서 코끼리 코를 하고 바닥을 10번 돌거든 맛있는 빵을 주라고 했다. Virtual Function의 상속이 이루어 졌다. 후계자 아들이 빵집을 물려 받아서 빵을 만들면서 아들은 생각했다. 누군가 코끼리 코를 하고 바닥을 10번 돌면 맛있는 곰보빵을 만들어 줘야지! 이제 Virtual Function의 구현이 이루어 졌다. 10년 후 여행자가 여행에서 돌아와서 코끼리 코를 하고 바닥을 10번 돌자 아들이 곰보빵을 주었다. Virtual Function이 호출 된것이다.
여기서 빵집 아저씨는 CVParent 이고 코끼리 코를 하고 바닥을 10번 도는 것은 func1() 이다. 여행자가 빵찝 아저씨한테 func1() 즉 코끼리 코를 잡고 바닥을 10번 도는 것을 호출해도 빵찝 아저씨는 빵을 주지 않을 것이다. 하지만 후계자 아들어 func1()을 구현하고 난 뒤에 여행자는 func1()을 통해서 곰보 빵을 얻을 수 있다.
그리고 옆 마을에 빵집 아저씨의 딸이 빵집을 열어서 코끼리 코를 하고 바닥을 10번 돌면 단팥빵을 주기로 했다면 CVOtherChild가 func1()을 구현한 것이다.
빵얘기가 나오니깐 좀 쉽지 않은가?
즉 가상 함수는 자식 Class 어딘가에서 구현될 것을 예상하고 미리 만들어 놓는 것이다. 구현을 해도 되고 하지 않아도 된다. 구현을 함께 하면 Virtual Function이고 구현을 하지 않으면 Pure Virtual Function이다.
이런 가상 함수가 존재 하기 때문에 COM이라는 바이너리로 된 코드가 내가 새로 만든 Class의 Virtual Function을 호출 할 수 있게 되는 것이다.


자 그럼 이제 강좌 취지에 맞게 MFC에서는 Virtual 이라는 놈이 어떻게 사용되는지 알아 보도록 하자. Dialog Base Applicatoin을 만들어 보자. Dlg Class의 함수들을 살펴 보면 OnInitDialog()가 보일 것이다. 함수의 선언을 보면 virtual 이라는 예약어가 붙어 있다. 이외에도 OnOK()나 OnCancel() 같은 함수도 virtual 함수이다. 왜일까? 이들 함수를 잘 살펴 보자 OnInitDialog()은 Dialog가 초기화 되면서 호출되는 함수 있다. OnOK()나 OnCancel은 확인이나 취소 버튼을 눌를 때 호출 된다. 이들 함수들은 MFC의 Framework에 따라서 일정한 순서 또는 일정한 이벤트에 의해서 호출 되는 함수들이다. 기본적으로 Override되어 있지는 않지만 OnMotify(), DoModal()같은 함수들도 일정한 규칙에 의해서 호출되는 함수들이다. 프로그래머는 자신이 원하는 시점에 호출되는 virtual function을 override해서 원하는 작업을 할 수 있다. CDialog는 virtual 함수들을 순서에 의해서 호출을 하고 사용자가 특정 시점의 vitrual function을 Override 함으로써 원하는 작업을 수행할 수 있는 것이다. 이것이 바로 virtual 함수가 필요한 이유인 것이다. 생각보다 간단하다. 사실 virtual 이라는 개념이 없어도 OnInitDialog()가 Dialog 초기시점에서 호출된다는건 이해할 수 있다. 필자도 그랬다. 하지만 virtual function을 이해하기 위해서 OnInitDialog()를 살펴 보면 virtual function도 이해하고 OnInitDialog()도 이해하고 일석 이조 아닌가?
COM을 사용하기 위해서는 virtual function의 이해는 기본이다. COM에서 interface를 얻어 와서 interface를 통해서 COM의 함수를 호출하는 것은 쉽다. COM의 개념이 쉽다는 것이 아니라 COM에서 virtual 함수의 쓰임새가 쉽다는 얘기이다. 그럼 일반적인 COM의 사용이외에 어떤 좋은 점이 있을까? 바로 Automation과 Shell Namespace Extention 같은 것들 이다. Automation이 무엇일까? Automation이란 어떤 프로그램에서 자신의 시스템에 추가적인 기능을 코드 레벨에서가 아닌 BInary 레벨에서 추가 할 수 있게 한 기능이다. 방법은? Automation에 필요한 Interface를 상속 받아서 구현을 한다. 물론 이때 COM으로 작성해야 한다. 그리고 COM을 등록 한 후에 Automation에 필요한 특정 레지스트리에 등록을 한다. 그러면 끝! 말은 쉽지만 구현해야 할 Interface들이 좀 많을 것이다. 아무튼 이렇게 등록이 끝나면 해당 프로그램이 실행 되면서 레지스트리의 특정 위치에서 Load해야 할 COM들의 목록을 보고 Interface를 통해서 필요한 함수들을 호출 한다. Interface를 충실하게 구현했다면 그 프로그램의 메뉴나 화면 어느 곳엔가 내가 만든 프로그램들이 돌고 있을 것이다. 인터넷 익스플로러에 보면 엠에스엔이나 프론트 페이지 아이콘이 툴바에 보이는 경우가 그런 과정을 통해서 만들어 지는 것이다. Shell Namespace Extention 역시 Automation의 일종인데 Windows Shell의 특정 위치에 내가 원하는 View로 표현하기 위한 방법이다. 지금 탐색기를 열어 보도록 해보나. 폴더 이외에 내 문서 등이 있고, WS_FTP나 Bluetooth 환경, PDA 폴더 등등이 보일 것이다. 이것들이 Shell의 확장으로 보여지는 COM 모듈들이다.
폴더 내에서도 Shell 이 확장 될 수 있는데 .NET 개발툴을 설치 한 사람이라면 Windows 폴더 바로 아래 있는 assembly 라는 폴더를 열어 보자. 다른 폴더와는 다르게 파일 목록이 아닌 assembly 목록이 나오는데 파일 뷰와는 다를 것이다. 이부분이 .NET Framework이 Shell Namespace Extention으로 확장된 GAC(Global Assembly Cache)이다. Command Line에서 assembly로 이동하면 GAC와 Native Image등의 폴더로 되어 있다. 또 GAC 폴더에는 탐색기에서 볼 수 있는 어셈블리 이름으로 된 폴더가 존재 하는데 이 폴더 안에 어셈블리와 어셈블리 정보 파일 ini가 들어 있다. 이렇게 특정 폴더를 볼 때 확장된 다른 모습으로 볼 수 있게 해주는 것이다. Shell Namespace Extention이다.

Virtual function은 C++에게 많은 유연성을 가질 수 있게 해주었다. 단순히 가상의 함수이고 메모리를 어떻게 가지고 하는 단편적인 모습으로의 virtual function이 아닌 좀 더 큰 그림으로 이해를 하고 그 속에서 virtual function의 쓰임새를 본다면 왜 virtual function이 필요한지 쉽게 알 수 있을 것이다.


Name Memo Password  
        


Prev
   VC단축키 정리

이상수
Next
   System의 CreateWindow 함수를 모니터링 하는 프로그램 [1]

조규남


Copyright 1999-2018 Zeroboard / skin by JiYoo / edit by Mystous