기술 문서

Delphi용 HotXLS의 증분 수식 재계산 시스템

Delphi 및 C++Builder용 네이티브 Excel 라이브러리인 HotXLS는 TXLSXWorkbook.Recalculate를 통해 증분 수식 재계산(Incremental formula recalculation)을 수행합니다. 최초 호출 시 수식 종속성 그래프를 빌드하고 모든 수식 셀을 계산하며, 이후의 호출에서는 위상 정렬(Topological order) 순서에 따라 이전 연산 이후 값 변경의 영향을 받은 더티(Dirty) 셀들만 선별적으로 다시 계산합니다. 이 단일 스윕 과정에서 발생하는 연산 비용은 전체 통합 문서의 크기가 아닌 실제 변경이 감지된 더티 셀의 개수와 비례합니다

이 설계상의 결정 하나가 입력 파라미터가 변경되었을 때 밀리초 단위로 반응하는 금융 수식 모델과, 변경 시마다 수 초 동안 멈추는 오동작 모델을 가르는 분수령이 됩니다. 소량의 입력 셀이 수천 개의 하위 수식을 구동하는 보고서 생성 시스템을 구축하는 경우, 이 가이드를 통해 종속성 그래프의 동작 방식, 증분 계산 연산에서 배제되는 특수 함수들, 순환 참조 오류가 무한 루프를 돌지 않고 즉시 보고되는 방식을 파악할 수 있습니다

왜 단 하나의 셀만 변경해도 10만 개의 수식이 다시 계산되나요?

단순하게 설계된 수식 엔진은 수식 간의 상호 종속 관계를 기억하지 못하므로, 값을 수정했을 때 시트 전체를 처음부터 끝까지 다 계산하는 방법만이 유일하게 안전한 선택입니다. 더 나쁜 방식은 수식 A가 수식 B를 참조할 때 그 자리에서 바로 B를 연산하는 클래식 재귀형(Recursive) 구동 방식입니다. 이 방식은 기존의 캐시된 결과값을 완전히 무시하고 피참조 셀들을 무조건적으로 다시 평가합니다. 이전 셀을 순차 참조하는 n개 수식 체인의 경우 연산당 O(n²) 회의 계산 비용을 유도하며, 순환 참조가 감지되면 재귀 연산이 스택 오버플로를 일으킵니다. 재귀식 수식 해석기에 종속 모델을 연결해 본 개발자라면 누구나 이 두 가지 실패 양상을 직접 경험해 보았을 것입니다

Excel은 수십 년 전에 수식 계산 체인(Calculation chain) 개념을 통해 이 문제를 해결했습니다: 특정 편집 작업이 소량의 셀만 더티(Dirty) 상태로 표시하도록 수식 셀들의 실행 순서를 유지하고, 엔진은 해당 변경 사항의 영향을 받는 체인의 후미 영역만 순회합니다. HotXLS는 컴파일된 수식 트리에서 단 한 번 빌드된 후 재계산 주기마다 재사용되는 명시적인 종속성 그래프(Dependency graph)를 통해 동일한 설계를 구현합니다. 관건은 정교한 기법 그 자체보다 재계산 연산 비용이 전체 워크북 크기가 아닌 실제 변경 사항의 규모에 비례해야 한다는 원칙입니다

종속성 그래프가 편집 연산을 단일 패스로 최적화하는 방법

HotXLS 종속성 그래프는 각 수식 셀을 하나의 노드로 설정하고, 선행 셀에서 종속 셀로 향하는 에지(Edge)를 그립니다. 코드로 특정 셀 값을 수정하면 워크북은 해당 셀을 더티(Dirty) 상태로 기록합니다. Recalculate가 수행되면 더티 표시가 에지를 타고 모든 하위 수식으로 전파되며, Kahn 알고리즘을 사용한 위상 정렬 순서에 따라 더티 서브그래프(Dirty subgraph)가 단 한 번 계산됩니다. 모든 수식이 선행 조건 셀의 연산이 끝난 후에만 호출되므로, 각 노드는 단 한 번만 평가되어 O(dirty) 연산 효율성을 보장합니다

위상 정렬 순서는 재귀 호출 오작동 문제를 근본적으로 예방합니다. 재계산 주기 동안 엔진은 다른 수식 셀을 가리키는 모든 참조가 피참조 셀의 캐시 값을 직접 읽어 오도록 제어합니다. 위상 정렬 순서상 피참조 셀의 연산은 이미 끝난 상태이므로 캐시 값의 신뢰성이 항상 보장됩니다. 이 구조로 인해 순환 참조 상황에서도 무한 루프 재귀 오류가 발생하지 않습니다: 패스 내부의 어떠한 동작도 이웃 셀에 대한 평가기에 재진입하지 않기 때문입니다

