기술 문서

HotXLS Delphi Component: Delphi에서 Office-free workbook automation

서버의 유일한 임무가 Excel 파일을 내보내는 것이라면, 그 서버가 Excel을 실행할 이유는 없습니다. 빌드 에이전트나 보고서 서비스에 Office를 설치해 COM 자동화로 구동하는 것은 잘못된 설계이며, 이 관행이 존재해 온 이래로 줄곧 잘못된 설계였습니다. Microsoft 스스로도 20년 동안 누그러지지 않은 안내에서 이렇게 말합니다. Office는 무인 서버 측 프로세스에서 자동화되도록 만들어지지도, 라이선스되지도 않았습니다. 올바른 답은 BIFF와 OOXML 바이트를 직접 작성하는 것이며, Excel은 그림에 전혀 등장하지 않아야 합니다. 이것이 스프레드시트 형식 자체를 직접 읽고 쓰는 네이티브 Object Pascal 라이브러리인 HotXLS의 전체 전제입니다. 그러니 멈추거나 누수를 일으키거나 좌석당 비용을 치러야 할 데스크톱 애플리케이션은 어디에도 없습니다

서비스에서 EXCEL.EXE를 구동하면 실패하는 이유

COM 자동화는 데스크톱 프로그램을 원격으로 제어하며, 데스크톱 프로그램은 Windows 서비스가 건네줄 수 없는 세 가지, 즉 로드된 사용자 프로필, 대화형 윈도우 스테이션, 화면을 지켜보는 사람을 조용히 전제합니다. 이것들을 빼앗으면 실패는 어떤 개발자 컴퓨터도 재현하지 못하는 형태로 도착합니다. 파일 복구 프롬프트, 애드인 오류, 라이선스 활성화 대화상자가 아무도 볼 수 없는 데스크톱에서 열리고, 이를 촉발한 자동화 호출은 결코 반환되지 않습니다. 결국 호출자는 타임아웃을 맞고 죽지만, Excel 인스턴스는 종종 그렇지 않아서 파일 잠금을 붙든 채 고아로 살아남아 다음 실행을 오염시킵니다. 서비스 계정 아래 열한 개의 떠도는 EXCEL.EXE 프로세스가 쌓이는 것을 지켜본 사람이라면 그 뒷이야기를 이미 압니다

COM 자동화로 EXCEL.EXE를 구동하는 Delphi 서비스(숨은 대화상자와 고아 프로세스가 호출을 막음)와, HotXLS가 프로세스 내에서 BIFF8과 OOXML 통합문서 바이트를 직접 쓰는 것 대비 다이어그램
COM 자동화는 데스크톱 프로그램의 빠진 가정들을 물려받는 반면, HotXLS는 BIFF8과 OOXML 바이트를 직접 써서 서버에 설치할 것이 아무것도 없습니다

아무것도 크래시하지 않을 때조차 확장성 이야기는 나을 것이 없습니다. Excel 인스턴스는 단일 워크북 파이프라인이고, 모든 속성 접근은 프로세스 간 COM 마샬링의 비용을 치르며, 코드를 실행하는 상자에는 바로 이 사용 사례를 조건에서 제외하는 Office 라이선스가 걸려 있습니다. 대부분의 팀은 이런 한계를 장애 하나하나를 겪으며 만나게 되고, 이것이 대략 "COM 계층을 은퇴시키자"가 로드맵에 오르게 되는 경위입니다

이 재작성이 시작되기 전에 범위 관련 질문 하나를 정리해야 합니다. 이것이 실제 작업량을 결정하기 때문입니다. COM 코드는 셀 값만 설정하는 경우가 거의 없습니다. 형식 상수와 함께 Workbook.SaveAs를 호출하고, 재계산을 강제하며, 인쇄 설정을 밀어붙이고, 때로는 클립보드에 손을 뻗습니다. 옛 코드를 훑으며 그 동작들 중 실제로 출력물에 실리는 것이 무엇인지 적어 두십시오. 각각은 네이티브 라이브러리의 서로 다른 구석에 안착하며, 그중 몇 가지(클립보드 상호운용이 가장 뚜렷한 예입니다)는 서버 측에서는 아무 의미가 없으므로 이식하는 대신 버려야 합니다

두 개의 네이티브 엔진, 두 개의 소유권 모델

HotXLS는 Excel 프로세스를 두 개의 직접적인 형식 구현으로 대체합니다. BIFF8 레코드 스트림 엔진(TXLSWorkbook, lxHandle 유닛)이 .xls를 처리합니다. OOXML 패키지 라이터(TXLSXWorkbook, lxHandleX 유닛)는 ECMA-376 / ISO/IEC 29500을 준수하는 .xlsx를 생성합니다. 서버에서 등록할 것도 설치할 것도 없으며, 메모리가 허용하는 만큼 워크북을 동시에 얼마든지 열어 둘 수 있습니다

