Excel 다운로드

Yeeun_Kim·2023년 7월 4일
0
post-thumbnail

기존 대량 엑섹 다운로드 방식은 POI의 XSSF를 사용하고 있었다. 하지만 XSSF 방식에서 몇 만건의 데이터를 다운로드 할 경우 많은 시간이 소요되어 기존의 XSSF 방식의 다운로드 방식에서 SXSSF 방식의 엑셀 다운로드 할 수 있도록 구현하였다.

HSSF, XSSF, SXSSF 란?

  • HSSF : Excel 2007 하위버전(.xls) 파일 포맷을 사용할 때 사용
  • XSSF : Excel 2007 (.xlsx) 파일 포맷을 사용할 때 사용
  • SXSSF : 대용량 엑셀 파일을 출력할 때 사용
※SXSSF는 임시파일을 중간에 생성하여 메모리를 적게 사용하기 때문에 XSSF보다 큰 엑셀 파일 생성이 가능하다.

엑셀 다운로드 기능을 공통화 할 수 있도록 개발 작업을 진행하였습니다.
먼저 ExcelFile이라는 인터페이스를 하나 만들어 추후에 ExcelFile의 여러가지 형태가 나오더라도 공통 기능을 정의할 수 있도록 만들었습니다.

ExcelFile.java

public interface ExcelFile {
    void write(OutputStream stream) throws IOException;
}

엑셀 다운로드 기능을 모두 공통적으로 가지는 엑셀 파일 객체들을 만들 것이기 때문에 이렇게 write기능만을 추가하였습니다.

BulkExcelFile.java

public class BulkExcelFile implements ExcelFile{
    private static final SpreadsheetVersion supplyExcelVersion = SpreadsheetVersion.EXCEL2007;

    public final SXSSFWorkbook wb;
    private final Sheet sheet;

    private final ExcelStyle excelStyle;

    //단일 시트 대량 엑셀 파일 객체 생성
    public BulkExcelFile(String fileName, List<?> data) {
        validateMaxRows(data);
        this.wb = new SXSSFWorkbook();
        wb.setCompressTempFiles(true);
        this.sheet = this.wb.createSheet(fileName);
        this.excelStyle = new ExcelStyle(wb);
    }

    private void validateMaxRows(List<?> data){
        int maxRows = supplyExcelVersion.getMaxRows();
        if(data.size() > maxRows){
            throw new IllegalArgumentException("최대 처리건수가 초과되었습니다.");
        }
    }

    protected void setHeaders(int rowIndex, int columnStartIndex, List<String> fieldName){
        Row row = sheet.createRow(rowIndex);
        int columnIndex = columnStartIndex;

        for(String headerName : fieldName){
            Cell cell = row.createCell(columnIndex++);
            cell.setCellStyle(excelStyle.headerGrayCellStyle);
            cell.setCellValue(headerName);
        }
    }

    protected void setBody(String[] columns, Object data, int rowIndex, int columnStartIndex) {
        Row row = sheet.createRow(rowIndex);
        int columnIndex = columnStartIndex;

        for (String name : columns) {
            Cell cell = row.createCell(columnIndex++);
            try {
                if(!name.equals("no")){ //순번 Column이 아닐경우
                    Field field = data.getClass().getDeclaredField(name);
                    field.setAccessible(true);
                    renderCellValue(cell, field.get(data));
                } else {
                    renderCellValue(cell, String.valueOf(rowIndex));
                }
            } catch (Exception e) {
                throw new IllegalArgumentException();
            }
        }
    }

    private void renderCellValue(Cell cell, Object cellVal) {
        if (cellValue instanceof Number) {
            Number numberValue = (Number) cellVal;
            cell.setCellStyle(excelStyle.bodyNumCellStyle);
            cell.setCellValue(numberValue.doubleValue());
            return;
        }
        cell.setCellStyle(excelStyle.bodyStrCellStyle);
        cell.setCellValue(cellVal == null ? "" : cellVal.toString());
    }

    @Override
    public void write(OutputStream stream) throws IOException {
        wb.write(stream);
        wb.close();
        stream.close();
    }
}

ExcelStyle.java

public class ExcelStyle {

    protected final CellStyle headerGrayCellStyle;
    protected final CellStyle bodyNumCellStyle;
    protected final CellStyle bodyStrCellStyle;

