야간 델파이 서비스가 고객마다 XLSX를 하나씩, 몇백 개 파일을 생성하고 그중 몇몇은 40만 행에 이른다고 해 봅시다. 프로파일을 떠 보면 놀라운 것은 좀처럼 셀 채우기 루프가 아닙니다. SaveAs 호출입니다. 기본 기록기에서는 각 워크시트가 메모리 안 XML 문자열 하나로 직렬화된 다음에야 그 문자열이 OOXML zip으로 압축되는데, 넓은 시트에서는 그 일시적 문자열이 자기가 만들어져 나온 셀 모델을 압도할 수 있습니다. 그래서 데이터를 넉넉히 짓고 800MB에 앉아 있던 작업이 저장 중에 2GB 컨테이너 한계를 뚫고 치솟고, 아무도 보지 않는 새벽 3시에 OOM 킬러가 버그 보고서를 접수합니다. 델파이와 C++Builder를 위한 losLab의 네이티브 스프레드시트 라이브러리 HotXLS에는 그 봉우리를 정면으로 겨눈 속성이 있습니다. StreamingWrite입니다. 그 둘레에는 배치 작업자가 메모리와 시간 예산 안에 머무는지를 가르는 지렛대 둘이 더 놓여 있는데, 바로 행 단위 쓰기 콜백과 빡빡한 루프 안에서 스타일 풀이 행동하는 방식입니다
기본 저장 경로가 버퍼링하는 것과 StreamingWrite가 바꾸는 것
기본 XLSX 기록기는 단순함을 편듭니다. 워크시트 XML을 완전히 렌더링한 다음 완성된 문자열을 zip 압축기에 건넵니다. 시트 전체 XML이 몇 메가바이트에 들어가는 압도적 다수의 통합 문서에는 그것이 맞는 절충입니다. 시트 하나의 직렬화된 형태가 수백 메가바이트에 이르는 순간 더는 맞지 않게 됩니다. 스프레드시트 XML은 수다스럽습니다. 숫자 셀 하나마다 마크업 수십 글자가 들고, 그 전부를 담는 문자열은 연속되어야 합니다. 메모리 그래프에서는 그 서명을 놓치기 어렵습니다. 행이 채워지는 동안 길고 평평한 고원, 그다음 SaveAs 중의 뾰족한 삼각형 봉우리, 그리고 zip이 흘려 나간 뒤의 붕괴입니다
Book.StreamingWrite := True로 설정하면 SaveAs가 시트 XML을 생성하는 대로 zip 스트림에 곧장 내보내는 워크시트 기록기로 바뀝니다. 중간 문자열은 아예 할당되지 않고, 삼각형 봉우리는 잡음 속으로 평평해집니다
그것이 실제로 무엇을 사 주는지는 정확히 짚어야 합니다. 부풀려 말하면 잘못된 용량 계획으로 이어지기 때문입니다. 이 플래그는 저장 경로만 바꿉니다. 통합 문서를 짓는 일은 여전히 메모리 안 셀 모델 전체를 할당하므로, 채우기 단계의 고원은 예전과 똑같은 높이입니다. 사라지는 것은 저장 시점에 그 고원 위로 쌓이던 직렬화 봉우리이며, 40만 행을 채우는 작업에서 그 봉우리는 으레 메모리 예산에 들어맞느냐 터뜨리느냐의 차이 전부입니다. 이 속성의 기본값이 False인 것은 과거 동작을 지키기 위해서이므로, 켜는 것은 일부러 쓰는 명시적인 한 줄입니다
플래그를 켠 대량 내보내기
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // 풀 인덱스, 0 기반
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
if (R mod 1000) = 0 then
Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // 셀에서는 1 기반
end;
Book.StreamingWrite := True; // 시트 XML을 zip으로 곧장 흘려보냅니다
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Cells[R, C]는 필요할 때 셀을 만들어 주므로 루프 본문이 깔끔하게 유지됩니다. 격자 상한 두 가지는 외워 둘 만합니다. 1,048,576행과 16,384열이며, XlsxMaxRow와 XlsxMaxCol로 드러나 있습니다. 행 상한을 넘기는 데이터 공급은 여러분 코드에서 시트 여럿으로 쪼개야 합니다. 하류의 무엇도 그 초과를 알아채거나 대신 고쳐 주지 않으며, 파일은 그저 한계에서 잘린 채 끝납니다
셀마다 드는 Variant 부담 없이 행 채우기
Cells[R, C].Value 대입은 매번 셀 조회와 Variant 변환의 값을 치릅니다. 만 행에서는 아무도 알아채지 못합니다. 스무 열짜리 백만 행에서는 그 호출당 부담이 채우기 단계의 지배적 비용이 되고, 프로파일러가 곧장 그것을 가리킵니다. 배치 인터페이스를 쓰면 대신 기록기에 행 전체를 한 번에 건넬 수 있습니다. WriteRows는 호출마다 행 하나를 공급하는 콜백을 몰아 줍니다:
procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
LastCol: Integer; var Values: Variant; var Skip: Boolean;
var Cancel: Boolean);
begin
if not FReader.Next then
begin
Cancel := True; // 데이터 원본 고갈: 깔끔하게 멈춥니다
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// 리더에서 당겨 와 2..100001행, A..C열을 채웁니다
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
Cancel 플래그는 고정된 행 범위를 "최대 N행"으로 바꿔 주는데, 행 수가 아직 다 실행하지 않은 질의에서 나올 때 자연스러운 모양입니다. Skip은 더 가벼운 손길입니다. 실행을 멈추지 않고 개별 행 하나를 비워 둡니다. 셀을 채우는 것을 넘어, 이 콜백은 그러지 않았다면 채우기 루프에 어색하게 덧붙었을 운영상의 관심사를 담기에 좋은 집이 됩니다. 천 행마다 째깍이는 진행 계수기, 작업 스케줄러에서 폴링하는 취소 토큰, 원본 데이터베이스 읽기에 거는 속도 제한기까지 전부가 셀 쓰기 코드 사이로 꿰이는 대신 한자리에 삽니다. 읽기 쪽에서는 ForEachRow와 ForEachCell이 같은 패턴을 되비추는데, 배치 작업이 대용량 파일을 소비하는 동시에 생산할 때 중요합니다
스타일 풀은 끌어올림에 보답합니다
XLSX 스타일 모델은 공유 풀의 집합입니다. Fonts.Add, Fills.AddSolid, Borders.Add는 모두 0 기반 풀 인덱스를 돌려주고, 셀은 그 인덱스에 하나를 더해 FontIndex에 저장하는 식으로 글꼴을 참조하는데, 0은 통합 문서 기본값으로 예약되어 있습니다. 그 +1이 위의 대량 예제에 바로 나와 있습니다. 잊으면 셀이 조용히 엉뚱한 스타일을 집어 듭니다. 스타일 풀 인덱스에서 하나 어긋난 값도 여전히 유효한 인덱스이고 아무것도 예외를 일으키지 않기 때문입니다
여기서 따라 나오는 규율은 모든 스타일 객체를 행 루프 앞에서 만들고 루프 안에서는 그 인덱스를 참조하는 것입니다. Fonts.Add는 똑같은 정의를 중복 제거하므로 행마다 부르면 CPU만 낭비합니다. 함정은 Alignments.Add입니다. 호출마다 새 항목을 돌려주기 때문입니다. 10만 행 루프 안에서 그렇게 하면 styles.xml이 중복된 맞춤 레코드 십만 개에 파묻히고, 디스크의 파일이 부풀고, 중복을 다시 구문 분석하느라 이후 Excel에서 열 때마다 느려집니다. 스타일은 루프 밖에서 한 번씩 짓고, 그 인덱스를 필요한 만큼 여러 번 참조하십시오
스트림, 임시 디렉터리, 그리고 그 모두를 둘러싼 배치 루프
이 중 어느 것도 파일 시스템을 요구하지 않습니다. 양쪽 파사드 모두 IO 표면 전반에 TStream 오버로드를 지니고 있고, 그 안에는 Open, SaveAs, SaveAsCSV, SaveAsHTML, SaveAsODS가 있으므로, 배치 작업자는 디스크를 한 번도 건드리지 않고 블롭 저장소나 HTTP 응답으로 갈 TMemoryStream에 곧장 렌더링할 수 있습니다. 기억해 둘 날카로운 모서리가 하나 있습니다. SaveAs(Stream)은 스트림의 현재 위치부터 쓰고 그 뒤에 되감지 않으므로, 그 스트림을 전달을 맡을 무엇인가에 건네기 전에 직접 Position := 0으로 두십시오. 그러지 않으면 소비자가 0바이트를 읽습니다. XLS 파사드는 자기 나름의 손잡이를 둘 더합니다. SetTempDir은 BIFF 기록기의 임시 파일을 그것을 받아 낼 공간과 IO 여유가 있는 볼륨으로 향하게 하는데, 기본 임시 경로가 비좁은 시스템 디스크에 놓인 서버에서 중요합니다. UseSharedFormulas는 되풀이되는 수식 본문을 공유 그룹으로 접는데, 수식 하나가 열 전체로 복사되는 고전적 보고서 모양에서는 실질적인 크기 감소입니다
배치 루프 자체는 일부러 지루하게 둡니다:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // 새 인스턴스: 상태가 새지 않습니다
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // 나쁜 입력 하나가 배치를 죽여서는 안 됩니다
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
파일마다 새 통합 문서 인스턴스를 두는 비용은 마이크로초이고, 그것으로 파일 간 오염 버그 한 부류가 통째로 사라집니다. 17번 파일의 스타일, 정의된 이름, 문서 속성이 18번 파일로 샐 경로가 없습니다. Open 실패 시의 건너뛰고 계속하기도 그만큼 값을 합니다. 600개 파일 배치에서 잘려 올라온 업로드 하나는 남은 실행 전체가 아니라 로그 한 줄만큼만 값을 치러야 하기 때문입니다. 짚어 둘 만한 것이 하나 더 있는데, CSV 구간이 일부러 하지 않는 일입니다. SaveAsCSV는 수식을 글자 그대로의 텍스트로 써 내고 결코 평가하지 않으므로, 소비자가 계산된 숫자를 기대하는 변환 배치라면 해당 셀에 먼저 Calculate를 돌리거나, 앞선 계산에서 캐시된 결과를 이미 지닌 통합 문서에서 시작해야 합니다
동시성 모델: 스레드마다 통합 문서 하나
어느 파사드의 객체도 스레드 안전하지 않고, 설계가 그런 척한 적도 없습니다. 인스턴스 사이에 공유되는 전역 상태가 없으므로, 확장 규칙은 그저 작업자 스레드마다 통합 문서 하나이며 통합 문서를 스레드 간에 공유하지 않는 것입니다. 저마다 자기 TXLSXWorkbook을 가진 작업자 N개의 풀은 메모리가 천장이 될 때까지 거의 선형으로 확장하고, 그 천장에는 숫자를 붙일 수 있습니다. 동시에 존재하는 가장 큰 셀 모델에 작업자 수를 곱하고, StreamingWrite가 평평하게 만든 저장 시점 부담을 더하면 됩니다. 큐가 깊어질 때는 기록기 안이 아니라 작업 큐에서 역압을 거십시오. 통합 문서를 절반쯤 쓰다 굶은 스레드는 쓸모 있는 것을 아무것도 만들어 내지 못한 반면, 빈 작업자를 몇 초 기다린 작업은 온전하게 끝납니다
공유 수식, 읽기 쪽 그래픽 건너뛰기, XLS 전용 지렛대를 포함한 더 넓은 조정 그림은 대용량 통합 문서 성능 안내를 보십시오. 행이 질의에서 곧장 나오는 배치 작업은 델파이 보고서를 위한 데이터베이스 내보내기 패턴에서 따로 다룹니다
HotXLS는 외부 의존성 없는 네이티브 Object Pascal로 여러분의 델파이 또는 C++Builder 서비스에 컴파일되어 들어갑니다. 에디션과 라이선스는 HotXLS Delphi Component 제품 페이지에 있습니다