var
  Book: TXLSXWorkbook;
  Inputs, Model: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Inputs := Book.Sheets.Add('Inputs');
    Model  := Book.Sheets.Add('Model');

    Inputs.Cells[2, 2].Value := 0.05;                 // 성장 모델 가정
    Model.Cells[2, 2].Formula := 'Inputs!B2*1000';    // XLSX 수식에는 시작 기호인 '='를 쓰지 않습니다
    Model.Cells[3, 2].Formula := 'B2*(1+Inputs!B2)';
    // ... Data!A2:D100 영역을 상세 데이터 행으로 채웁니다 ...

    Book.Recalculate;                 // 첫 번째 호출: 그래프를 빌드하고 전체 연산을 수행합니다

    Inputs.Cells[2, 2].Value := 0.07; // 값 변경 시 해당 셀이 더티로 마킹됩니다
    Book.Recalculate;                 // 두 번째 호출: 하위 체인 영역만 연산이 실행됩니다
  finally
    Book.Free;
  end;
end;

모든 계산 결과는 각 셀의 캐시된 Value에 저장되므로, Recalculate가 끝난 후 다른 일반 셀처럼 값을 꺼내 쓸 수 있습니다. 일괄 보고서 생성 루프의 기본 공식은 정확히 위의 코드와 같습니다: 수식 모델을 한 번 로드하거나 구성한 뒤, 소량의 입력 값을 바꾸고 Recalculate를 연이어 호출하면 실제로 영향을 받는 수식들에 대한 계산 비용만 지불하게 됩니다

어떤 Excel 함수들이 매 패스마다 재계산을 강제하나요?

HotXLS는 NOW, TODAY, RAND, OFFSET, INDIRECT 함수를 휘발성(Volatile)으로 정의합니다: 이들 함수를 포함하는 수식은 선행 셀들의 수정 여부에 관계없이 매 Recalculate 연산 주기마다 매번 재계산됩니다. 처음 세 함수가 휘발성인 이유는 Excel과 마찬가지로 연산 결과가 다른 셀 데이터가 아닌 연산 시점에 좌우되기 때문입니다. OFFSETINDIRECT가 휘발성으로 분류되는 이유는 더 정교한 원인이 있습니다: 참조할 대상 셀 주소가 런타임에 동적으로 해석되므로, 종속성 그래프 빌드 단계에서 고정적인 에지를 그릴 수 없기 때문입니다

그래프 빌더가 단일 직사각형 범위로 확정할 수 없는 참조 관계에도 동일한 보수적인 규칙이 적용됩니다. 다중 영역이 정의된 범위 이름을 활용하거나 외부 통합 문서를 참조하는 수식 또한 휘발성 모드로 하향 조정되어 주기마다 다시 평가됩니다. 이 정책은 의도된 것입니다: 불필요한 연산은 성능 소모를 조금 일으킬 뿐이지만, 종속성 에지가 누락되어 정산 수치 오류가 있는 보고서가 최종 저장되는 것은 비즈니스상 심각한 재앙이기 때문입니다. 통합 문서 범위 이름을 주로 사용하는 모델인 경우, 이름 정의 및 교차 시트 수식 가이드에서 단일 영역 이름이 정상적으로 종속성 그래프 연산에 참여하여 연산이 최적화되는 구조를 다룹니다

실제적인 지침은 직관적입니다. 대용량 수식 모델에서 연산이 빈번한 핵심 경로는 종속성 그래프가 원활하게 작동할 수 있도록 일반 셀 및 범위 참조로 설계하고, OFFSETINDIRECT의 동적 주소 접근 기능은 필수적인 소수 구역에만 격리하여 사용하십시오. 휘발성 수식이 수천 개 포진한 모델은 아무리 사소한 변경 시에도 매번 해당 수식들을 전부 재계산하므로, 사용자들이 키보드를 입력할 때마다 화면이 멈춘다고 불평하는 Excel 문서의 성능 저하 원인과 정확히 일치하게 됩니다

HotXLS는 순환 참조(Circular reference)를 어떻게 보고하나요?

