저작권에 대한 공지
이 문서에 포함된 모든 글과 소스 코드는 작자인 저 김성동의 지적 재산이지만 무단으로 배포하거나 책이나 강의 교재등을 위한 복제 등 저작권을 저해하는 행위를 제외하고는 자유롭게 사용할 수 있습니다.
또한 이 페이지에 대한 링크는 자유롭게 할 수 있으나 전문을 전제하는 일은 허용하지 않습니다.


1. 콤포넌트 개발 기초

1.1 개요

이 문서를 읽고 있는 독자들 중에서 델파이가 콤포넌트를 기반으로 한 어플리케이션 개발 환경이라는 걸 모르는 독자는 없으리라고 생각한다. 하지만 콤포넌트 팔레트에서 사용할 콤포넌트를 폼 위에 끌어다 놓고 콤포넌트의 속성을 설정하고 적절하게 다른 콤포넌트나 폼과의 관계를 설정해 주고 이벤트 핸들러에 코드를 삽입하기만 하면 모든 것이 끝날까? 우리가 개발하고자 하는 어플리케이션에서 필요한 모든 기능이 이미 콤포넌트로 만들어져 있어서 우리는 그걸 그냥 사용하기만 하면 될까? 아마도 실제 업무에 사용하려면 여러 가지 이유에서 콤포넌트의 부족함 또는 어떤 특정 기능을 하는 콤포넌트가 있었으면 하는 생각을 필자가 그랬던 것처럼 한번쯤은 하게 될 것이다.
델파이가 다른 개발 툴에 비해서 여러 가지 장점을 가지고 있지만 그 중에서도 주목할 만한 것은 바로 콤포넌트를 쉽게 개발할 수 있다는 것이다. 델파이로 어플리케이션을 개발할 때 사용하는 기술에다 오브젝트 파스칼에 대한 지식을 조금만 더하면 원하는 기능을 하는 콤포넌트를 쉽게 개발할 수 있다. 마이크로소프트의 ActiveX 콘트롤과 달리 델파이 콤포넌트가 주는 이런 편리함이 델파이 프로그래밍 세계에서 하루에도 수십 개의 공개 혹은 상용 콤포넌트들이 개발되고 발표되는 원동력이라 할 수 있겠다. 필자는 델파이가 나오기 전 비주얼 베이직을 사용해서 몇몇 어플리케이션을 개발한 적이 있는데 델파이와 유사한 개발 환경을 가진 비주얼 베이직이 가졌던 맹점은 비주얼 베이직에서 사용할 콘트롤을 개발하려면 사용하기 쉬운 비주얼 베이직이 아닌 비주얼 C++을 이용해서 개발해야 한다는 것이었다. 물론 비주얼 베이직 5.0부터는 베이직만으로도 콘트롤을 개발할 수 있게 되었지만….(비주얼 베이직이나 비주얼 C++등 마이크로소프트사의 개발 도구들은 콤포넌트라는 용어 대신 콘트롤이라고 한다.)
콤포넌트 개발이나 어플리케이션 개발이나 어차피 같은 델파이 개발 환경을 가지고 개발하지만 콤포넌트 개발은 어플리케이션 개발과 비교해서 많은 차이점을 가지고 있다.
첫번째로 콤포넌트를 개발하는 것은 비주얼하지 않다(Non-Visual). 콤포넌트 개발은 폼 기반의 개발 환경이 아니고 코드 에디터로 오브젝트 파스칼 코드를 직접 작성하는 것이 대부분의 일이다. 델파이의 장점인 폼 디자이너나 오브젝트 인스펙터를 거의 사용하지 않고 내장 디버거나 오브젝트 브라우저등을 더 많이 사용하게 될 것이다.
두번째로 콤포넌트는 일반 어플리케이션과 다른 사용자를 가지고 있다. 바로 델파이를 사용하는 다른 개발자들이다. 이들은 콤포넌트가 델파이 개발환경 내에서 어떤 식으로 사용되고 어떻게 동작할거라는 것을 어느 정도 예상하고 있다. 예를 들어 콤포넌트의 이벤트는 관습적으로 On이라는 글자로 시작한다. 이 규칙을 꼭 지킬 필요는 없다. OnClick을 MyClick처럼 만들어도 아무런 문제 없이 사용할 수 있다. 하지만 대부분의 개발자가 알고 있는 관습을 지키지 않고 콤포넌트를 개발했다면 그 콤포넌트를 사용하는 개발자들은 혼란스러워 하게 되고 결국 외면당하지 않을까 생각된다.
세번째로 콤포넌트 개발은 어플리케이션 개발보다 더 객체지향적이다. 콤포넌트 개발은 이미 존재하는 VCL 클래스로부터 상속 받아서 오브젝트 파스칼 클래스를 만드는 것이기 때문에 캡슐화나 상속성, 다형성, 재사용성 등의 객체지향의 중심이 되는 이론을 잘 알고 있어야 하며 유연하고 확장이 용이하도록 만들어야 한다.

