본문 바로가기

프로그래밍/Delphi

virtual 과 dynamic 그리고 override 의 내부 메카니즘 소개

[출처 : http://sojimara.egloos.com/4429651]

본 글은 어느 외국 델파이 매거진에 기재된 Q&A 내용입니다. 내용이 너무 좋아
강좌란에 등재합니다. 본인이 아직 번역이 서툴러 재대로 번역해 놓지 못한 부분이
많을 것이니 이점은 양해를 부탁드립니다. 하지만 객체지향의 다형성을 지지하는
virtual 과 dynamic 그리고 override 에 대한 보다 깊은 이해에 도움이 될것이라
생각됩니다.

Virtual 과 Override 에 대한 이해

여러분들은 델파이가 왜 다형성을 지원하기 위해 virtual 과 override 지시어를 가지는지 아는가?

델파이는 사실 다형성을 지원하기 위해 세 가지 지시어(virtual, dynamin, override)를 가진다. 그렇다고 너무 복잡한 물건들이 아니니 일단 이 질문에 답을 달기전에 프로그램 작업에서 언제 다형성을 구사할 필요가 있는지에 대해 먼저 생각해 보자.

다형성은 어떤 상황속에서 어떤 오브젝트의 메소드를 호출 할 것인지를 런타임시에 택할 수 있다는 것이다. 런타임시에 오브젝트화가 될 수 있는 인스턴스의 대상은 하나로 고정된 것이 아니고 여러 개의 각기 다른 클래스들 일 수 있다는 말임을 유념하기 바란다.

컴파일러는 클래스가 런타임시에 등장할지 안 할지를 선견할 수 없기에 클래스의 실행주소를 실행전에 미리 계산해 놓을 수가 없다. 그러므로 런타임시에 검사를 하여 수행할 코드를 호출해야 하며 적당한 클래스 코드를 얻어와야 한다.

C++ 과 델파이방식에서는 클래스 선언부에 virtual 이란 지시어를 넣어주므로써 Base 클래스에 새로운 다형성 루틴을 작동시킨다. 만약에 Base 클래스로부터 상속된 새로운 클래스를 만든다면 개발자는 virtual 메소드에 대해 새로운 기능을 선언하고 싶어 질 수 있다. C++에서는 virtual 지시어를 재사용 하므로써 메소드를 재선언 하도록 한다. 하지만 델파이에서는 override 지시어를 사용해야만 한다.

2방식의 차이점에 대해 좀더 살펴 보도록 하자!

새로운 다형성 루틴을 추가하여 기존 다형성 루틴을 재선언 한다고 할때, C++은 하나의 지시어(virtual)로 두가지 조작을 모두 행 할 수 있다. 델파이는 두개의 키워드(virtual, override)를 이용해 상황을 구분지워 줘야 한다.

아래 예제를 보자.
var Cars : array[1..2] of TCar;
...
Cars[1] := TCar.Create;
Cars[2] := TRacingCar.Create;
Cars[1].Drive;
Cars[2].Drive;
Cars[1].Free;
Cars[2].Free;

두개의 TCar 오브젝트들은 배열로 선언되어 있다. 배열의 요소로는 TCar 로부터 상속된 상황별 오브젝트들 할당되어 있다. 어떤 TCar 나 TCar 의 자손은 프로그램의 실행중에 배열의 요소로 할당 될 수 이다. 컴파일러는 배열의 요소인 오브젝트들에 대해 미리 컴파일 해 놓을 수 있는 기회를 가지기 못하기 때문에 런타임시에 적합한 코드를 찾아 생성시켜야 한다.

virtual과 override 지시어를 호출하므로써 작업시에 다형성을 지원하게 해준다. 그러나 그 지시자들이 내부적으로 과연 어떤 기능을 할까?

런타임때 까지 대기했다가 실행 할 것을 찾는 것을 late binding 이라 칭하고, 보통 컴파일시에 실행할 것을 결정하는 컴파일 작업을 early binding 이라 칭한다.

Late binding 은 델파이와 C++에서 virtual method tables(VMTs)라 불리우는 도구에 의해 주소지들의 테이블들을 검사한다. 각각의 클래스는 컴파일러에 의해 만들어진 하나의 VMT를 가지고 있는데 그것들은 heap 상에 저장되어 있게 된다.(볼랜드 파스칼에 의해 만들어진 오래된 오브젝트 모델에서는 datasegment 에 저장되어져 클래스들의 전채 수량이 제한되어 지기도 했다.)

VMT는 클래스를 위한 모든 virtual 메소드들의 주소를 가지고 있으며 virtual 메소드를 호출할 때 VMT 안의 offset 에 저장된 주소지로 간접이동하여 실제의 코드를 생성한다.

델파이에서 virtual 지서어를 추가하는 것은 VMT상에 새로운 엔트리를 추가하는 것이다.
override 는 VMT에서 엔트리를 변경(수정)하는 것이다.

C++과 예전 파스칼 오브젝트 모델의 차이점은 같은 이름의 메소드를 선언하기 위해 새로운 VMT 엔트리를 어디에 추가할 것인가 하는 것이다. 그렇지만 virtual 은 반드시 VMT 엔트리에 연계해 변경해야한다. 아래 예에서 TCar 와 TRacingCar VMT 구조를 보여 준다.

TObject's VMT TCar's VMT TRacingCar's VMT

TObject.DefaultHandler TObject.DefaultHandler TObject.DefaultHandler
TObject.NewInstance TObject.NewInstance TObject.NewInstance
TObject.FreeInstance TObject.FreeInstance TObject.FreeInstance
TObject.Destroy TObject.Destroy TObject.Destroy
TCar.Drive TRacingCar.Drive

각각의 클래스의 VMT는 선조의 것을 포함함 모든 virtual 메소드들을 가지고 있다는 것에 관심을 가져야 한다. 그 이유는 선조와 후손 클래스 둘다가 virtual 의 사용을 시도할 것을 고려 해 주었기 때문이다. (C++에서도 마찬가지이다.) 우리는 이런 상황에서 결과를 예측할 수가 없을 것이다. 대신에 VMT는 아래 예제 처럼 보여질 수 있다.

TRacingCar's VMT
TObject.DefaultHandler
TObject.NewInstance
TObject.FreeInstance
TObject.Destroy
TCar.Drive
TRacingCar.Drive

virtual 의 이중사용은 새로운 엔트리들의 추가를 의미한다. 두개의 메소드들은 강제적으로 동일한 이름을 가지게 된다.

이쯤에서 Dynamic 지시어에 대해 얘기를 시작해야 한다.
Dynamic은 virtual 지시어 대신에 사용이 가능하다. 그리고 dynamic method table (DMT)에 엔트리 추가가 가능하다. DMT는 VMT와 동일해 보이지만 하나 다른 점이 있다. VMT는 선조와 자손의 모든 virtual 메소드들에 대한 엔트리를 보관하지만 DMT는 오직 해당클래스에서 선언한 dynamic 메소드들에 대한 엔트리 만을 저장한다는 것이다. 이 것은 DMT가 VMT보다 적은 메모리를 사용한다는 것을 의미한다. 델파이 메시지 핸들러들은 dynamic 메소드들로 도구화 되어져 있다. 어떤 주어진 클래스의 선조 클래스들이 많은 메시지 핸들러들을 가질 수 있을때 과도한 메모리 사용을 방지하기 위해 DMT에는 오직 해당 클래스 자신의 메시지 핸들러 만을 저장하는 것이다. 그렇지만 DMT는 또 다른 의미를 내포하고 있다.. dynamic 메소드들을 이용해 메소드 작업을 호출하기 위해서는 꼭 필요한 메소드를 찾기위해 사전에 그것의 선조 엔트리들을 먼저 검색해 보아야 한다. 그것은 퍼포먼스의 저하를 초래할 수 있다. (결국 VMT에 비해 메모리는 적게 사용하는 대신에 퍼포먼스가 다소 느려 질 수 있다는 말이다.)

실행속도에 구애받지 않는 메소드라면 개발자는 virtual을 대신하여 dynamic을 사용해도 무관하겠다. override 지시어는 virtual 과 dynamic 메소드 모두의 재선언에 사용되어 진다.

그럼 원래의 질문으로 다시 돌아가, 왜 메소드의 재 선언이 필요한지에 대해 생각해 보자!
override 지시어의 추가는 앞으로 확장 클래스가 제공될 수 있음을 의미한다. 몇 몇 가능한 시나리오에 대해 고려해 보도록 하자.

최초로 델파이 1버젼 VCL에서 Virtual 로써 TEdit의 선조를 만들어 어떤 새로운 메소드 Foo를 추가 했다고 가정해 보자. 델파이 2버젼 VCL에서 볼랜드는 TEdit에 Foo 라 불리우는 새로운 virtual 메소드를 추가했다. override 구문이 없었기에 버전을 업데이틀 했을때 사용자의 코드는 작동이 멈춰 버리는 결과를 초래할 것이다.

두 번째 시나리오로 델파이 1의 VCL 은 bar 라 불리우는 메소드를 가지게 되었다고 가정해 보자. 그것은 virtual 로 지정되어 후손 클래스 사용자들은 override를 사용할 수 있었다. 델파이 2버젼 VCL에서 볼랜드는 그것의 메모리사용에 주안을 두고 dynamic 메소드로 변경하기로 결정했다. 만약 overrde 구문이 있지 않았다면 첫 번째 시나리오에서는 사용자 코드가 작동을 정지 할 것이다. 이유는 virtual 메소드의 재선언에 virtual을 사용했으며 dynamic 메소드의 재선언에 dynamic을 사용 해야만 했기 때문이다.

첫 번째 시나리오의 문제점은 랭귀지에 override 가 추가된 가장 큰 이유이다. 기본 클래스들의 메소들에 virtual 지시어를 지정하는 것은 제품의 버전들 사이에 공정한 규칙성을 발효시킨다. virtual에서 dynamic 으로의 변경인 두 번째 시나리오는 거의 일어나기 힘든 경우의 작업이다. 주요한 사실은 override를 사용함으로써 Base 클래스의 변경에 따른 화근을 줄일 수있다는 것이다. 이것의 의미는 델파이의 차기버젼이 나올때 사용자의 컴퍼넌트 클래스가 수정해야할 작업이 있을 가능성이 희박해 진다는 것이다.

요약해 보면, virtual이나 dynamic 단독으로도 보다 뛰어난 결과물을 제공한다는 것이다. 만약 Base 클래스의 인터페이스에 변경이 발생하면 자손클래스의 소스코드들 조차 모두 리컴파일 시켜야 한다. 그렇지만 override 가 존재함에 따라 자손클래스의 소스는 수정을 가해야할 이유가 적어지게 된 것이다.

감사의 말
virtual 과 override 에 관한 세부적인 답변을 해 주신 Borland 사의 Allen Bauer 와 Danny Thorpe 님께 감사의 말씀을 전해 드리고 싶습니다.