IT 일기장

[Java] POI로 SXSSFWorkbook 엑셀 다운로드 구현 본문

프로그래밍 언어/Java

[Java] POI로 SXSSFWorkbook 엑셀 다운로드 구현

뽕슈 2024. 8. 27. 11:16
반응형

엑셀 다운로드를 구현했을 때 아래 링크를 참고하여 만들었다.

https://techblog.woowahan.com/2698/ 

 

아 엑셀다운로드 개발,,, 쉽고 빠르게 하고 싶다 (feat. 엑셀 다운로드 모듈 개발기) | 우아한형제

실제로는 적절한 이름을 가진 private method로 코드가 나누어져 있습니다. @NoArgsConstructor(access = AccessLevel.PRIVATE) public ExcelRenders { public static createCellStyle(Workbook wb, ColumnType columnType) { XSSFCellStyle cellStyl

techblog.woowahan.com

 

 

현재 Maven Project를 사용하고 있으며 poi 버전은 5.2.5를 사용했다.

 

 

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.5</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

 

 

전자정부 프레임워크를 사용하여 엑셀 다운로드를 테스트해 보며 항상 느꼈던 게 다운로드 시 속도가 느린 것이었다.

데이터가 몇 천 개만 돼도 다운로드하는데 뭐 이렇게 느리나 싶어서 짧은 지식으로 여기저기 구글링을 해보니 아주 좋은 글을 발견했다. 

 

1. 우선 엑셀 다운로드를 클릭할 버튼을 만들어준다. (NobleUI를 사용하여 만든 버튼이다.)

<button class="btn btn-primary btn-xs btn-icon-text" onclick="location.href='/system/excel/select'">
    <i class="btn-icon-prepend" data-feather="plus-square"></i>엑셀 다운로드
</button>

 

 

2. 버튼 클릭했을 때 동작할 Controller를 만들어준다. selectTestList는 단순한 SELECT 쿼리다.

@GetMapping("select")
public void selectListExcel(HttpServletRequest request, HttpServletResponse response) throws Exception {
    List<LinkedHashMap<String, Object>> rtnList = null;
    rtnList = excelService.selectTestList();
    SimpleExcelGenerator excelFile = new SimpleExcelGenerator(rtnList);
    excelFile.write(response, response.getOutputStream());
}

 

<select id="selectTestList" resultType="java.util.LinkedHashMap">
    SELECT 	ssaId AS '아이디',
            ssaName AS '이름',
            ssaAuth AS '권한',
            ssaDept AS '부서',
            ssaPosition AS '직위',
            ssaState AS '상태',
            ssaRegId AS '등록자',
            ssaRegDt AS '등록일'
    FROM test
    ORDER BY ssaRegDt DESC
</select>

 

 

3. 실질적인 엑셀 다운로드 로직은 SimpleExcelGenerator에 있는데 Excel2007 기준으로 구현했으며 생성자에는 행 개수체크와 칼럼명 가져오기, 그리고 렌더링 역할을 해줄 것이다.

 

private static final SpreadsheetVersion supplyExcelVersion = SpreadsheetVersion.EXCEL2007;
private static final int rowIndex = 0;
private static final int cellIndex = 0;

private SXSSFWorkbook sxssWorkbook;
private SXSSFSheet sxssSheet;
private String[] cellData;

public SimpleExcelGenerator(List<LinkedHashMap<String,Object>> data){
    validateMaxRow(data);
    this.sxssWorkbook = new SXSSFWorkbook();
    if (!data.isEmpty()) {
        this.cellData = StringUtil.extractColumnNames(data.get(0));
    } else {
        this.cellData = new String[0];
    }
    renderExcel(data);
}

 

 

4. renderExcel 함수는 다음과 같다. 편의상 Sheet명은 example로 설정했다.

 

private void renderExcel(List<LinkedHashMap<String,Object>> data){

    sxssSheet = sxssWorkbook.createSheet("example");

    renderHeaders(rowIndex, cellIndex);

    if (data.isEmpty())
        return;

    int rowIdx = rowIndex + 1;

    renderBody(data, rowIdx++);
}

 

 

5. 엑셀의 헤더를 만드는 부분인 renderHeaders 함수 부분이다. 

 

private void renderHeaders(int rowIdx, int cellIdx){

    SXSSFRow headerRow = sxssSheet.createRow(rowIdx++);

    CellStyle greyCellStyle = sxssWorkbook.createCellStyle();
    ExcelUtil.applyExcelCellStyle(greyCellStyle, new Color(231, 234, 236));

    for (int i = 0; i < cellData.length; i++) {
        sxssSheet.setColumnWidth(i, (sxssSheet.getColumnWidth(i))+1024);
    }

    for (int i = 0; i < cellData.length; i++) {

        SXSSFCell headerCell = headerRow.createCell(cellIdx++);
        headerCell.setCellValue(cellData[i]);
        headerCell.setCellStyle(greyCellStyle);
    }
}

 

 

6. 엑셀의 데이터를 출력할 부분인 renderBody 함수 부분이다. 숫자인 경우에는 3자리 단위로 ,를 붙여주고 날짜인 경우에는 yyyy-MM-dd으로 출력하도록 분기 처리를 해줬다. ExcelUtil에서 사용한 numberFormat 함수와 dateFormat 함수는 다음과 같다.

 

private void renderBody(List<LinkedHashMap<String,Object>> data, int rowIdx){

    CellStyle numberStyle = ExcelUtil.numberFormat(sxssWorkbook);
    CellStyle dateStyle = ExcelUtil.dateFormat(sxssWorkbook);
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    for (Map<String,Object> map : data) {
        SXSSFRow sxssDataRow = sxssSheet.createRow(rowIdx++);
        for (int i = 0; i < cellData.length; i++) {

            SXSSFCell sxssDataCell = sxssDataRow.createCell(i);

            Object value = map.get(cellData[i]);

            // 숫자인 경우, 날짜인 경우 분기처리
            if (value != null && ObjectUtil.isNumeric(value.toString())) {
                sxssDataCell.setCellType(CellType.NUMERIC);
                sxssDataCell.setCellValue(Double.parseDouble(value.toString()));
                sxssDataCell.setCellStyle(numberStyle);
            } else if (value != null && ObjectUtil.isDate(value.toString())) {
                try {
                    sxssDataCell.setCellType(CellType.NUMERIC);
                    sxssDataCell.setCellValue(dateFormat.parse(value.toString()));
                    sxssDataCell.setCellStyle(dateStyle);
                } catch (ParseException e) {
                    sxssDataCell.setCellType(CellType.STRING);
                    sxssDataCell.setCellValue(value.toString());
                }
            } else {
                sxssDataCell.setCellType(CellType.STRING);
                sxssDataCell.setCellValue(StringUtil.isNull(value, ""));
            }

        }
    }
}

 

 

public static CellStyle numberFormat(SXSSFWorkbook sxssWorkbook) {
    CellStyle cellStyle = sxssWorkbook.createCellStyle();
    DataFormat dataFormat = sxssWorkbook.createDataFormat();
    cellStyle.setDataFormat(dataFormat.getFormat("#,##0"));

    return cellStyle;
}

public static CellStyle dateFormat(SXSSFWorkbook sxssWorkbook) {
    CellStyle cellStyle = sxssWorkbook.createCellStyle();
    DataFormat dataFormat = sxssWorkbook.createDataFormat();
    cellStyle.setDataFormat(dataFormat.getFormat("yyyy-MM-dd"));

    return cellStyle;
}

 

 

7. 마지막으로 write 함수다. 엑셀 파일명 또한 편의상 example로 표현했다.

 

public void write(HttpServletResponse response,OutputStream stream) throws IOException{

    response.setHeader("Set-Cookie", "fileDownload=true; path=/");
    response.setHeader("Content-Disposition", "attachment;filename=example.xlsx");

    sxssWorkbook.write(stream);
    sxssWorkbook.close();
    sxssWorkbook.dispose();
    stream.close();
}

 

 

8.  전체 SimpleExcelGenerator 소스코드는 다음과 같다.

 

package com.bizpack.excel;

import java.awt.Color;
import java.io.IOException;
import java.io.OutputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import com.bizpack.util.ExcelUtil;
import com.bizpack.util.ObjectUtil;
import com.bizpack.util.StringUtil;

import jakarta.servlet.http.HttpServletResponse;

public class SimpleExcelGenerator {

    private static final SpreadsheetVersion supplyExcelVersion = SpreadsheetVersion.EXCEL2007;
    private static final int rowIndex = 0;
    private static final int cellIndex = 0;

    private SXSSFWorkbook sxssWorkbook;
    private SXSSFSheet sxssSheet;
    private String[] cellData;

    public SimpleExcelGenerator(List<LinkedHashMap<String,Object>> data){
    	validateMaxRow(data);
        this.sxssWorkbook = new SXSSFWorkbook();
        if (!data.isEmpty()) {
            this.cellData = StringUtil.extractColumnNames(data.get(0));
        } else {
            this.cellData = new String[0];
        }
        renderExcel(data);
    }

    private void validateMaxRow(List<LinkedHashMap<String,Object>> data){

    	int maxRows = supplyExcelVersion.getMaxRows();
        if (data.size() > maxRows)
            throw new IllegalArgumentException(String.format("현재 Excel 버전은 %s 행 이상 데이터를 지원하지 않습니다.", maxRows));
    }

    private void renderExcel(List<LinkedHashMap<String,Object>> data){

        sxssSheet = sxssWorkbook.createSheet("example");

        renderHeaders(rowIndex, cellIndex);

        if (data.isEmpty())
            return;

        int rowIdx = rowIndex + 1;

        renderBody(data, rowIdx++);
    }

    private void renderHeaders(int rowIdx, int cellIdx){

    	SXSSFRow headerRow = sxssSheet.createRow(rowIdx++);

    	CellStyle greyCellStyle = sxssWorkbook.createCellStyle();
    	ExcelUtil.applyExcelCellStyle(greyCellStyle, new Color(231, 234, 236));

        for (int i = 0; i < cellData.length; i++) {
        	sxssSheet.setColumnWidth(i, (sxssSheet.getColumnWidth(i))+1024);
		}

        for (int i = 0; i < cellData.length; i++) {

        	SXSSFCell headerCell = headerRow.createCell(cellIdx++);
			headerCell.setCellValue(cellData[i]);
			headerCell.setCellStyle(greyCellStyle);
		}
    }

    private void renderBody(List<LinkedHashMap<String,Object>> data, int rowIdx){

    	CellStyle numberStyle = ExcelUtil.numberFormat(sxssWorkbook);
    	CellStyle dateStyle = ExcelUtil.dateFormat(sxssWorkbook);
    	SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    	for (Map<String,Object> map : data) {
			SXSSFRow sxssDataRow = sxssSheet.createRow(rowIdx++);
			for (int i = 0; i < cellData.length; i++) {

				SXSSFCell sxssDataCell = sxssDataRow.createCell(i);

				Object value = map.get(cellData[i]);

				// 숫자인 경우, 날짜인 경우 분기처리
                if (value != null && ObjectUtil.isNumeric(value.toString())) {
                    sxssDataCell.setCellType(CellType.NUMERIC);
                    sxssDataCell.setCellValue(Double.parseDouble(value.toString()));
                    sxssDataCell.setCellStyle(numberStyle);
                } else if (value != null && ObjectUtil.isDate(value.toString())) {
                    try {
                        sxssDataCell.setCellType(CellType.NUMERIC);
                        sxssDataCell.setCellValue(dateFormat.parse(value.toString()));
                        sxssDataCell.setCellStyle(dateStyle);
                    } catch (ParseException e) {
                        sxssDataCell.setCellType(CellType.STRING);
                        sxssDataCell.setCellValue(value.toString());
                    }
                } else {
                    sxssDataCell.setCellType(CellType.STRING);
                    sxssDataCell.setCellValue(StringUtil.isNull(value, ""));
                }

			}
		}
    }

    public void write(HttpServletResponse response,OutputStream stream) throws IOException{

    	response.setHeader("Set-Cookie", "fileDownload=true; path=/");
		response.setHeader("Content-Disposition", "attachment;filename=example.xlsx");

    	sxssWorkbook.write(stream);
    	sxssWorkbook.close();
    	sxssWorkbook.dispose();
        stream.close();
    }
}

 

 

출처 글은 DTO로 구현했다면 나는 HashMap으로 구현했다.

나에게 숙제로 남은 건 고객의 요청사항 중 엑셀 Header가 병합된 경우도 따져봐야 한다는 것..

DB에 10만 개 데이터를 넣어봤을 때 다운로드 속도는 약 3-4초 정도 소요됐다.

반응형
Comments