콤포넌트 개발 시 RAD를 포기해야 하는 불편함을 없애고자 개발된 도구들이 있다. 대표적으로 Eagle Software사의 CDK, David Price의 Component Create, ToolsFactory사의 Class Explorer등이 있다.


1.2 콤포넌트 개발자가 알아야 할 OOP

앞에서도 언급했지만 콤포넌트를 개발한다는 것은 클래스를 만드는 것이다. 델파이는 예전 볼랜드 파스칼 버전7부터 도입한 객체 지향적인 특성을 더욱 향상시키고 확장해서 만들어진 오브젝트 파스칼이라는 언어를 사용한다. 오브젝트 파스칼의 객체 모델은 여러 C++ 컴파일러의 객체 모델과 유사하다. 하지만 어떤 면에서는 C++ 보다 더욱 강력한 객체 모델을 가지고 있다. 예를 들어 클래스의 성질에 대한 형식을 선언하는 프로퍼티 개념은 어떤 C++ 컴파일러에서도 찾아 볼 수 없는 기능이다. 이번 절에서는 오브젝트 파스칼의 객체 지향적인 특성과 콤포넌트 개발에 필요한 여러가지 개념에 대해 알아보겠다.
1.2.1 정보 은닉(Visibility)
객체 지향 프로그래밍의 중요 개념 중의 하나가 정보 은닉이다. 혼자서 어플리케이션을 개발한다면 클래스를 만든 사람이면서 동시에 사용하는 사람도 된다. 하지만 팀 단위 개발 상황에서는 내가 만든 클래스를 다른 개발자가 사용할 수도 있다. 이럴 경우에 클래스를 사용하는 개발자에게 클래스를 구현한 내부 원리나 데이터를 굳이 알려 줄 필요는 없다. 단지 사용하는 방법만 제공하면 된다. 오브젝트 파스칼에서는 정보 은닉의 깊이를 표현하는 Private, Protected, Public, Published 네 가지 접근 지시자를 가지고 있다. 정보 은닉의 깊이는 그 클래스가 어떻게 사용될지를 결정한다. 다음 예제 코드를 살펴 보자. 아래 예제 코드는 델파이 소스 디렉토리에 있는 classes.pas와 ComCtrls.pas의 일부분을 발췌한 것이다.


리스트 1.1 classes.pas
unit Classes;

type
TCollection = class
TCollectionItem = class(TPersistent)
  private
    FCollection: TCollection;
    FID: Integer;
    function GetIndex: Integer;
    procedure SetCollection(Value: TCollection);
  protected
    procedure Changed(AllItems: Boolean);
    function GetOwner: TPersistent; override;
    function GetDisplayName: string; virtual;
    procedure SetIndex(Value: Integer); virtual;
    procedure SetDisplayName(const Value: string); virtual;
  public
    constructor Create(Collection: TCollection); virtual;
    destructor Destroy; override;
    function GetNamePath: string; override;
    property Collection: TCollection read FCollection write SetCollection;
    property ID: Integer read FID;
    property Index: Integer read GetIndex write SetIndex;
    property DisplayName: string read GetDisplayName write SetDisplayName;
  end;
 
TCollectionItemClass = class of TCollectionItem;

