메서드 호출

메서드 호출 (Method invocation)

메서드는 일반 프로시저처럼 호출돼요. 다만 앞에 객체 인스턴스 식별자가 붙을 뿐이죠. 어떤 메서드가 호출될지 알아내려면 메서드의 타입을 알아야 해요. 각 타입을 하나씩 살펴볼게요.

출처: Method invocation

본문

일반 정적 메서드

일반(정적) 메서드는 abstractvirtual 키워드 없이 선언된 메서드예요. 정적 메서드를 호출하면 선언된(즉 컴파일 타임의) 객체 메서드가 사용돼요. 예를 들어 다음 선언을 보죠.

Type
  TParent = Object
    ...
    procedure Doit;
    ...
    end;
  PParent = ^TParent;
  TChild = Object(TParent)
    ...
    procedure Doit;
    ...
    end;
  PChild = ^TChild;

보시다시피 부모와 자식 객체 모두 Doit이라는 메서드를 가져요. 이제 다음 선언과 호출을 보죠.

Var
  ParentA,ParentB : PParent;
  Child           : PChild;

begin
   ParentA := New(PParent,Init);
   ParentB := New(PChild,Init);
   Child := New(PChild,Init);
   ParentA^.Doit;
   ParentB^.Doit;
   Child^.Doit;

세 번의 Doit 호출 중 마지막만 TChild.Doit을 호출하고, 나머지 두 호출은 TParent.Doit을 호출해요. 정적 메서드는 컴파일러가 컴파일 타임에 어느 메서드를 호출할지 결정하기 때문이죠. ParentB는 TParent 타입이라, TChild로 생성될 예정이어도 컴파일러는 TParent.Doit으로 불러야 한다고 결정해요. 실제로 호출되는 메서드가 런타임 객체의 실제 타입에 의존해야 하는 경우가 있을 수 있어요. 그렇다면 그 메서드는 정적 메서드가 될 수 없고 virtual 메서드여야 해요.

Virtual 메서드

앞 절의 상황을 해결하기 위해 virtual 메서드가 생겼어요. 간단히 메서드 선언에 virtual 한정자를 붙이면 돼요. 자손 객체는 같은 파라미터 목록으로 메서드를 재선언해(virtual 키워드를 붙여) 새 구현으로 오버라이드할 수 있어요.

앞 예제로 돌아가서, 다음 대안 선언을 보죠.

Type
  TParent = Object
    ...
    procedure Doit;virtual;
    ...
    end;
  PParent = ^TParent;
  TChild = Object(TParent)
    ...
    procedure Doit;virtual;
    ...
    end;
  PChild = ^TChild;

보시다시피 부모와 자식 객체 모두 Doit 메서드를 가져요. 이제 다음 선언과 호출을 보죠.

Var
  ParentA,ParentB : PParent;
  Child           : PChild;

begin
   ParentA := New(PParent,Init);
   ParentB := New(PChild,Init);
   Child := New(PChild,Init);
   ParentA^.Doit;
   ParentB^.Doit;
   Child^.Doit;

이제 객체의 실제 런타임 타입에 따라 다른 메서드가 호출돼요. ParentA는 TParent 인스턴스로 생성되므로 달라지지 않아요. Child도 TChild 인스턴스로 생성되므로 상황이 달라지지 않죠.

하지만 ParentB는 상황이 달라져요. TParent로 선언됐지만 TChild 인스턴스로 생성되거든요. 이제 프로그램이 실행될 때 Doit을 호출하기 전에 ParentB의 실제 타입을 확인하고, 그때 어떤 메서드를 호출할지 결정해요. ParentB가 TChild 타입이므로 TChild.Doit이 호출돼요. 객체의 실제 타입을 런타임에 검사하는 이 코드는 컴파일러가 컴파일 타임에 삽입해요.

TChild.DoitTParent.Doit을 오버라이드한다고 말해요. inherited 키워드로 TChild.Doit 안에서 TParent.Doit에 접근할 수 있어요.

Procedure TChild.Doit;
begin
  inherited Doit;
  ...
end;

위 예제에서 TChild.Doit이 호출되면, 가장 먼저 하는 일은 TParent.Doit을 호출하는 거예요. inherited 키워드는 정적 메서드에는 쓸 수 없고 virtual 메서드에만 쓸 수 있어요.

이게 가능하려면 컴파일러는 객체 타입마다 virtual 메서드 테이블, 즉 VMT(Virtual Method Table)을 유지해요. 이건 단순히 각 virtual 메서드를 가리키는 포인터들의 테이블이에요. 각 virtual 메서드는 이 테이블에서 고정된 위치(인덱스)를 가져요. 컴파일러는 이 테이블로 실제로 사용할 메서드를 조회해요. 자손 객체가 메서드를 오버라이드하면 부모 메서드 항목이 VMT에서 덮어써져요. VMT에 대한 자세한 내용은 프로그래머 가이드에 있어요.