초반에 사람들이 걸려 넘어지는 부분은 두 파사드가 메모리를 서로 다르게 소유한다는 점이며, 이 차이는 크래시가 나기 전까지는 조용합니다:

var
  Book: IXLSWorkbook;          // 인터페이스 참조: 자동으로 해제됨
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // 일반 객체: 직접 해제해야 함
  SheetX: TXLSXWorksheet;
begin
  // BIFF8 .xls 출력 - Free 없음; 인터페이스 참조 카운트가 소유함
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Report';
  Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
  Book.SaveAs('report.xls');

  // OOXML .xlsx 출력 - 명시적인 수명 관리
  BookX := TXLSXWorkbook.Create;
  try
    SheetX := BookX.Sheets.Add('Report');
    SheetX.Cells[1, 1].Value := 'Generated without Excel';
    BookX.SaveAs('report.xlsx');
  finally
    BookX.Free;
  end;
end;

XLS 파사드는 IXLSWorkbook 인터페이스를 통해 참조 카운트가 매겨집니다. 변수를 인터페이스 타입으로 선언하고 절대 그것에 Free를 호출하지 마십시오. 같은 객체를 일반 객체 변수에 담아 직접 해제하면, 참조 카운트가 두 번째로 그것을 해제해 버립니다. XLSX 파사드는 평범한 try..finally를 원하는 일반 객체입니다. 셀 주소 지정은 양쪽 모두 1부터 시작하며, 이것이 둘이 유일하게 일치하는 지점입니다. 시트 컬렉션은 그렇지 않습니다. XLS 쪽의 Entries는 1부터 시작하고, XLSX의 Items 인덱서는 0부터 시작하며, 이 1씩 어긋난 차이는 어느 쪽으로 잘못 알든 컴파일은 깔끔하게 되고 실행 시점에만 스스로 드러납니다

워크북을 곧바로 HTTP 응답으로 기록하기

서버 측 내보내기는 대개 디스크를 건드릴 이유가 없습니다. 임시 파일은 정리 정책을 요구하고, 동시 요청 아래에서 충돌하며, 아무도 감사할 생각을 못한 볼륨 위에 고객 데이터를 남겨둡니다. 두 파사드 모두 SaveAs 오버로드를 통해 TStream을 받으므로, 워크북을 곧바로 응답으로 보낼 수 있습니다:

두 HotXLS Delphi 퍼사드 비교 다이어그램: TXLSWorkbook은 IXLSWorkbook 인터페이스 참조 카운팅으로 자동 해제되고, TXLSXWorkbook은 try..finally 블록 안의 명시적 Free가 필요한 평범한 객체
XLS 퍼사드는 인터페이스 참조 카운트로 해제되지만 XLSX 퍼사드는 명시적 Free가 필요하고, 시트 컬렉션은 1 기반 Entries와 0 기반 Items 사이에서 다릅니다
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // 현재 스트림 위치부터 기록함
  Mem.Position := 0;         // 스트림을 넘기기 전에 되감음
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // 이제 프레임워크가 Mem을 소유함
finally
  Book.Free;
end;

되감기 부분이야말로 주석을 받을 자격이 있는 줄입니다. SaveAs(Stream)는 스트림의 현재 위치부터 기록하며 이후 절대 처음으로 다시 탐색하지 않습니다. Mem.Position := 0을 빠뜨리면 클라이언트는 0바이트짜리 다운로드를 받거나 Excel이 파일을 손상되었다고 판단합니다. 이는 웹을 상대하는 워크북 코드에서 가장 흔한 버그이며 가장 잔인한 버그이기도 합니다. 스트림 길이가 0이 아니라는 것만 확인하는 단위 테스트는 이를 무사히 통과시켜 버리기 때문입니다

하나의 워크북 구축 루틴이 재구성 없이 다른 모든 배포 형식에 닿습니다. SaveAsCSV는 "그냥 원시 데이터를 달라"는 요청에 답하고, SaveAsHTML은 "포털 페이지에 떨어뜨려 달라"를 처리하며, SaveAsRTF는 문서 파이프라인에 공급하고, SaveAsODS는 OpenDocument 의무 요건을 다룹니다. 모두 파일과 스트림 오버로드를 함께 갖추고 있습니다. 단일 내보내기 루틴과 형식 매개변수 하나가 예전에는 네 개의 별도 COM 매크로였던 것을 대체합니다. HTML 익스포터의 TXLSXHtmlExportOptions는 제목, CSS 클래스, 조각/전체 문서 스위치를 담고 있어 포털 사용 사례가 내보낸 마크업을 정규식으로 편집하는 일에서 벗어나게 해 줍니다

Delphi 요청 핸들러 다이어그램: HotXLS 통합문서를 TMemoryStream에 저장하고, Mem.Position을 0으로 되감아 스트림을 HTTP 응답에 넘기며, 곁에 CSV, HTML, RTF, ODS 익스포터 병기
TMemoryStream에 저장하고 인계 전에 되감으면 워크북 바이트가 클라이언트로 곧장 흐르고, 하나의 익스포트 루틴이 CSV, HTML, RTF, ODS 라이터를 모두 커버합니다

