포스트

Apache POI로 엑셀 파일 읽기 — Workbook/Sheet 유틸

InputStream에서 Workbook을 만들고 Sheet의 각 행을 헤더 키 기반 Map으로 변환하는 ExcelUtils

Apache POI로 엑셀 파일 읽기 — Workbook/Sheet 유틸

업로드된 엑셀을 InputStream에서 받아 헤더 키 기반의 List<Map<String, Object>>로 다루는 유틸 패턴.

의존성

1
2
implementation group: 'org.apache.poi', name: 'poi', version: '4.1.2'
implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '4.1.2'

.xlsxpoi-ooxml이, .xlspoi가 처리한다.

ExcelUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class ExcelUtils {
    private static final Logger log = LoggerFactory.getLogger(ExcelUtils.class);

    public static Workbook getWorkbookFromStream(InputStream targetStream, String extension) throws Exception {
        checkExcelExtension(extension);

        if (extension.equals("xlsx")) return new XSSFWorkbook(targetStream);
        if (extension.equals("xls"))  return new HSSFWorkbook(targetStream);
        return null;
    }

    private static void checkExcelExtension(String extension) throws Exception {
        if (!extension.equals("xlsx") && !extension.equals("xls")) {
            throw new IOException("엑셀 아닙니다.");
        }
    }

    public static List<Map<String, Object>> readSheet(Workbook workbook, String sheetName) {
        Sheet sheet = Optional.ofNullable(workbook.getSheet(sheetName))
            .orElse(workbook.getSheetAt(0));

        Row header = sheet.getRow(ExcelConstants.HEADER_ROW_NUMBER);
        List<Map<String, Object>> result = new ArrayList<>();

        for (int rowIndex = 2; rowIndex < sheet.getLastRowNum() + 1; rowIndex++) {
            Row row = sheet.getRow(rowIndex);
            if (isNotEmptyCell(row.getCell(0))) {
                Map<String, Object> keymapfiedRow = getHeaderRowWeaved(header, row);
                keymapfiedRow.put("excelRowNum", rowIndex);
                result.add(keymapfiedRow);
            }
        }
        return result;
    }

    private static LinkedHashMap<String, Object> getHeaderRowWeaved(Row header, Row row) {
        LinkedHashMap<String, Object> result = new LinkedHashMap<>();
        for (int columnIndex = 0; columnIndex <= header.getLastCellNum(); columnIndex++) {
            Cell headerCell = header.getCell(columnIndex);
            if (isNotEmptyCell(headerCell)) {
                Cell cell = row.getCell(columnIndex);
                result.put((String) readCell(headerCell), readCell(cell));
            }
        }
        return result;
    }

    private static boolean isNotEmptyCell(Cell cell) {
        return cell != null && !cell.toString().isBlank();
    }

    private static Object readCell(Cell cell) {
        if (cell == null) return null;

        if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
            return cell.getDateCellValue().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        }

        switch (cell.getCellType()) {
            case FORMULA:
                return cell.getCachedFormulaResultType() == CellType.NUMERIC
                    ? cell.getNumericCellValue()
                    : cell.getStringCellValue();
            case NUMERIC: return cell.getNumericCellValue();
            case BOOLEAN: return cell.getBooleanCellValue();
            case ERROR:   return cell.getErrorCellValue();
            case BLANK:   return null;
            default:      return cell.getStringCellValue();
        }
    }
}

사용 패턴

1
2
3
4
5
try (InputStream is = file.getInputStream()) {
    Workbook wb = ExcelUtils.getWorkbookFromStream(is, "xlsx");
    List<Map<String, Object>> rows = ExcelUtils.readSheet(wb, "Sheet1");
    rows.forEach(row -> /* 도메인 객체로 매핑 */);
}

데이터 시작 행이 2로 고정돼 있는 등 헤더 컨벤션이 박혀 있으니, 실제 양식에 맞춰 HEADER_ROW_NUMBER와 시작 인덱스를 조정해서 쓴다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.