TCollection = class(TPersistent)
  private
    FItemClass: TCollectionItemClass;
    FItems: TList;
    FUpdateCount: Integer;
    FNextID: Integer;
    FPropName: string;
    function GetCount: Integer;
    function GetPropName: string;   
    procedure InsertItem(Item: TCollectionItem);
    procedure RemoveItem(Item: TCollectionItem);
  protected
    property NextID: Integer read FNextID;
    { Design-time editor support }
    function GetAttrCount: Integer; dynamic;
    function GetAttr(Index: Integer): string; dynamic;
    function GetItemAttr(Index, ItemIndex: Integer): string; dynamic;
    procedure Changed;
    function GetItem(Index: Integer): TCollectionItem;
    procedure SetItem(Index: Integer; Value: TCollectionItem);
    procedure SetItemName(Item: TCollectionItem); virtual;
    procedure Update(Item: TCollectionItem); virtual;
    property PropName: string read GetPropName write FPropName;
    property UpdateCount: Integer read FUpdateCount;
  public
    constructor Create(ItemClass: TCollectionItemClass);
    destructor Destroy; override;
    function Add: TCollectionItem;
    procedure Assign(Source: TPersistent); override;
    procedure BeginUpdate; virtual;
    procedure Clear;
    procedure Delete(Index: Integer);
    procedure EndUpdate; virtual;
    function FindItemID(ID: Integer): TCollectionItem;
    function GetNamePath: string; override;
    function Insert(Index: Integer): TCollectionItem;
    property Count: Integer read GetCount;
    property ItemClass: TCollectionItemClass read FItemClass;
    property Items[Index: Integer]: TCollectionItem read GetItem write SetItem;
  end;


리스트 1.2 comctrls.pas
unit ComCtrls;

type
TStatusPanel = class(TCollectionItem)
  private
    FText: string;
    FWidth: Integer;
    FAlignment: TAlignment;
    FBevel: TStatusPanelBevel;
    FBiDiMode: TBiDiMode;
    FParentBiDiMode: Boolean;
    FStyle: TStatusPanelStyle;
    FUpdateNeeded: Boolean;
    procedure SetAlignment(Value: TAlignment);
    procedure SetBevel(Value: TStatusPanelBevel);
    procedure SetBiDiMode(Value: TBiDiMode);
    procedure SetParentBiDiMode(Value: Boolean);
    procedure SetStyle(Value: TStatusPanelStyle);
    procedure SetText(const Value: string);
    procedure SetWidth(Value: Integer);
    function IsBiDiModeStored: Boolean;
  protected
    function GetDisplayName: string; override;
  public
    constructor Create(Collection: TCollection); override;
    procedure Assign(Source: TPersistent); override;
    procedure ParentBiDiModeChanged;
    function UseRightToLeftAlignment: Boolean;
    function UseRightToLeftReading: Boolean;
  published
    property Alignment: TAlignment read FAlignment write SetAlignment default taLeftJustify;
    property Bevel: TStatusPanelBevel read FBevel write SetBevel default pbLowered;
    property BiDiMode: TBiDiMode read FBiDiMode write SetBiDiMode stored IsBiDiModeStored;
    property ParentBiDiMode: Boolean read FParentBiDiMode write SetParentBiDiMode default True;
    property Style: TStatusPanelStyle read FStyle write SetStyle default psText;
    property Text: string read FText write SetText;
    property Width: Integer read FWidth write SetWidth;
  end;


제한이 없으며 어디에서든 클래스의 멤버를 사용할 수 있도록 해주는 지시자가 Public이다. Public 영역에 선언된 멤버는 클래스가 선언된 유닛에서는 물론이고 프로그램의 다른 유닛에서도 참조할 수 있다. 예를 들어 리스트 6.1에서 TCollection 클래스의 GetNamePath 메소드는 리스트 6.2의 ComCtrls.pas에서도 사용할 수 있다. Puiblic 영역은 실행 시에 모든 코드들이 이용할 수 있기 때문에 실행 시 클래스의 인터페이스를 나타낸다.
가장 제한적인 지시자는 Private이며 Private 멤버는 클래스를 선언한 유닛에서만 사용할 수 있다. TCollectionItem의 FID라는 멤버 변수는 classes.pas 유닛 내에서만 접근할 수 있고 TStatusPanel이 선언된 ComCtrls.pas에서는 접근할 수 없다. 하지만 TCollection 클래스에서는 이 변수를 참조할 수 있다. 즉 같은 유닛 내에서는 비록 Private로 선언되어 있다 할지라도 Public과 같은 접근 권한을 가진다.(이런 특징은 다른 객체 지향 언어와 약간 다르다고 할 수 있다.)
Protected 멤버에 대한 접근 권한은 Private와 Public의 사이에 있다. 같은 유닛 내에서는 Private, Public과 같은 접근 권한을 가지지만 다른 유닛에서는 해당 클래스로부터 상속 받은 클래스만이 접근할 수 있다. 예를 들어 TCollectionItem의 GetOwner, GetDisplayName, SetIndex, SetDisplayName 같은 멤버 함수는 TStatusPanel과 같이 TCollectionItem에서 상속 받은 클래스만이 사용할 수 있다.
마지막으로 Published 지시자는 Public과 같은 접근 권한을 가지지만 좀 더 특별한 용도를 가지고 있으며 콤포넌트를 개발할 때 프로퍼티를 선언하기 위해 사용한다. Public은 실행 시에 클래스에 관한 정보를 얻을 수 있지만 Published 멤버는 설계 시 클래스 정보를 나타낸다. 또한 Published에 선언된 프로퍼티는 델파이 폼(*.dfm) 파일로 저장될 수 있다.
클래스 형 선언 바로 아래에 접근 지시자 없이 멤버 변수를 선언하면 이 멤버 변수는 기본적으로 Public 권한을 가진다. 하지만 {$M+} 컴파일러 지시자를 먼저 선언하면 Published 권한을 가지게 된다. 아래 예제 코드를 보면


