java V1SharedStringsVsInline 200000// sharedStrings 방식 작성기의 핵심: 문자열 → 인덱스 맵을 끝까지 유지
Map<String, Integer> index = new LinkedHashMap<>();
int sst(String s) { return index.computeIfAbsent(s, k -> index.size()); }
// 셀은 <c t="s"><v>idx</v></c>, close() 에서 index.keySet() 을 sharedStrings.xml 로 기록
// 출력(예, 20만 행 × 문자열 4열, 고유 문자열 약 300개):
// inlineStr : 8,910,112 bytes, 2,050 ms, 힙 증가 3 MB
// sharedStrings : 5,120,448 bytes, 2,210 ms, 힙 증가 3 MB (고유 문자열이 적어 맵이 작다)
// 고유 문자열을 행마다 다르게(ORD-000001…) 하면 sharedStrings 의 힙이 행 수에 비례해 증가반복되는 코드값(상태, 지역, 상품명)이 많은 표는 sharedStrings 가 40% 가량 작습니다. 그러나 주문번호처럼 행마다 고유한 문자열이 많으면 크기 이점이 사라지고 맵 메모리만 늘어납니다. POI SXSSFWorkbook 이 기본으로 inlineStr 을 쓰는 이유입니다(useSharedStringsTable=false).
// worksheet 자식 요소는 스키마 순서를 지켜야 Excel 이 연다:
// sheetViews? cols? sheetData autoFilter? mergeCells? … (sheetViews/cols 는 앞, autoFilter/mergeCells 는 뒤)
out.write("<sheetViews><sheetView workbookViewId=\"0\">"
+ "<pane ySplit=\"1\" topLeftCell=\"A2\" activePane=\"bottomLeft\" state=\"frozen\"/>" // 1행 틀 고정
+ "</sheetView></sheetViews>");
// … <cols> … <sheetData> 행들 … </sheetData>
out.write("<autoFilter ref=\"A1:E" + lastRow + "\"/>"); // 헤더 자동 필터
out.write("<mergeCells count=\"1\"><mergeCell ref=\"A" + (lastRow + 1) + ":D" + (lastRow + 1) + "\"/></mergeCells>"); // 합계 라벨 병합
// styles.xml 에 사용자 정의 서식 추가: numFmtId 164 부터
"<numFmts count=\"1\"><numFmt numFmtId=\"164\" formatCode=\""₩"#,##0\"/></numFmts>"
"<xf numFmtId=\"164\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\"/>" // cellXfs[5]요소 순서가 이 변형의 전부입니다. autoFilter 를 mergeCells 뒤에 쓰면 Excel 은 "복구했습니다"라며 필터를 버립니다. formatCode 안의 큰따옴표는 XML 속성 안이므로 " 로 이스케이프합니다.
String sheet = """
<worksheet xmlns="…/main"><sheetData>
<row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" s="1"/></row> <!-- B1: 서식만 있는 빈 셀 -->
<row r="2"><c r="A2"><f>SUM(B2:C2)</f><v>30</v></c></row> <!-- 수식: <f> 무시, <v> 만 -->
<row r="5"><c><v>1E+15</v></c><c t="str"><v>계산결과</v></c></row> <!-- 빈 행 3,4 없음. r 생략 셀 -->
<row r="6"><c r="A6" t="inlineStr"><is><r><t>굵게</t></r><r><t>보통</t></r></is></c></row> <!-- 리치 텍스트 -->
<row r="7"><c r="A7" t="e"><v>#DIV/0!</v></c></row> <!-- 오류값 -->
</sheetData></worksheet>""";
SimpleXlsxReader.parseSheet(new ByteArrayInputStream(sheet.getBytes(UTF_8)), List.of("공유0"), new boolean[]{false, false}, row ->
System.out.println(row.num() + " " + row.cells()));
// 출력:
// 1 [Cell[ref=A1, col=0, value=공유0, date=false]] ← B1 은 값이 없어 cells 에 없음
// 2 [Cell[ref=A2, col=0, value=30, date=false]]
// 5 [Cell[ref=A5, col=0, value=1E+15, date=false], Cell[ref=B5, col=1, value=계산결과, date=false]]
// 6 [Cell[ref=A6, col=0, value=굵게보통, date=false]] ← <r> 조각을 이어 붙임
// 7 [Cell[ref=A7, col=0, value=#DIV/0!, date=false]]parseSheet 를 public static 으로 열어 둔 이유가 이런 테스트입니다. 파일을 만들지 않고 XML 문자열만으로 파서를 검증합니다. 큰 수 1E+15 는 원문 그대로 오므로 숫자가 필요하면 new BigDecimal("1E+15") 로 파싱합니다. Long.parseLong 은 실패합니다.
for (int level : new int[]{0, 1, 6, 9}) {
long t0 = System.nanoTime();
try (var w = new SimpleXlsxWriter(Files.newOutputStream(out), "S", level)) { /* 5만 행 */ }
System.out.printf(" level %d: %,10d bytes %,5d ms%n", level, Files.size(out), (System.nanoTime() - t0) / 1_000_000);
}
// 출력(예, 5만 행 × 8열):
// level 0: 14,205,331 bytes 420 ms ← 무압축. XML 원문 크기
// level 1: 1,842,116 bytes 610 ms ← 87% 감소
// level 6: 1,702,904 bytes 980 ms ← 기본값. 레벨 1 보다 8% 더 작고 시간은 1.6배
// level 9: 1,688,220 bytes 1,750 ms ← 거의 이득 없음XLSX 의 XML 은 태그 반복이 많아 압축률이 극단적으로 높습니다. 다운로드 API 라면 레벨 1~3 이 응답 시간 대비 최적입니다. 레벨 9 는 CPU 만 태웁니다.