w.row("김철수", "2026-01-05"); // ❌ 문자열 셀. Excel 에서 정렬·필터·수식 불가
w.row("김철수", LocalDate.of(2026, 1, 5)); // ✅ 숫자 + 날짜 스타일. Excel 이 날짜로 인식문자열 "2026-01-05" 는 Excel 에서 왼쪽 정렬된 텍스트로 보이고 =B2+30 같은 수식이 #VALUE! 가 됩니다. 반대로 읽을 때 사용자가 날짜를 텍스트로 입력했다면 Cell.date() 가 false 이므로 검증 오류로 잡아야 합니다(예제 5 의 7행).
sb.append("<v>").append(12345678.0).append("</v>"); // ❌ "1.2345678E7" → Excel 이 열긴 하지만 일부 도구가 거부
sb.append("<v>").append(BigDecimal.valueOf(12345678.0).toPlainString()); // ✅ "12345678"Double.toString() 은 10^7 이상이나 10^-3 미만에서 지수 표기를 씁니다. 금액 계산은 처음부터 long(원 단위) 또는 BigDecimal 로 하고 double 을 셀에 쓰지 않는 것이 근본 해결입니다.
int expected = 1;
r.forEachRow(0, row -> {
if (row.num() != expected++) throw new IllegalStateException("행 누락"); // ❌ 사용자가 3행을 지웠으면 r=2 다음 r=4
});
// ✅ row.num() 을 그대로 사용자에게 보여 주는 행 번호로 쓴다. 연속성을 가정하지 않는다Excel 에서 행을 지우면(내용만 지우면) <row> 자체가 저장되지 않습니다. 서식이 남은 경우에는 <row r="3"><c r="A3" s="1"/></row> 처럼 값 없는 셀만 있는 행이 오므로 row.isBlank() 로 걸러야 합니다.
String phone = row.cells().get(2).value(); // ❌ B 열이 비어 있으면 cells 는 [A, C, D] 라 C 가 1번에 온다
String phone = row.get(2); // ✅ 열 인덱스로 찾는다. 없으면 ""cells 는 파일에 있는 셀만 담고, 빈 셀은 <c> 가 없습니다. 반드시 Cell.col() 기준으로 찾아야 합니다. Row.get(col), Row.cell(col), Row.values() 가 그 일을 합니다.
"<t>" + name + "</t>" // ❌ 이름이 "A&B <주식회사>" 면 XML 이 깨져 Excel 이 "손상" 판정
"<t>" + XlsxParts.escape(name) + "</t>" // ✅ & < > " 치환 + 제어 문자(0x00~0x1F, 탭·개행 제외) 제거특히 DB 에서 온 문자열에 \u0000 같은 제어 문자가 섞여 있으면 이스케이프해도 Excel 이 거부합니다. escape() 가 제어 문자를 제거하는 이유입니다. 시트 이름도 규칙이 있습니다(1~31자, []:*?/\ 금지). 작성기 생성자가 검사합니다.
try (var out = new BufferedWriter(new OutputStreamWriter(zip))) { … } // ❌ out.close() → zip.close(). 뒤에 부품을 못 붙인다
out.flush(); zip.closeEntry(); // ✅ 시트 엔트리만 끝내고 다음 엔트리로Writer.close() 는 감싼 스트림을 연쇄로 닫습니다. ZipOutputStream 위에 Writer 를 올렸을 때는 엔트리 전환 시 flush() 만 하고, 최종 close() 는 zip 에 한 번만 합니다.
XMLInputFactory.newFactory().createXMLStreamReader(in); // ❌ DTD·외부 엔티티 허용 → XXE 로 서버 파일 유출
xmlFactory().createXMLStreamReader(in); // ✅ SUPPORT_DTD=false, IS_SUPPORTING_EXTERNAL_ENTITIES=falseXLSX 는 정상적으로는 DTD 를 쓰지 않으므로 끄는 것에 부작용이 없습니다. 함께 zip 엔트리 크기 상한과 행 수 상한을 두어 zip 폭탄과 무한 행을 막습니다.