type
  TExample1 = class
    Field1 : integer;  // Public
  public
    .....
  end;

{$M+}
  TExample2 = class
    Field2 : integer;  // Published
  public
    ....
  end;
{$M-}

  TExample3 = class
    Field3 : integer;   // Public
  public
    ....
  end;


Field1과 Field3는 Public 멤버이지만 Field2는 Published 멤버로 선언된다.


가급적이면 혼란을 막기 위해 접근 지시자를 써 주는 게 좋다.


1.2.2. 생성자(Constructor)
오브젝트 파스칼에서 클래스는 생성자라고 하는 특별한 메소드를 가진다. 생성자는 클래스의 인스턴스를 생성하기 위해서 사용되며 클래스를 위한 메모리를 힙에서 할당하고 모든 멤버 변수를 0으로 초기화하고 기타 초기화 작업을 수행한다.
아래 예제 코드에서 TSalary는 Create라는 생성자와 CreateWidthAmount라는 생성자를 선언했다. 생성자의 이름은 꼭 Create가 아니어도 상관 없지만 반드시 Constructor라는 키워드로 선언되어야 한다. Constructor 키워드가 다른 메소드와 생성자를 구별하는 유일한 방법임을 명심해야 한다. 생성자를 선언해 주지 않으면 클래스의 인스턴스를 만들 때 부모 클래스의 생성자를 사용한다. 델파이에 있는 모든 클래스의 조상인 TObject는 기본 생성자인 Create와 소멸자 Destroy를 정의해 놓았다. 생성자는 특별한 형태의 메소드이기 때문에 반환 값을 가질 수 없다. 그리고 클래스는 아래 예제 코드처럼 여러 개의 생성자를 가질 수 있다. 생성자의 이름을 다르게 선언하거나 델파이 4에서 처음 도입된 메소드 오버로딩을 통해서 여러 개의 생성자를 선언할 수 있다. 델파이에서는 보통 관습적으로 여러 개의 생성자를 선언할 때 다른 이름을 가지도록 하지만 볼랜드의 또 다른 RAD 개발 툴인 C++ Builder와의 호환성을 생각한다면 메소드 오버로딩을 통해서 구현하는 것이 좋을 것이다. 왜냐하면 C++에서는 생성자가 특별한 이름을 가질 수 없기 때문이다.
1.2.3. 소멸자(Destructor)
소멸자는 생성자의 반대 개념이다. 즉 클래스의 인스턴스가 소멸될 때 자동으로 호출된다. 소멸자는 인스턴스를 위해 할당된 메모리를 해제하고 기타 최종 작업(동적으로 할당된 메모리의 해제 등…)을 수행한다. 소멸자도 생성자와 마찬가지로 반환값을 가질 수 없으며 별도로 선언해 주지 않으면 부모 클래스의 소멸자를 사용하게 된다. 생성자와 달리 소멸자는 Destroy라는 이름을 가져야 한다. 왜냐하면 보통 클래스의 인스턴스를 메모리에서 제거할 때는 Free 메소드를 호출하는데 Free 메소드는 인스턴스의 메모리를 해제하기 전에 TObject 클래스의 Destroy 메소드를 호출하도록 되어 있다. 예제 코드에서 보이지는 않지만 소멸자는 Free 메소드에서 클래스의 인스턴스가 실제로 생성되었는지 검사하고 Destroy를 호출해 준다. 직접 Destroy 메소드를 호출하는 것보다 Free 메소드를 호출하는게 더욱 안전하다. 리스트 6.3에서 생성자와 소멸자가 실제로 어떻게 쓰이는지 볼 수 있을 것이다.


