Delphi 내장 표현식 엔진 활용하기

Delphi의 RTL(Run-Time Library)에는 라이브 바인딩(Live Bindings) 기능의 핵심으로 사용되는 강력한 표현식 엔진이 포함되어 있습니다. 하지만 이 엔진은 단독으로도 문자열 형태의 수식이나 로직을 동적으로 평가하는 용도로 매우 유용하게 사용할 수 있습니다. 많은 개발자들이 이 기능의 존재를 인지하지 못하고 외부 라이브러리를 도입하곤 하는데, 사실 Delphi는 이미 오래 전부터 이러한 기능을 자체 제공해 왔습니다.

예를 들어, Delphi 10.4.2에서 새롭게 추가된 VCL NumberBox 컴포넌트는 사용자가 10 + 2 * 5와 같은 수식을 입력하면 이를 자동으로 계산하여 결과인 20으로 치환합니다. 이 기능의 내부 구현 역시 바로 이 TBindingExpression 기반의 엔진을 활용하고 있으며, 간단한 정적 메서드만으로도 쉽게 적용할 수 있습니다.

var
  LResult: Double;
  LExpr: TBindingExpression;
begin
  LExpr := TBindings.CreateExpression([], '3.14 + 2.86');
  try
    LResult := LExpr.Evaluate.GetValue.AsExtended;
    ShowMessage('결과: ' + FloatToStr(LResult)); // 출력: 결과: 6
  finally
    LExpr.Free;
  end;

여기서 TBindings.CreateExpression는 가장 간단한 형태의 표현식 평가를 위한 편의 메서드입니다. 그러나 보다 복잡한 시나리오에서는 직접 TBindingExpression을 생성하고 컴파일 및 평가 과정을 제어하는 것이 유리합니다.

표현식 처리의 두 단계: 컴파일과 평가

표현식 엔진의 핵심 원리는 두 가지 작업으로 나뉜다는 점입니다:

  • 컴파일(Compile): 표현식 문자열을 구문 분석하여 내부 트리 구조로 변환합니다.
  • 평가(Evaluate): 컴파일된 표현식을 실제 값으로 계산합니다.

같은 표현식을 여러 번 다른 데이터로 평가해야 할 경우, 컴파일은 한 번만 수행하고 평가를 반복함으로써 성능을 최적화할 수 있습니다.

기본 예제: 문자열 표현식 처리

아래는 사용자가 입력한 표현식을 실시간으로 평가하는 간단한 폼 기반 예제입니다. 두 개의 TMemo와 하나의 버튼을 배치하고, 다음 코드를 버튼 클릭 이벤트에 연결합니다.

procedure TForm1.Button1Click(Sender: TObject);
var
  Expr: TBindingExpression;
  ResultValue: TValue;
begin
  Expr := TBindingExpressionDefault.Create;
  try
    Expr.Source := MemoInput.Lines.Text; // 예: "Hello " + "World!"
    if Expr.Compile then
    begin
      ResultValue := Expr.Evaluate;
      MemoOutput.Lines.Add(ResultValue.ToString); // 출력: Hello World!
    end
    else
    begin
      MemoOutput.Lines.Add('구문 오류: 유효하지 않은 표현식');
    end;
  finally
    Expr.Free;
  end;
end;

이 예제는 문자열 연결(+) 정도만 지원하므로 제한적이지만, 객체 바인딩을 통해 기능을 확장할 수 있습니다.

객체 바인딩: RTTI 기반 동적 접근

엔진의 진정한 힘은 런타임 타입 정보(RTTI)를 통해 객체의 속성과 메서드를 표현식 내에서 직접 참조할 수 있다는 점입니다. 먼저 다음과 같은 클래스를 정의합니다.

type
  T사람 = class
  private
    F이름: string;
    F나이: Integer;
    procedure SetName(const Value: string);
    procedure SetAge(Value: Integer);
  public
    property 이름: string read F이름 write SetName;
    property 나이: Integer read F나이 write SetAge;
  end;

이제 이 객체를 표현식에 바인딩합니다.

var
  사람1: T사람;
  식: TBindingExpression;
begin
  사람1 := T사람.Create;
  사람1.이름 := '홍길동';
  사람1.나이 := 25;

  식 := TBindingExpressionDefault.Create;
  식.Source := '사람.이름 + "은(는) " + 사람.나이.ToString + "세 입니다."';

  // 객체를 심볼 '사람'으로 등록하여 컴파일
  if 식.Compile([TBindingAssociation.Create(사람1, '사람')]) then
  begin
    MemoOutput.Lines.Add(식.Evaluate.ToString); // 출력: 홍길동은(는) 25세 입니다.
  end;

  사람1.Free;
  식.Free;
end;

함수 객체 통합

속성뿐 아니라 메서드도 표현식에서 호출할 수 있습니다. 아래와 같은 유틸리티 클래스를 만들어보겠습니다.

type
  T수학함수 = class
  public
    function 제곱(X: Integer): Integer;
    function 역수(X: Double): Double;
  end;

function T수학함수.제곱(X: Integer): Integer;
begin
  Result := X * X;
end;

function T수학함수.역수(X: Double): Double;
begin
  if X <> 0 then
    Result := 1 / X
  else
    Result := 0;
end;

이 객체를 바인딩하면 표현식 내에서 함수를 자유롭게 사용할 수 있습니다.

var
  수학: T수학함수;
  식: TBindingExpression;
begin
  수학 := T수학함수.Create;
  식 := TBindingExpressionDefault.Create;
  식.Source := '수학.제곱(5) + 수학.역수(2)'; // 25 + 0.5

  if 식.Compile([TBindingAssociation.Create(수학, '수학')]) then
  begin
    MemoOutput.Lines.Add(식.Evaluate.GetValue.AsExtended.ToString); // 25.5
  end;

  수학.Free;
  식.Free;

응용 가능성

이 기술은 다음과 같은 시나리오에서 유용합니다:

  • 사용자 정의 수식 필터 (예: "가격 > 100 AND 재고 < 10")
  • 보고서 또는 템플릿 엔진 내에서의 동적 값 삽입
  • 스크립팅 기능이 필요한 경량 애플리케이션

또한, 필요 시 알림 콜백을 등록하여 바인딩된 객체의 속성 변경을 감지하고 표현식을 다시 평가하는 것도 가능합니다.

태그: Delphi RTL Expression Engine TBindingExpression RTTI

7월 2일 18:43에 게시됨