앞서 말했듯 VMT를 가진 객체는 생성자로 초기화해야 해요. 객체 변수는 그 객체가 생성된 실제 타입의 VMT 포인터로 초기화돼야 하죠.

Abstract 메서드

abstract 메서드는 virtual 메서드의 특별한 종류예요. abstract로 선언된 메서드는 구현이 없어요. 이 메서드를 오버라이드하고 구현하는 것은 상속받는 객체의 몫이에요.

이로부터 두 가지가 따라나와요. 첫째, virtual이 아닌 메서드는 abstract일 수 없어요. (문법 다이어그램에서 확인할 수 있어요.) 둘째, abstract 메서드를 가진 객체의 인스턴스는 직접 생성할 수 없어요.

이유는 명확해요. 컴파일러가 점프할 메서드가 없기 때문이죠! abstract로 선언된 메서드는 구현이 없어서, 상속받는 객체가 오버라이드해 구현해야 해요. 예제를 계속 보죠.

Type
  TParent = Object
    ...
    procedure Doit;virtual;abstract;
    ...
    end;
  PParent=^TParent;
  TChild = Object(TParent)
    ...
    procedure Doit;virtual;
    ...
    end;
  PChild = ^TChild;

보시다시피 부모와 자식 객체 모두 Doit 메서드를 가져요. 이제 다음 선언과 호출을 보죠.

Var
  ParentA,ParentB : PParent;
  Child           : PChild;

begin
   ParentA := New(PParent,Init);
   ParentB := New(PChild,Init);
   Child := New(PChild,Init);
   ParentA^.Doit;
   ParentB^.Doit;
   Child^.Doit;

먼저, 3번째 줄(Line 3)은 컴파일 에러를 만들어요. abstract 메서드를 가진 객체의 인스턴스는 생성할 수 없다는 에러죠. 컴파일러가 PParent가 abstract 메서드를 가진 객체를 가리킨다는 것을 감지했거든요. 3번째 줄을 주석 처리하면 프로그램은 컴파일할 수 있어요.

Remark: abstract 메서드가 오버라이드되면 부모 메서드가 없으므로 inherited로 부모 메서드를 호출할 수 없어요. 컴파일러가 이를 감지하고 이렇게 경고해요.

testo.pp(32,3) Error: Abstract methods can't be called directly

어떤 메커니즘으로든 abstract 메서드가 런타임에 호출되면 런타임 에러가 발생해요. (정확히는 런타임 에러 211이에요.)

클래스 또는 정적 메서드

클래스 메서드 또는 static 지시어로 선언된 메서드는 객체 타입에 전역인 메서드예요. 호출되면 암묵적 "self" 포인터를 쓸 수 없어요. 즉 일반 메서드는 호출할 수 없고, 객체의 필드에도 접근할 수 없어요. 다만 클래스 변수는 사용할 수 있어요.

클래스 또는 정적 메서드는 일반 메서드라서 프로시저 변수에 할당할 수 있어요. 다음 프로그램이 이 모든 것을 보여줘요. 주석 처리된 문장들은 컴파일되지 않아요.

{$APPTYPE CONSOLE}
{$IFDEF FPC}{$MODE DELPHI}{$H+}{$ENDIF}
type
  TTest = object
    const Epsylon = 100;
    var f : integer;
    class var cv1,cv2:integer;
    procedure myproc;
    class procedure testproc;
    class procedure testproc2;static;
    procedure testproc3; static;
  end;

  procedure TTest.myproc;
  begin
    cv1:=0;
    f:=1;
  end;

  class procedure TTest.Testproc;
  begin
    cv1:=1;
    // f:=1;
  end;

  class procedure TTest.Testproc2;
  begin
    cv1:=2;
    // f:=1;
  end;

  procedure TTest.Testproc3;
  begin
    cv1:=3;
    // f:=1;
  end;


Var
  P : Procedure;

begin
  P:[email protected];
  P:[email protected];
  P:[email protected];
  P:[email protected];
end.

주석 처리된 문장 중 하나를 주석 해제하고 컴파일하면 컴파일 에러가 발생해요.

ocv.pp(32,6) Error: Only class methods, class properties and
  class variables can be accessed in class methods

더 알아보기

정적 메서드는 컴파일 타임에 결정되고, virtual 메서드는 런타임 타입에 따라 달라져요. abstract 메서드는 구현이 없어 오버라이드가 필수라는 점도 기억하세요.