리스트 1.3 생성자와 소멸자
program Salary;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TSalary = class
  public
    Amount : integer;
    constructor Create;
    constructor CreateWithAmount(AAmount : integer);
  end;

constructor TSalary.Create;
begin
  inherited Create;
  // 기본적으로 모든 변수는 0으로 초기화 되기 때문에 이 루틴은 굳이 써 주지 않아도 된다.
  Amount := 0;
end;

constructor TSalary.CreateWithAmount(AAmount : integer);
begin
  inherited Create;
  Amount := AAmount;
end;

var
  Temp1, Temp2 : TSalary;
  TempStr : string;
begin
  Temp1 := TSalary.Create;
  Temp2 := TSalary.CreateWithAmount(1000000);
  try
    writeln('Temp1의 급여는 ', IntToStr(Temp1.Amount));
    writeln('Temp2의 급여는 ', IntToStr(Temp2.Amount));
  finally
    Temp1.Free;
    Temp2.Free;
  end;
  WriteLn('종료하려면 ENTER 키를 누르세요!!');
  ReadLn(TempStr);
end. 




콤포넌트를 개발할 때 생성자와 소멸자는 주의해서 사용해야 한다. 콤포넌트의 생성과 소멸을 잘못하면 델파이가 종료되거나 윈도 프로그래머가 가장 싫어하는 블루 스크린이나 Access Violation등의 에러를 수도 없이 만날 수 있음을 명심해야 한다.


1.2.4. 선행 클래스 선언
종종 두 개의 클래스를 선언할 때 서로가 다른 클래스를 참조하게 되는 경우가 발생한다. 이런 경우 C++이나 예전 볼랜드 파스칼에서는 클래스의 포인터를 먼저 선언하고 클래스를 선언할 때 위에서 선언한 포인터로 멤버 변수를 정의해서 사용한다. 하지만 델파이에서는 이와 달리 클래스의 이름만 먼저 정의해 주면 다른 클래스에서 이 클래스에 대해 참조할 수 있도록 선행 클래스 선언이라는 개념을 도입했다. 리스트 6.1을 다시 보면 TCollection이 클래스라는 것만 먼저 선언하고 TCollectionItem에서 아직 모두 선언되지 않은 TCollection에 대한 참조를 하고 있음을 알 수 있다. TCollectionItem도 마찬가지로 TCollection을 참조하고 있다. 두 클래스 모두 서로를 참조하기 때문에 둘 중 TCollection을 먼저 선행 선언하고 TCollectionItem은 순서대로 선언을 해 놓았다.
1.2.5. 메소드 바인딩
OOP의 특성 중 다형성(Polymorphism)과 상속성(Inheritance)를 이용하면 객체의 동작(메소드)를 일반화 한 추상 클래스를 만들고 이 클래스를 부모로 하는 파생 클래스에서 객체의 동작을 실제로 정의할 수 있다. 즉 기능적으로는 같은 동작을 하지만 그 동작을 수행하는 실제 방법은 다르게 구현할 수 있다는 것이다. 예를 들어 자동차의 핸들을 생각해 보자. 자동차의 핸들은 자동차의 진행 방향을 변경하는 기능을 가진다. 하지만 파워 핸들과 수동 핸들은 같은 기능을 하지만 다른 방법으로 그 기능을 수행한다. 운전자는 핸들이 내부적으로 어떻게 동작하는 지를 몰라도 핸들을 돌리면 자동차의 진행 방향이 변경된다는 것만 알고 있으면 된다.
다형성을 얘기할 때 빼놓을 수 없는 것이 바로 객체의 구현 방법이 결정되는 시기이다. 컴파일할 때 결정되는 것을 정적 바인딩(Static Binding, Early Binding)이라 하고 실행 시에 결정되는 것을 동적 바인딩(Dynamic Binding, Late Binding)이라 한다. 정적 바인딩은 컴파일시 모든 것이 결정되기 때문에 최적화할 수 있으며 수행 속도가 빠르다. 동적 바인딩은 객체의 유연성이나 추상화 등의 장점이 있지만 실행 속도가 다소 느려진다는 단점이 있다.
1.2.6. 정적 메소드(Static Methods)
클래스의 모든 메소드는 특별하게 지정하지 않는 한 정적 메소드로 선언된다. 컴파일러는 컴파일 시에 메소드의 실제 어드레스를 결정하고 직접 메소드를 연결해 주기 때문에 빠르게 동작한다. 부모 클래스에서 선언된 정적 메소드와 파생된 자손 클래스에서 사용하는 그 메소드는 부모 클래스와 실제로 똑 같은 메모리 주소를 가지게 되며 하나의 메소드를 부모 클래스와 공유하게 된다. 만약에 자손 클래스에서 부모 클래스의 정적 메소드와 같은 이름을 가지는 메소드를 선언하게 되면 부모 클래스의 정적 메소드는 자손 클래스에서 새로운 메소드로 대체된다.
1.2.7. 가상 메소드(Virtual Methods)
가상 메소드는 정적 메소드에 비해 다소 복잡하지만 확장성이 좋은 장점을 가진다. 가상 메소드는 자손 클래스에서 재정의될 수 있으며 메소드의 주소는 컴파일할 때 결정되지 않고 동적으로 바인딩되기 때문에 실행할 때 결정된다. 델파이는 두가지 가상 메소드를 가지고 있다. 가상 메소드를 선언하려면 메소드 선언문 뒤에 virtual 지시자나 dynamic 지시자를 추가해 주면 된다. virtual 지시자와 dynamic 지시자는 메소드 테이블을 관리하는 방법이 다르다. virtual 지시자는 객체의 모든 가상 메소드의 주소를 보존하는 가상 메소드 테이블(Virtual Method Table, VMT)에 새로운 엔트리를 하나 생성한다. 자손 클래스는 부모 클래스의 모든 가상 메소드 주소와 자기 자신의 가상 메소드에 대한 주소를 같이 보존하는 자신만의 가상 메소드 테이블을 가진다. dynamic 지시자는 가상 메소드 테이블과 별도로 동적 메소드 테이블(Dynamic Method Table, DMT)에 객체에 새롭게 추가되거나 재정의된 가상 메소드에 대한 주소만 관리한다. virtual 메소드는 객체가 차지하는 메모리가 dynamic 메소드에 비해 크지만 속도가 빠르며 dynamic 메소드는 효율적으로 메모리를 관리할 수 있지만 주소를 찾기 위해 클래스 계층도를 거슬러 올라 가면서 찾아야 하기 때문에 성능이 떨어진다. 메소드가 자주 호출되거나 높은 성능을 요구한다면 virtual 메소드를 사용하는 것이 효과적일 것이다. 부모 클래스의 가상 메소드를 재정의(Redefine)하려면 override 지시자를 사용한다. 아래 예제 코드를 살펴 보자. 예제에서처럼 override 지시자는 가상 메소드가 virtual 이든 dynamic 이든 관계없이 사용한다. BrokenProc의 경우에는 자손 클래스에서 다시 virtual로 선언했기 때문에 아래 그림과 같이 부모 클래스의 BrokenProc만 호출된다.

