공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 01 / 22

Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기

섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

4. 응용 변형 예제

변형 1: inlineStr vs sharedStrings — 크기·시간·메모리 실측

powershell
java V1SharedStringsVsInline 200000
java
// 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).

변형 2: 틀 고정 · 자동 필터 · 병합 셀 · 사용자 정의 숫자 서식

java
// 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=\"&quot;₩&quot;#,##0\"/></numFmts>"
"<xf numFmtId=\"164\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\"/>"   // cellXfs[5]

요소 순서가 이 변형의 전부입니다. autoFilter 를 mergeCells 뒤에 쓰면 Excel 은 "복구했습니다"라며 필터를 버립니다. formatCode 안의 큰따옴표는 XML 속성 안이므로 &quot; 로 이스케이프합니다.

변형 3: 실제 Excel/다른 도구가 만든 시트의 엣지 케이스

java
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 은 실패합니다.

변형 4: ZipOutputStream 압축 레벨

java
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 만 태웁니다.

응용 변형 예제
  • 변형 1: inlineStr vs sharedStrings — 크기·시간·메모리 실측
  • 변형 2: 틀 고정 · 자동 필터 · 병합 셀 · 사용자 정의 숫자 서식
  • 변형 3: 실제 Excel/다른 도구가 만든 시트의 엣지 케이스
  • 변형 4: ZipOutputStream 압축 레벨
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)