공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

6. 연습 문제

문제 1

예제 3 이 만든 data/orders.xlsx 를 읽어 고객별 금액 합계와 주문 건수를 구하고, data/orders_by_customer.xlsx 로 저장하세요. 헤더는 고객, 건수, 합계, 합계는 통화 서식, 마지막에 굵은 총합 행, 틀 고정. 금액은 #,##0 서식이 적용된 셀이므로 DataFormatter 결과에 쉼표가 있음을 주의하세요.

정답 보기
java
Map<String, long[]> byCustomer = new TreeMap<>();       // 고객 → {건수, 합계}
try (Workbook wb = WorkbookFactory.create(Path.of("data/orders.xlsx").toFile(), null, true)) {
    Sheet sheet = wb.getSheetAt(0);
    for (int r = 1; r <= sheet.getLastRowNum(); r++) {
        Row row = sheet.getRow(r);
        if (row == null) continue;
        String customer = row.getCell(2).getStringCellValue();
        long amount = (long) row.getCell(4).getNumericCellValue();   // NUMERIC 셀은 저장값을 직접
        long[] agg = byCustomer.computeIfAbsent(customer, k -> new long[2]);
        agg[0]++;
        agg[1] += amount;
    }
}

try (Workbook wb = new XSSFWorkbook()) {
    ExcelWriter.Styles s = ExcelWriter.Styles.of(wb);
    CellStyle totalMoney = wb.createCellStyle();
    totalMoney.cloneStyleFrom(s.money());
    totalMoney.setFont(wb.getFontAt(s.bold().getFontIndex()));
    Sheet sheet = wb.createSheet("고객별");
    ExcelWriter.writeHeader(sheet, 0, new String[]{"고객", "건수", "합계"}, s);
    int r = 1;
    long totalCount = 0, totalAmount = 0;
    for (Map.Entry<String, long[]> e : byCustomer.entrySet()) {
        Row row = sheet.createRow(r++);
        row.createCell(0).setCellValue(e.getKey());
        ExcelWriter.setValue(row.createCell(1), e.getValue()[0], s);
        Cell sum = row.createCell(2);
        sum.setCellValue(e.getValue()[1]);
        sum.setCellStyle(s.money());
        totalCount += e.getValue()[0];
        totalAmount += e.getValue()[1];
    }
    Row total = sheet.createRow(r);
    Cell label = total.createCell(0); label.setCellValue("총합"); label.setCellStyle(s.bold());
    ExcelWriter.setValue(total.createCell(1), totalCount, s);
    Cell sum = total.createCell(2); sum.setCellValue(totalAmount); sum.setCellStyle(totalMoney);
    sheet.createFreezePane(0, 1);
    for (int c = 0; c < 3; c++) sheet.setColumnWidth(c, 14 * 256);
    ExcelWriter.save(wb, Path.of("data/orders_by_customer.xlsx"));
}
// 출력(예): 고객0 4건 ... 총합 20건 1,0xx,000원

문제 2

MemberUploadValidator 에 전화번호 컬럼(6번째)을 추가하세요. 규칙: 필수, 숫자만 10~11자리, 파일 안 중복 금지. 사용자가 01012345678 을 숫자 셀로 입력하면 엑셀은 앞의 0 을 버리고 1012345678 로 저장합니다. 이 경우를 "앞자리 0 이 사라졌습니다.

셀 서식을 텍스트로 바꾸거나 템플릿을 사용하세요"라는 오류로 잡고, 템플릿(writeTemplate)의 전화번호 열에는 텍스트 서식(@)을 미리 적용하세요.

정답 보기
java
// 검증 (행 루프 안)
Cell phoneCell = row.getCell(5);
String phone = ExcelReader.text(row, 5, fmt, ev).replace("-", "");
if (phone.isEmpty()) {
    errors.add(new RowError(excelRow, "전화번호", "", "필수값입니다"));
} else if (phoneCell != null && phoneCell.getCellType() == CellType.NUMERIC) {
    errors.add(new RowError(excelRow, "전화번호", phone,
            "앞자리 0 이 사라졌습니다. 셀 서식을 텍스트로 바꾸거나 템플릿을 사용하세요"));
} else if (!phone.matches("\\d{10,11}")) {
    errors.add(new RowError(excelRow, "전화번호", phone, "숫자 10~11자리여야 합니다"));
} else {
    Integer first = seenPhone.putIfAbsent(phone, excelRow);
    if (first != null) errors.add(new RowError(excelRow, "전화번호", phone, first + "행과 중복"));
}

