공공부하자개발 · 영어 학습 노트
자바
실무 확장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 공부하자
홈 › 실무 확장 › 02 / 22

Apache POI로 Excel 업로드/다운로드

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

5. 자주 하는 실수 (Tip)

❌ 실수 1: for-each 로 행을 돌면서 카운터로 행 번호를 만든다

java
int rowNum = 1;
for (Row row : sheet) {                       // 빈 행은 건너뜀
    rowNum++;
    if (invalid(row)) errors.add(rowNum + "행 오류");   // 빈 행이 있으면 번호가 어긋남
}

사용자가 5행을 지우고 올리면 6행 이후의 번호가 하나씩 밀려 "12행 오류"가 실제로는 13행입니다. 사용자는 12행을 아무리 봐도 문제를 못 찾습니다.

✅ 인덱스 루프로 null 행을 직접 다루거나, row.getRowNum() + 1 을 씁니다.

java
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
    Row row = sheet.getRow(r);                            // null 가능
    if (ExcelReader.isBlankRow(row, fmt, ev)) continue;
    int excelRow = r + 1;                                 // 항상 실제 행 번호
    ...
}

❌ 실수 2: 타입을 보지 않고 getStringCellValue / getNumericCellValue 를 부른다

java
String name = row.getCell(0).getStringCellValue();       // 셀이 null 이면 NPE
int age = (int) row.getCell(2).getNumericCellValue();    // "30" 문자열이면 IllegalStateException

Cannot get a NUMERIC value from a STRING cell 은 POI 업로드 기능의 대표 예외입니다. 담당자가 다른 시스템에서 내보낸 파일은 숫자가 문자열로 들어 있는 경우가 흔합니다.

✅ null 검사 + DataFormatter 로 문자열 통일 후 우리 규칙으로 파싱, 또는 타입 분기.

java
String ageText = ExcelReader.text(row, 2, fmt, ev);      // null 안전, "30" 또는 "30" (숫자 30.0 → "30")
int age = Integer.parseInt(ageText);                     // 실패하면 NumberFormatException → 에러 리포트

❌ 실수 3: 셀마다 createCellStyle

java
for (Row row : rows) {
    Cell c = row.createCell(4);
    CellStyle money = wb.createCellStyle();               // 행마다 새 스타일
    money.setDataFormat(wb.createDataFormat().getFormat("#,##0"));
    c.setCellStyle(money);
}

1만 행이면 1만 스타일, 6.4만 행을 넘으면 엑셀이 "파일이 손상되어 복구했습니다"를 띄우고 서식을 버립니다. 그 전에도 파일이 비대해지고 열리는 데 오래 걸립니다.

✅ 워크북당 한 번 만들고 재사용합니다. 변형이 필요하면 cloneStyleFrom.

java
ExcelWriter.Styles s = ExcelWriter.Styles.of(wb);        // 루프 밖에서 1회
for (Row row : rows) row.createCell(4).setCellStyle(s.integer());

❌ 실수 4: 대용량 다운로드를 XSSFWorkbook 으로 만든다

java
XSSFWorkbook wb = new XSSFWorkbook();
for (Order o : orderRepository.findAll()) {               // 50만 건
    ...
}
wb.write(response.getOutputStream());

50만 행 × 8열이면 힙 2GB 이상입니다. 요청 두 개가 겹치면 OutOfMemoryError 로 서버 전체가 죽습니다. 조회도 findAll 로 전부 메모리에 올려 두 배로 위험합니다.

✅ SXSSFWorkbook + 커서/페이징 조회. 창 크기는 100~1000 이면 충분합니다.

java
try (SXSSFWorkbook wb = new SXSSFWorkbook(200)) {
    Sheet sheet = wb.createSheet();
    Styles s = Styles.of(wb);
    int r = 0;
    try (Stream<Order> orders = orderRepository.streamAll()) {          // 커서 기반 스트림
        for (Iterator<Order> it = orders.iterator(); it.hasNext(); ) writeOrder(sheet.createRow(++r), it.next(), s);
    }
    wb.write(out);
}                                                                        // close 가 임시 파일 정리