    public ExcelStyle(Workbook wb){
        this.headerGrayCellStyle = makeHeaderGrayCellStyle(wb);
        this.bodyNumCellStyle = makeBodyCellStyle(wb, true);
        this.bodyStrCellStyle = makeBodyCellStyle(wb, false);
    }

    public static void setCellStyle(CellStyle cellStyle, Color color,
                                    HorizontalAlignment horizAlign, Font font) {
        XSSFCellStyle style = (XSSFCellStyle) cellStyle;
        //배경색 지정
        style.setFillForegroundColor(new XSSFColor(color, new DefaultIndexedColorMap()));
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        style.setAlignment(horizAlign);
        style.setFont(font);
    }

    public static void setBorderStyle(CellStyle style){
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
    }

    private CellStyle makeHeaderGrayCellStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();

        Font font = workbook.createFont();
        font.setFontName("맑은 고딕");
        font.setBold(true);

        setBorderStyle(style);

        setCellStyle(style, Color.LIGHT_GRAY, HorizontalAlignment.CENTER, font);

        return style;
    }

    private CellStyle makeBodyCellStyle(Workbook workbook, Boolean isNum) {
        CellStyle style = workbook.createCellStyle();

        Font font = workbook.createFont();
        font.setFontName("맑은 고딕");

        setBorderStyle(style);

        setCellStyle(style, Color.WHITE,
                isNum ? HorizontalAlignment.RIGHT : HorizontalAlignment.CENTER, font);

        return style;
    }
}

엑셀의 모든 스타일 객체

ExcelColumn.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface ExcelColumn {
	public String name() default "";
}

각 컬럼의 네이밍을 하기 위한 커스텀 어노테이션

Student.java

@Getter
@Setter
@NoArgsConstructor
public class StudentExcelColumn {
	@ExcelColumn(name = "NO")
    private String no;
    
    @ExcelColumn(name = "이름")
    private String name;
    
    @ExcelColumn(name = "나이")
    private String age;
}

엑셀 다운로드할 Column 객체

BulkExcelService.java

@Service
@RequiredArgsConstructor
public class BulkExcelService {
    /**
     * 대량 엑셀 다운로드
     * @param data    Row 데이터
     * @param type    Column 타입
     * @param fileNm  파일 이름
     * @param sheetNm 시트 이름
     * @throws IOException
     */
    public void downloadBulkExcel(HttpServletResponse res, List<?> data, 
    							  Class<?> type, String fileNm) throws IOException {
        if (StringUtils.isBlank(fileNm)) {
            throw new AbleInvalidFormatException("필수 입력값 없음");
        }

        //Column 이름
        List<String> headers = Arrays.stream(type.getDeclaredFields())
                .map(v -> v.getAnnotation(ExcelColumn.class).name())
                .toList();
        //Column 필드명
        String[] columns = Arrays.stream(type.getDeclaredFields())
                .map(Field::getName)
                .toList().toArray(String[]::new);


        BulkExcelFile file = new BulkExcelFile(
                StringUtils.isBlank(sheetNm) ? "Sheet1" : sheetNm,
                data);

        file.setHeaders(0, 0, headers);

        int rowIdx = 1;
        for (Object rowData : data) {
            file.setBody(columns, rowData, rowIdx++, 0);
        }
        
        res.setContentType("application/vnd.ms-excel");
		res.setHeader("Content-Disposition", "attachment;filename=" + fileNm + ".xlsx");

        file.write(res.getOutputStream());
    }
}

이와같이 커스텀 어노테이션에서 해당 컬럼의 이름과 필드명을 빼와 데이터를 넣을 수 있도록 하였다.

Excel 다운로드 방식을 진행하면서 어떤 구조로 엑셀 다운로드 기능을 만들지에 대해서 많은 고민과 난관에 부딪히게 되었다. 그래도 갓구글과 함께라면(?) 어떠한 난관도 잘 해결할 수 있다는 믿음(?)이 크흐흠... 생기게 되었던거 같다.

성능적으로 보았을땐 원래 몇분이 걸리던게 1분 안쪽으로 다운로드 된다는 점을 확인하였지만 더 좋은 방법이 있지 않을까 싶다... 내가 한게 정말 괜찮은 코드인지 확인할 수는 없지만 좀 더 공부하고 노력해야 겠다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN