가상 함수의 활용 - Virtual Function ch.1
<가상 함수는 왜 쓸까? - 정적 바인딩의 한계와 동적 바인딩>

http://wahnfried.net







  코딩을 하다 보면 자주 만나게 되는 것 중 하나가 "virtual" 이라는 키워드입니다. 가상 함수라는 것이 막연히 '가상의 함수다' 라고 이해하기는 쉽지만, 막상 따지다 보면 생각해 볼 부분이 몇 가지 있답니다. 이번 기회에는 가상 함수 사용의 예와 팁에 대해 이야기해보기로 합니다. 가상 함수의 기본적인 내용에 대해 더 알고 싶다면, 왼쪽 메뉴의 구글 검색을 이용해서 검색해주세요. 여기에서는 최대한 간결하게 내용을 적어내려갈 예정입니다 :-)










- 문제의 발견


  말보다 코드를 보는 것이 빠른 게 프로그래머인 만큼, 아래의 예를 한 번 보지요.
  (아래는 가상 함수를 이용하지 않은 예입니다.)



023: class CAnimal
024: {
025: public:
026:         void Say()
027:         {
028:                 cout << "animal" << std::endl;
029:         };
030: };
031:
032: class CDog : public CAnimal
033: {
034: public:
035:         void Say()
036:         {
037:                 cout << "dog" << std::endl;
038:         };
039: };
040:
041: void DoSomething()
042: {
043:         CAnimal         animal;                                 ////    animal  = CAnimal
044:         CDog            dog;                                      ////    dog             = CDog
045:
046:         animal.Say();                                           ////    "animal"
047:         dog.Say();                                               ////    "dog"
048:
049:         CAnimal*        panimal = new CDog;             ////    panimal = CDog         
050:         CDog*           pdog = new CDog;                ////    pdog    = CDog
051:
052:         panimal->Say();                                         ////    "animal"        (??)    
053:         pdog->Say();                                            ////    "dog"
054:
055:         delete          panimal;
056:         delete          pdog;
057: }





  다름이 아니라 당황스러운 곳이 보이네요. 52번 줄의 결과가 그것입니다. 49번 줄에서 볼 수 있듯 panimal은 부모 class인 CAnimal형이기는 하지만, 엄연히 CDog을 가리키고 있었습니다. 헌데 52번 줄에서 Say가 불릴 때에, Overriding (재정의)된 자식 class의 함수가 아니라 부모 class의 Say가 수행되어 버렸네요. 왜냐고 물으시면 곤란합니다. 이것은 정해진 룰이니까요. 이것을 Static Binding (정적 바인딩)이라고 합니다. 무슨 말인고 하면, 아래와 같습니다.




Static Binding (정적 바인딩)

(부모 클래스의 포인터 - 자식 객체의 상속관계에서)
- 포인터가 가리키고 있는 본체의 타입에 관계 없이, 포인터의 타입에 따라 함수를 호출
- Runtime (수행 시점)이 아닌 Compile Time (컴파일 시점)에 호출할 함수를 결정한다.




  말이 조금 어렵습니다만, 그게 전부입니다. 이게 왜 문제가 될까요? 위와 같은 경우에 절대 "자식 클래스의 객체를 부모 클래스의 포인터로 넣지 않는다면 CAnimal은 "animal"이라고 말하고 CDog은 "dog"이라고 말할 텐데요?" 라고 말하고 싶은 분도 있을 겁니다. 하지만 아래와 같은 경우를 생각해 보면 그게 그리 쉽지 않은 문제라는 걸 알 수 있습니다.



023: class CAnimal
024: {
025: public:
026:         void Say()
027:         {
028:                 cout << "animal" << std::endl;
029:         };
030: };
031:
032: class CDog : public CAnimal
033: {
034: public:
035:         void Say()
036:         {
037:                 cout << "dog" << std::endl;
038:         };
039: };
040:
041: class CCat : public CAnimal
042: {
043: public:
044:         void Say()
045:         {
046:                 cout << "cat" << std::endl;
047:         };
048: };
049:
050: void DoSomething()
051: {
052:         CAnimal         animal;                                 ////    animal  = CAnimal
053:         CDog            dog;                                    ////    dog             = CDog
054:
055:         animal.Say();                                           ////    "animal"
056:         dog.Say();                                               ////    "dog"
057:
058:         CAnimal*        panimal1 = new CDog;         ////    panimal1 = CDog        
059:         CAnimal*        panimal2 = new CCat;          ////    panimal2 = CCat         
060:
061:         CAnimal*        array[2];                                                                       
062:         array[0] = panimal1;                                                                            
063:         array[1] = panimal2;                                                                            
064:
065:         for (int i = 0; i < 2; i++)
066:         {
067:                 array[i]->Say(); ////    "animal" , "animal" (??!?!?!?!?)                
068:         }
069:
070:         delete panimal1;
071:         delete panimal2;
072: }




  안타까운 상황입니다. 61~63번 줄에서 볼 수 있듯이 CAnimal 포인터 형태의 Array에 여러 종류의 동물들을 싹 넣어두고, 67번 줄에서 순서대로 말을 하게 하고 싶은 경우이지요. 기대했던 결과는 멍멍이랑 고양이가 각자 "dog" , "cat" 하며 말을 하는 것이었는데, 안타깝게도 멍멍이도 누렁이도 냐옹이도 모두 "animal" 이라는 말만 되내게 됩니다.


  물론 해결을 한다면 할 수도 있을 것입니다. 멍멍이와 야옹이에게 자신의 타입을 알 수 있도록 해서, 위의 65번째줄부터 시작되는 for 반복문 내에서 Say() 를 부르기 전에 "너는 누구냐!" 라고 묻고, 원래의 타입으로 다시 casting 한 뒤 조건문 내에서 Say를 불러 주는 방법이지요. if~else if~ 의 향연이나 엄청난 case 문의 연속이 될 테지만, 할 수는 있습니다.


  하지만 이마저도 심각한 문제가 있습니다. 서울 대공원의 모든 동물들을 모아 놓고 "앞에서부터 순서대로 자기 이름을 말해봐!" 하는 것 뿐만 아니라 이번에는 "모두들 춤 춰봐!" 라든가 "모두들 뒤집어봐(?)" 와 같은 다양한 명령을 내려야 하는 경우라면 어떨까요? 앞서 사용했던 조건문들을 매 작업마다 필요로 하게 될 겁니다.


  뿐만 아니라, 새로운 동물이 동물원에 들어올 때마다 "모두 춤추기" 함수라든가 "모두 말하기" 함수에 가서 새로운 동물을 역시 if 나 case 조건문으로 추가해주어야 할 것입니다. 상상만으로 슬프고 고된 작업입니다. 머지 않아 천문학적인 횟수의 반복 작업을 하는 자신을 발견할 수 있게 되거든요.
















- 그래서 나타난 동적 바인딩



  아래 예제를 보고 이야기하도록 하겠습니다. 차이는 극명하나 코드상으론 미미하거든요.



023: class CAnimal
024: {
025: public:
026:         virtual void Say()                                                                            
027:         {
028:                 cout << "animal" << std::endl;
029:         };
030: };
031:
032: class CDog : public CAnimal
033: {
034: public:
035:         void Say()
036:         {
037:                 cout << "dog" << std::endl;
038:         };
039: };
040:
041: class CCat : public CAnimal
042: {
043: public:
044:         void Say()
045:         {
046:                 cout << "cat" << std::endl;
047:         };
048: };
049:
050: void DoSomething()
051: {
052:         CAnimal         animal;                                 ////    animal  = CAnimal
053:         CDog            dog;                                    ////    dog             = CDog
054:
055:         animal.Say();                                           ////    "animal"
056:         dog.Say();                                               ////    "dog"
057:
058:         CAnimal*        panimal1 = new CDog;    ////    panimal1 = CDog
059:         CAnimal*        panimal2 = new CCat;    ////    panimal2 = CCat
060:
061:         CAnimal*        array[2];
062:         array[0] = panimal1;
063:         array[1] = panimal2;
064:
065:         for (int i = 0; i < 2; i++)
066:         {
067:                 array[i]->Say();      ////    result :  "dog", "cat"                     
068:         }
069:
070:         delete panimal1;
071:         delete panimal2;
072: }



  달라진 것이라고는 26번줄의 "virtual" 키워드 뿐입니다. 헌데 출력은 완전히 바뀌었네요. 각 객체는 부모 클래스의 포인터에 의해 가리켜지지만, 똑똑하게도 각자의 Overriding된 함수를 불렀구요. 이 녀석이 Dynamic Binding (동적 바인딩) 입니다.




Dynamic Binding (동적 바인딩)

(부모 클래스의 포인터 - 자식 객체의 상속관계에서)
- 객체를 가리키고 있는 포인터의 타입에 관계 없이 자식 객체의 함수를 호출
- 어떤 함수가 불리게 될 지를Runtime (수행 시점)에 결정한다.




  이제 부모 클래스의 포인터에 마음껏 자식 클래스의 객체들을 던져주어도 문제가 생기지 않게 되었습니다. 오늘은 우선 위의 내용들에 대해서만 기억해주세요. "virtual로 함수를 선언하면 부모 클래스 타입의 포인터에 집어넣어도 알아서 적당한 자기 클래스 내의 함수를 호출한다." 그리고 혹시나 해서 또 한 가지, "virtual 키워드는 상속과 다형성의 문제에 대응하는 키워드이다." 라구요. 두 번째의 이야기가 어떤 것인지는 다음 챕터에서 더 이야기하도록 하겠습니다. 일단은 그저 기억해 주세요.













- Outro


  상속의 문제만 보아도 역시 부모 되기란 어려운 일인가 봅니다. 자식과 원만한 관계를 유지하고, 때로는 부모가 원하는 길을 가게 하다가도 자식의 개성이 드러날 수 있도록 이끌어주는 일이 부모의 역할이라는 건 프로그래밍 세계에서도 마찬가지인 것 같네요. 무슨 말이냐구요? 다음 챕터에서 명쾌하게 이해하실 수 있습니다.