❌ 실수 5: SXSSF 에서 flush 된 행을 다시 만진다

java
try (SXSSFWorkbook wb = new SXSSFWorkbook(100)) {
    Sheet sheet = wb.createSheet();
    for (int r = 1; r <= 10_000; r++) sheet.createRow(r)...;
    sheet.getRow(0).createCell(5).setCellValue("합계: " + total);    // getRow(0) == null → NPE
    sheet.autoSizeColumn(0);                                          // 추적 안 하면 IllegalStateException
}

100행 창을 넘긴 행은 이미 임시 파일에 XML 로 쓰였습니다. 되돌아갈 수 없습니다.

✅ 위에 써야 할 값은 미리 계산해 먼저 쓰고, 자동 너비는 trackAllColumnsForAutoSizing() 후 창 안에서 호출하거나 고정 너비를 씁니다.

java
Row header = sheet.createRow(0);                     // 합계를 먼저 알 수 있으면 먼저 쓴다
header.createCell(5).setCellValue("합계: " + precomputedTotal);
sheet.trackAllColumnsForAutoSizing();                // 또는 setColumnWidth 고정

❌ 실수 6: 수식 셀을 평가하지 않고 저장하거나 읽는다

java
cell.setCellFormula("SUM(E3:E12)");
wb.write(out);                                        // 캐시 값 없음 → 미리보기/다른 도구에서 0 또는 빈 칸
...
String v = new DataFormatter().formatCellValue(cell); // "SUM(E3:E12)" 수식 문자열이 그대로

엑셀 데스크톱은 열 때 재계산하니 "내 PC 에선 되는데" 상태가 됩니다. 메일 미리보기, 모바일, Google Sheets 가져오기, 다른 서버의 POI 는 캐시 값을 봅니다.

✅ 저장 전 evaluateAll(), 읽을 때 formatCellValue(cell, evaluator).

java
wb.getCreationHelper().createFormulaEvaluator().evaluateAll();
wb.write(out);
...
FormulaEvaluator ev = wb.getCreationHelper().createFormulaEvaluator();
String v = fmt.formatCellValue(cell, ev);             // "14,185,000원"

❌ 실수 7: 워크북을 닫지 않거나 쓰기 모드로 연다

java
Workbook wb = WorkbookFactory.create(new File("upload.xlsx"));    // readOnly=false, close 없음
List<Member> members = parse(wb);
Files.delete(uploadPath);                                         // Windows: 파일이 잠겨 삭제 실패

File 로 연 워크북은 파일 핸들을 잡고 있습니다. 쓰기 모드면 close() 시 파일을 다시 쓰려고까지 합니다. SXSSF 는 임시 파일이 /tmp 에 쌓입니다.

✅ 항상 try-with-resources, 읽기는 readOnly=true.

java
try (Workbook wb = WorkbookFactory.create(uploadPath.toFile(), null, true)) {
    return parse(wb);
}                                                                  // 핸들 반납 후 삭제 가능
자주 하는 실수 (Tip)
  • ❌ 실수 1: for-each 로 행을 돌면서 카운터로 행 번호를 만든다
  • ❌ 실수 2: 타입을 보지 않고 getStringCellValue / getNumericCellValue 를 부른다
  • ❌ 실수 3: 셀마다 createCellStyle
  • ❌ 실수 4: 대용량 다운로드를 XSSFWorkbook 으로 만든다
  • ❌ 실수 5: SXSSF 에서 flush 된 행을 다시 만진다
  • ❌ 실수 6: 수식 셀을 평가하지 않고 저장하거나 읽는다
  • ❌ 실수 7: 워크북을 닫지 않거나 쓰기 모드로 연다
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제