리스트 1.4 가상 메소드
program VMethods;

{$APPTYPE CONSOLE}

uses
  classes;

type

  TParent = class
    procedure VirtualProc; virtual;
    procedure DynamicProc; dynamic;
    procedure BrokenProc; virtual;
  end;

  TDescendant = class(TParent)
    procedure VirtualProc; override;
    procedure DynamicProc; override;
    procedure BrokenProc; virtual;
  end;

  procedure TParent.VirtualProc;
  begin
    writeln('TParent.VirtualProc');
  end;

  procedure TParent.DynamicProc;
  begin
    writeln('TParent.DynamicProc');
  end;

  procedure TParent.BrokenProc;
  begin
    writeln('TParent.BrokenProc');
  end;

  procedure TDescendant.VirtualProc;
  begin
    inherited VirtualProc;
    writeln('TDescendant.VirtualProc');
  end;

  procedure TDescendant.DynamicProc;
  begin
    inherited DynamicProc;
    writeln('TDescendant.DynamicProc');
  end;

  procedure TDescendant.BrokenProc;
  begin
    inherited BrokenProc;
    writeln('TDescendant.BrokenProc');
  end;

  procedure TestVirtProc(Obj : TParent);
  begin
    Obj.VirtualProc;
  end;

  procedure TestDynProc(Obj : TParent);
  begin
    Obj.DynamicProc;
  end;

  procedure TestBrokenProc(Obj : TParent);
  begin
    Obj.BrokenProc;
  end;
var
  TempObj : TDescendant;
  TempStr : string;