// 템플릿: 전화번호 열(5) 2~1001행에 텍스트 서식을 미리 적용
CellStyle textStyle = s.text();                                   // DataFormat "@"
for (int r = 1; r <= 1000; r++) {
    Row row = sheet.getRow(r) == null ? sheet.createRow(r) : sheet.getRow(r);
    Cell c = row.getCell(5) == null ? row.createCell(5) : row.getCell(5);
    c.setCellStyle(textStyle);
}
// 더 가벼운 방법: 열 기본 스타일
sheet.setDefaultColumnStyle(5, textStyle);

숫자 셀은 NUMERIC 타입으로 오고 DataFormatter 결과는 1012345678(10자리)이라 형식 검사만으로는 통과해 버립니다. 그래서 타입 검사를 형식 검사보다 먼저 둡니다. setDefaultColumnStyle 은 열 전체의 기본 서식을 지정하므로 셀을 미리 만들 필요가 없습니다.

문제 3

SXSSFWorkbook 으로 250만 행짜리 주문 목록을 내려주는 메서드를 작성하세요. 시트 하나의 한계는 1,048,576 행이므로 100만 행마다 새 시트(orders_1, orders_2, ...)를 만들고 각 시트에 헤더를 다시 씁니다. 행 공급은 Iterator<Object[]> 로 받고, 힙이 일정하게 유지되도록 하세요. 창 크기 500, 임시 파일 압축 사용.

정답 보기
java
static Path exportLarge(Path out, String[] header, Iterator<Object[]> rows) throws IOException {
    final int ROWS_PER_SHEET = 1_000_000;                          // 1,048,576 보다 여유 있게
    try (SXSSFWorkbook wb = new SXSSFWorkbook(500)) {
        wb.setCompressTempFiles(true);                              // 임시 XML 을 gzip
        ExcelWriter.Styles s = ExcelWriter.Styles.of(wb);          // 워크북당 1회
        Sheet sheet = null;
        int sheetNo = 0, r = 0;
        while (rows.hasNext()) {
            if (sheet == null || r > ROWS_PER_SHEET) {              // 시트 전환
                sheet = wb.createSheet("orders_" + (++sheetNo));
                ExcelWriter.writeHeader(sheet, 0, header, s);
                for (int c = 0; c < header.length; c++) sheet.setColumnWidth(c, 16 * 256);
                sheet.createFreezePane(0, 1);
                r = 1;
            }
            Object[] values = rows.next();
            Row row = sheet.createRow(r++);
            for (int c = 0; c < values.length; c++) ExcelWriter.setValue(row.createCell(c), values[c], s);
        }
        ExcelWriter.save(wb, out);
    }                                                               // close: 임시 파일 삭제
    return out;
}

// 호출 (테스트용 공급자)
Iterator<Object[]> supplier = IntStream.rangeClosed(1, 2_500_000)
        .mapToObj(i -> new Object[]{"ORD-" + i, LocalDate.of(2026, 9, 1), "고객" + (i % 1000), i % 10, (long) i * 100})
        .iterator();
exportLarge(Path.of("data/orders_large.xlsx"), new String[]{"주문번호", "주문일", "고객", "수량", "금액"}, supplier);
// 출력(예): orders_1 ~ orders_3 시트, 힙 증가 수 MB 유지, 파일 약 70MB

IntStream.iterator() 는 지연 평가이므로 250만 개 배열이 동시에 존재하지 않습니다. 실제 서비스에서는 DB 커서(ResultSet 또는 JPA Stream)를 Iterator 로 감싸 넘깁니다. 시트 전환 조건을 r > ROWS_PER_SHEET 로 두어 헤더 행을 포함해도 한계를 넘지 않게 했습니다.

연습 문제
  • 문제 1
  • 문제 2
  • 문제 3
이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리