계산해 줄 Excel 프로세스 없이 수식 값 얻기

COM 자동화 아래에서는 Excel이 모든 것을 공짜로 재계산해 주었고, COM을 없애면 조용히 이 혜택이 사라집니다. SaveAs는 수식을 평가하지 않은 채 텍스트로 저장합니다. 숫자는 Excel이 파일을 열고 재계산할 때만 나타나며, 이 동작은 XLS 파사드에서 RecalcOnSaveCalculationMode로 조율할 수 있습니다. 사람에게 향하는 파일이라면 이것이 정확히 옳습니다. 배포 전에 합계를 확인해야 하는 서비스에는 잘못된 방식이며, 수식 텍스트를 결과 대신 기록하는 CSV 내보내기에도 잘못된 방식입니다. 어느 경우든 내장 엔진으로 서버에서 평가해야 합니다:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // XLSX 파사드: '=' 접두사 없음
Total := BookX.Calculate('SUM(A1:A2)');       // 지금 서버에서 평가함
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

파사드 관례는 여기서도 다시 뭅니다. XLSX 쪽은 등호 없이 Cell.Formula를 통해 표현식을 대입하고, XLS 쪽은 앞에 '='가 붙은 채로 Cell.Value를 통해 기록합니다. 한쪽 코드를 그대로 다른 쪽으로 옮기면, 잘못된 관례가 그저 수식처럼 보이기만 하는 텍스트 문자열을 저장해 버리고, 이를 알려줄 오류는 없습니다. 워크북의 수식이 여러분의 비즈니스 로직에 닿아야 할 때는 OnUserFunction 콜백을 통해 엔진이 알 수 없는 함수 이름을 평가 시점에 Delphi 코드로 넘길 수 있습니다. 이것이 COM 자동화 시스템이 자라온 바로 그 스프레드시트 안에 숨어 있곤 하던 UDF 애드인의 네이티브 대체물입니다

서버에서만 드러나는 배포 관련 함정

몇 가지 세부사항이 배포가 깔끔한지 아니면 골치 아픈지를 결정하며, 첫 번째는 유닛 그래프입니다. 드래그 앤 드롭 방식의 데이터셋 익스포터인 TDataToXLS는 VCL Forms, Controls, Dialogs를 끌어들입니다. 데스크톱 도구에서는 무해하지만, 콘솔 서비스에서는 VCL 전체를 뒤에 매달고 옵니다. 핵심 유닛인 lxHandlelxHandleXWindows, Classes, SysUtils, Variants만 필요로 하므로, 순수 서비스라면 편의를 위해 컴포넌트를 가져오는 대신 핵심 API에 대해 자신만의 데이터셋 루프를 작성하는 편이 낫습니다

그다음은 스레딩입니다. 워크북 인스턴스는 스레드 안전하지 않지만 전역 상태를 전혀 공유하지도 않으므로, 확장되는 패턴은 가장 단순한 패턴입니다. 작업당, 또는 워커 스레드당 워크북 객체 하나입니다. 이는 단일 공유 Excel 인스턴스로는 결코 할 수 없는 병렬 보고서 생성을 가능하게 합니다. 자신만의 워크북을 만들고 채우고 저장하고 해제하는 요청 핸들러는 락이 전혀 필요 없으며, 실패의 파급 범위는 "공유된 Excel 인스턴스가 모두에게 막혀 버렸다"에서 "이 요청 하나가 예외를 던졌다"로 줄어들고, 이는 이미 기존 오류 처리가 어떻게 다뤄야 할지 아는 상황입니다

형식 대상 지정이 마지막입니다. TXLSWorkbook.SaveAs는 기본적으로 BIFF(xlExcel97)를 기록하며, XLS 콘텐츠를 .xlsx로 밀어 넣는 것은 SaveXLSWorkbookAsXLSX 브리지를 거쳐 낮아진 충실도로 이루어집니다. 파이프라인 끝에서 변환하는 대신, 설계 시점에 배포하려는 형식에 맞춰 파사드를 선택하십시오

전형적인 대체 프로젝트의 데이터 로딩 절반에 대해서는 데이터베이스-워크북 내보내기 패턴이 컴포넌트와 수작업 루프 모두를 다루며, 행 수가 6자리에 도달하면 대규모 워크북 성능 기법이 몇 분과 몇 초의 차이를 만듭니다. 디자이너가 유지 관리하는 레이아웃으로부터 구축된 보고서는 템플릿 보고서 생성 안내에서 다룹니다

HotXLS는 Delphi와 C++Builder를 위한 Object Pascal 소스로 제공됩니다. 에디션, 라이선스, 전체 API 레퍼런스는 HotXLS Delphi Component 제품 페이지에 있습니다