begin
  TempObj := TDescendant.Create;
  try
    writeln('Virtual 메소드');
    TestVirtProc(TempObj);
    writeln('Dynamic 메소드');
    TestDynProc(TempObj);
    writeln('메소드 연결이 끊어진다');
    TestBrokenProc(TempObj);
  finally
    TempObj.Free;
  end;
  WriteLn('종료하려면 ENTER 키를 누르세요!!');
  ReadLn(TempStr);
end.




그림 1-1 가상 메소드 예제 실행 화면


1.2.8. 추상 메소드(Abstract Methods)
클래스의 메소드를 선언할 때 virtual, dynamic 지시자와 함께 abstract 지시자를 사용하면(추상 메소드, C++에서는 순수 가상 함수라 한다) 그 클래스는 추상 클래스가 된다. 추상 클래스는 말 그대로 객체를 정의하는 것이 아니고 개념만 정의하는 것이다. 사각형, 삼각형, 원 등의 공통점은 도형이라는 개념이라고 볼 수 있다. 그래서 이 도형이라는 개념을 클래스로 만들고 사각형, 삼각형, 원 등은 도형 클래스의 자손 클래스로 선언하게 된다. 추상 클래스로부터 직접 객체를 생성할 수는 없다. 반드시 부모 클래스의 추상 메소드를 재정의해서 구현해 놓은 자손 클래스를 사용해야 한다. 추상 클래스로부터 객체를 생성하려 하면 델파이 컴파일러는 "Constructing instance of 'Class Name' containing abstract methods" 라는 경고 메시지를 출력하며 경고를 무시하고 실행하면 예외 상황을 발생시키게 된다. 초보 델파이 개발자들이 가끔 이 추상 클래스 때문에 고생하게 되는 경우가 있다. 예를 들어 TStringList를 사용해서 객체를 생성해야 하는데 추상 클래스인 TStrings로부터 객체를 생성하려다 위와 같은 상황을 경험하게 된다.
1.2.9. 클래스 참조 형(Class Reference Type)과 가상 생성자
오브젝트 파스칼에서는 메소드와 마찬가지로 생성자도 가상으로 만들 수 있다. 클래스 간의 상속 관계에 따라 적절한 생성자가 호출되도록 할 수 있다는 것이다. 하지만 가상 생성자 자체만으로는 특별하게 사용할 일이 없지만 클래스 참조 형(메타 클래스라고도 한다)과 함께 사용하면 아주 강력한 기능을 제공해 준다. 아래 예제 화면을 보자. 화면에서 라디오 버튼으로 생성할 콤포넌트를 선택하고 생성 버튼을 누르면 선택한 콤포넌트들이 동적으로 생성된다. 소스를 보면 클래스 참조 형이 얼마나 강력한 기능인지 실감하게 될 것이다.

그림 1-2 클래스 참조 형 예제


예제에서 사용한 콤포넌트들은 모두 TControl 클래스의 자손 클래스이기 때문에 원하는 콤포넌트를 클래스 참조 형을 사용해서 동적으로 생성하는 것이 가능하다. TControlClass는 controls.pas에 정의되어 있다. 클래스 참조 형은 class of 키워드로 정의한다.

리스트 1.5 클래스 참조 형 예제
unit fRefExam;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls;

type
  TfrmRefExam = class(TForm)
    rdoButton: TRadioButton;
    rdoEdit: TRadioButton;
    rdoCheckBox: TRadioButton;
    btnCreate: TButton;
    Bevel1: TBevel;
    panMsg: TPanel;
    procedure btnCreateClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmRefExam: TfrmRefExam;

implementation

{$R *.DFM}

// TControlClass = class of TControl
// TControlClass는 Controls.pas에 위와 같이 정의되어 있다.

procedure TfrmRefExam.btnCreateClick(Sender: TObject);
var
  Reference : TControlClass;
  Instance : TControl;
begin
  if rdoButton.Checked then
    Reference := TControlClass(TButton)
  else if rdoEdit.Checked then
    Reference := TControlClass(TEdit)
  else
    Reference := TControlClass(TCheckBox);

  Instance := Reference.Create(Self);
  Instance.Parent := Self;
  Instance.Left := 8;
  Instance.Top := 88;
  panMsg.Caption := Instance.ClassName + ' 생성';
end;

end.


