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")
- 보고서 또는 템플릿 엔진 내에서의 동적 값 삽입
- 스크립팅 기능이 필요한 경량 애플리케이션
또한, 필요 시 알림 콜백을 등록하여 바인딩된 객체의 속성 변경을 감지하고 표현식을 다시 평가하는 것도 가능합니다.