TXLSXWorkbook.Recalculate는 성공 시 lxOk를 반환하고 참조 사이클이 감지되었을 때 lxErrorRef를 반환합니다. 위상 정렬 연산 도중 Kahn 알고리즘에 의해 결코 해제될 수 없는 노드들로 사이클 구성 셀들을 판별해 내며, 무한 루프 오류를 방지하기 위해 이들을 건너뜁니다: 이들의 캐시 값은 변경되지 않고 그대로 보존되며, 사이클 외부의 다른 모든 수식들은 정렬된 순서대로 정상 작동합니다. 이에 따라 백엔드 프로그램은 먹통(Hang)이 되는 대신 명확한 에러 코드를 인계받습니다

case Book.Recalculate of
  lxOk:
    SaveReport(Book);
  lxErrorRef:
    // 참조 사이클이 존재합니다. 사이클 구성원들은 이전의
    // 캐시 값을 유지하며 사이클 외부의 데이터는 정상 연산됩니다
    LogWarning('Circular reference detected - review model inputs');
end;

어떤 셀들이 사이클을 유발했는지 판별하는 것은 디버깅 단계의 작업이며, 수식 평가 추적기가 이를 위한 최적의 수단입니다: 의심되는 수식을 추적하면 고리 형태의 참조 체인이 역방향으로 단계별 추적되어 정밀하게 식별할 수 있습니다. 상용 수식 모델에서의 순환 참조는 합계 범위가 자기 자신을 물리적으로 침범한 형태 등 대개 작성자의 단순 실수이므로, 연산 즉시 명확한 오류 코드를 수신하는 것이 대처에 매우 유리합니다

배열 수식, 더티 추적 및 그래프 재구축 조건

CSE 배열 수식은 개별 셀 단위가 아닌 고정된 전체 직사각형 영역을 대상으로 단 하나의 노드만 가집니다. 루트 수식이 매 회당 한 번 계산되면 결과 행렬이 멤버 셀들 전체에 직접 기입되며, 좌측 상단 루트 셀뿐만 아니라 고정된 범위 내의 모든 셀을 참조하는 임의의 외부 수식은 해당 루트 노드와 종속성 에지로 연결됩니다. 스칼라 계산 결과들은 Excel 레거시 배열 사양이 규정하는 대로 사각형 영역 전체로 브로드캐스트(분산)됩니다

더티 상태 추적(Dirty tracking)은 일반 속성의 세터(Setter)에 훅(Hook)으로 걸려 있으므로 개발자가 특별한 추가 작업을 수행할 필요가 없습니다. 특정 셀의 Value를 변경하면 엔진은 변경을 인지하고 종속 셀들을 더티로 지정합니다. 새로운 Formula를 작성하는 것은 구조 변경이므로 그래프 전체를 최신이 아닌(Stale) 상태로 표시하며 다음 Recalculate 호출 시 그래프가 먼저 재구축됩니다. 시트를 생성, 삭제 또는 이동시키는 연산 역시 노드 식별자가 시트 인덱스 정보와 직결되어 있으므로 그래프를 무효화시킵니다. Recalculate를 명시적으로 전혀 호출하지 않는 문서의 경우, 이 훅들은 연산할 때마다 단순히 nil 검사 한 번의 연산 비용만 소모하므로 일반적인 읽기/쓰기 작업에는 성능 저하를 초래하지 않습니다

주의해야 할 한계점 하나는 다음과 같습니다: 종속성 그래프는 셀과 셀 사이의 참조 관계를 관리하므로, OnUserFunction을 통해 등록한 사용자 정의 함수(UDF) 역시 입력 인수가 되는 셀 데이터들이 변경되었을 때 다른 일반 수식과 똑같이 재평가됩니다. 엔진을 UDF로 확장하여 빌드하려는 경우, HotXLS 수식 엔진 내 커스텀 함수 구현 가이드에서 콜백 사약 및 전달인자 획득 방식을 정밀하게 다룹니다

증분 재계산 시스템은 수식 해석기, 이름 정의 관리자 및 파일 입출력 파이프라인과 함께 HotXLS Delphi Excel 컴포넌트의 표준 XLSX 엔진에 탑재되어 있습니다. Delphi 또는 C++Builder 프로그램에서 견적 정산 시트, 보고서 집계 모델 등 복잡한 수식 모델을 상시 구동하는 경우, Recalculate는 전체 문서를 재연산하는 성능 낭비와 변경된 영역만 계산하는 고효율 연산을 가르는 결정적인 차이가 됩니다