보시다시피 TControlClass는 TControl 클래스의 참조 형으로 선언되어 있다. 따라서 TControl의 자손 클래스라면 어떤 클래스도 Reference 변수에 대입될 수 있다. 예제에서 가장 중요한 것은 콤포넌트를 생성하는 Reference.Create(Self) 행이다. 각 콤포넌트의 생성자 Create가 선택된 콤포넌트에 따라서 적절하게 호출된다는 것을 눈여겨 보아야 한다.
1.2.10. 클래스 메소드(Class Methods)
클래스 메소드는 클래스 참조를 통해서 실행될 수 있다는 것만 제외하고는 일반 메소드와 같다. 클래스 메소드 내에서는 클래스의 멤버 변수나 다른 일반 메소드를 사용할 수 없지만 클래스의 생성자나 다른 클래스 메소드는 참조할 수 있다. 결과적으로 클래스 메소드는 전역 데이터를 조작하거나 클래스에 대한 정보를 반환하는 메소드로 사용할 수 있다. 일반 메소드는 클래스를 인스턴스화 해야 호출할 수 있지만 클래스 메소드는 인스턴스를 만들지 않아도 사용할 수 있다. 아래 예제는 클래스 메소드를 가지는 간단한 클래스이다. 예제를 보면 클래스의 인스턴스를 만들지 않고 클래스 메소드를 사용하는 것을 볼 수 있다.

리스트 1.6 클래스 메소드 예제
program ClsMethod;
{$APPTYPE CONSOLE}
uses
  SysUtils;

type
  TExample = class
    class function GetClassName : string;
  end;

  class function TExample.GetClassName : string;
  begin
    Result := '클래스 메소드 예제 클래스';
  end;
var
  TmpObj : TExample;
  TmpStr : string;
begin
  // Insert user code here
  WriteLn('직접 호출 -> ', TExample.GetClassName);
  TmpObj := TExample.Create;
  try
    WriteLn('객체 생성 후 호출 -> ', TmpObj.GetClassName);
  finally
    TmpObj.Free;
  end;
  WriteLn('종료하려면 ENTER 키를 누르세요!!');
  ReadLn(TmpStr);
end.




그림 1-2 클래스 참조 형 예제


1.2.11. RTTI(Run-Time Type Information)
RTTI는 실행 시에 클래스에 대한 다양한 정보를 얻을 수 있는 방법을 제공한다. 클래스의 다형성과 상속 관계를 다루다 보면 객체 포인터가 가리키고 있는 클래스의 형이 어떤 것인지 알아야 할 때가 종종 있다. 델파이는 RTTI라는 것을 통해서 이러한 작업을 가능하게 해주는데 구체적으로 객체 포인터가 가리키고 있는 클래스의 형이 특정 클래스 또는 그 자손 클래스인지를 결정할 수 있게 해 주는 is 연산자와 객체 포인터가 가리키고 있는 클래스의 형을 특정 자손 클래스로 안전하게 형 변환(Type Casting)해 주는 as 연산자를 제공한다.
is 연산자는 객체 포인터와 클래스 형을 인자로 가지는 논리 연산자이다. 객체 포인터가 가리키고 있는 클래스가 지정한 클래스 형이거나 그 클래스의 자손 클래스이면 참을 반환하고 아니면 거짓을 반환한다. 아래 예제를 살펴 보자.
if ActiveControl is TCustomEdit then TCustomEdit(ActiveControl).CopyToClipboard;
ActiveControl의 클래스 형이 TCustomEdit이거나 그 자손(TEdit, TMaskEdit,TMemo)인지 검사해서 참이면 ActiveControl을 TCustomEdit로 형 변환을 하고 TCustomEdit의 CopyToClipboard 메소드를 호출한다.
위 코드에서는 TCustomEdit(ActiveControl)처럼 강제로 형 변환을 했는데 이를 좀 더 안전하게 형 변환하려면 as 연산자를 사용한다. as 연산자는 객체 포인터와 클래스 형을 인자로 가지며 객체 포인터가 가리키고 있는 클래스의 형을 지정 클래스 형으로 변환한 객체를 반환해 준다. 위 코드를 as 연산자로 사용하면
if ActiveControl is TCustomEdit then (ActiveControl as TCustomEdit).CopyToClipboard;
처럼 코딩할 수 있다.


저작권에 대한 공지
이 문서에 포함된 모든 글과 소스 코드는 작자인 저 김성동의 지적 재산이지만 무단으로 배포하거나 책이나 강의 교재등을 위한 복제 등 저작권을 저해하는 행위를 제외하고는 자유롭게 사용할 수 있습니다.
또한 이 페이지에 대한 링크는 자유롭게 할 수 있으나 전문을 전제하는 일은 허용하지 않습니다.