델파이의 인터페이스, 알고 쓰자

최근 객체지향 프로그래밍 언어라면 누구나 갖고 있는 인터페이스, 델파이에도 있다.
하지만 다른 언어처럼 ‘이렇게 쓰면 되겠지!’ 하다간 문제가 생길 수도 있다.
잘 모르고 쓰면 위험하지만 잘 쓰면 많은 도움이 되는 델파이의 인터페이스. 이번 글에서 제대로 알아보자.

레퍼런스 카운트가 뭐길래

델파이의 인터페이스는 평범한 인터페이스가 아니라, 레퍼런스 카운트 인터페이스이다. 직접적으로 차이나는 부분은 레퍼런스 카운팅이 된다는 사실이다.
레퍼런스 카운팅의 설명을 읽어보곤 그럼 좋은 거 아닌가? 하실 분들이 있겠지만, 사실은 잘못 쓰면 재앙이 될 수도 있는 부분이다.

예를 들어서 아래와 같은 코드를 생각해보자.

type
  IMyAncestor = interface
    procedure P1;
  end;

  IMyChild = interface(IMyAncestor)
    procedure P2;
  end;

  TMyChild = class(TObject, IMyChild)
    procedure P1;
    procedure P2;
  end;

implementation

{ TMyChild }

procedure TMyChild.P1;
begin
  ShowMessage('P1');
end;

procedure TMyChild.P2;
begin
  ShowMessage('P2');
end;

타 언어에서 넘어온 분들은 당연히 이렇게 쓰면 될 것 같다고 생각하겠지만, 실제로는 아래와 같은 에러가 난다

E2291 Missing implementation of interface method IInterface.QueryInterface
E2291 Missing implementation of interface method IInterface._AddRef
E2291 Missing implementation of interface method IInterface._Release

이 문제는 델파이의 인터페이스가 그냥 인터페이스가 아니라, Win32 인터페이스이기 때문이다.

델파이 프로그래밍 언어를 읽어보면 다음과 같은 부분이 나온다.

Win32 인터페이스는 “가벼운” 인터페이스가 아닙니다. 이것은 모든 타입 파라미터 constraints가 항상 COM IUnknown 메소드들인 _AddRef, _Release, QueryInterface를 지원하거나 TInterfacedObject로부터 상속받는다는 것을 의미합니다.

  • 델파이 프로그래밍 언어, Chapter 7 제네릭(Generics) 中

즉  델파이에서는 사용자에게 인터페이스를 쓰기 위한 전제조건으로, 레퍼런스 카운팅 메소드를 재정의하는 걸 강요하는 것이다. 참고로 인터페이스형 참조에 대해서 Free같은 건 불러지지 않는다.
델파이가 원래 레퍼런스 카운팅을 지원하는 언어였다면 괜찮았겠지만, 기존의 비 레퍼런스 카운팅 방식인 클래스를 그대로 남겨둔 상태에서 이런 동작은 재앙으로 가는 급행열차다.

조부모 위로는 나와 관계 없다는 신개념 상속

델파이에서 TChild가 TParent를 잇고, TParent가 TGrandparent를 이으면
(즉, TChild = class(TInterfacedObject, TParent)이고 TParent = class(TGrandparent)인 경우)
Grandparent: TGrandparent로 정의할 때 Grandparent := TChild.Create 식으로 대입이 가능하다.

그런데 인터페이스에서는 그게 안 된다.
즉, TChild = class(IParent)이고 IParent = interface(IGrandparent)일 때
Grandparent: IGrandparent인 경우 IGrandparent := TChild.Create로 대입을 하면 아래와 같은 에러가 난다.

E2010 Incompatible types: ‘IGrandparent’ and ‘TChild’

점점 핵가족 시대로 진행하는 현대 사회를 반영하는(?) 상속이 아닐 수 없다.

그럼에도 불구하고 쓰고 싶다면

이처럼 델파이의 인터페이스에는 ‘인터페이스’라는 이름의 범위를 초월하는 동작들이 준비되어 있다.
오죽하면 이 글에서는 레퍼런스 카운팅을 무시하라는 조언을 할 정도다.
따라서 델파이에서 인터페이스가 꼭 필요한 상황이라면 다음의 사항을 지키는 것이 좋다.

  1. TInterfacedObject의 적절한 활용
    TInterfacedObject는 해당 3가지 메소드를 델파이에서 구현해 둔 기본 구현같은 존재다.
    하지만 문제가 하나 있다. 이 클래스를 이어버리면 부모 클래스에 들어갈 수 있는 자리가 더 이상 없다.
    따라서 현실적으로는 여기나 Professional 이상 버전의 경우 기본 구현을 참고하여 복사&붙여넣기를 하시는 것이 최선일 듯 하다.
  2. FreeAndNil을 주의하라
    인터페이스형 참조에 대해서 .Free를 부르면 컴파일 에러가 난다.
    그러나 FreeAndNil을 부르면? 컴파일 에러가 아니라 Access violation이 난다!
    따라서 FreeAndNil을 애용하는 경우 인터페이스 도입은 극도로 조심해야 한다.
  3. 인터페이스를 쓰기로 결정했으면 인터페이스로만 지시하라
    닉 하지스의 Coding in delphi, Encodo의 Interfaces in Delphi에서 공통적으로 경고하는 부분이 바로 인터페이스와 클래스 참조를 섞어쓰는 것이다.
    위험할 수 있는 코드는 Interfaces in Delphi의 마지막 코드에 나와있으니 꼭 하나로 통일하여 사용하시기 바란다.그러지 않을 경우 메모리 누수 혹은 이중 해제 등의 문제가 생길 수 있다.
  4. 조부모 이상으로 거슬러가는 참조가 필요할 경우 클래스에 상속 계보를 모두 적어두라
    위의 조부모 참조 문제는 모든 인터페이스를 클래스에 적어두면 해결된다.
    즉 TChild = class(TInterfacedObject, IParent, IGrandparent)로 하면 에러가 발생하지 않는다.
    별로 좋은 코드는 아니지만 문제를 해결하기 위한 다른 방법은 존재치 않는 것으로 보인다.
  5. 다중 상속이 필요치 않다면 추상 클래스를 고려하라
    위의 문제를 해결할 수 있는 방법으로는 C++ 식으로 추상 클래스를 통해 인터페이스를 제작하는 방법이 존재한다.
    그러나 C++에서는 다중 상속을 지원하지만 델파이에서는 아니기에 다중 상속을 쓰려면 어쩔 수 없이 인터페이스를 필요로 한다.
    반대로 말하면 다중 상속이 필요없다면 추상 클래스로도 충분하다. 굳이 인터페이스로 인해 문제가 생기지 않도록 인터페이스를 쓰지 않는 것도 현명한 선택이다.

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.