[spring] Apache POI 추상화 모듈 구현하기 2

KIM Jongwan·2024년 1월 10일
0

SPRING

목록 보기
5/7
post-thumbnail

이 글은 우아한형제들 기술블로그의 아 엑셀다운로드 개발,,, 쉽고 빠르게 하고 싶다 (feat. 엑셀 다운로드 모듈 개발기) 를 참고하여 작성되었습니다.

목 차
1. Apache POI 추상화 모듈 구현하기 1 - 엑셀 다운로드 구현
2. Apache POI 추상화 모듈 구현하기 2 - 추상화 모듈 만들기
3. Apache POI 추상화 모듈 구현하기 3 - 추가 기능

기존 엑셀 다운로드 문제점


앞서 구현해 본 엑셀 다운로드 기능은 동작에는 문제가 없지만 몇가지 단점이 존재합니다.

  • 확장이 용이하지 않다.
  • 코드가 지저분하고 직관적이지 않다.
  • 작성 중 오탈자로인해 기능이 정상적으로 작동하지 않을 수 있다.

POI를 활용하여 엑셀 다운로드를 구현해 본 많은 개발자들이
엑셀 다운로드를 위한 추상화 모듈 있었으면 좋겠다! 라는 생각을 한번쯤은 해보았을 것입니다.

이미 관련된 여러 작업들이 포스팅 되어있고, 그중 대부분이 우아한형제들 기술 블로그의 최태현님께서 작성주신 포스트를 참고하고 있기 때문에 이 글에서도 해당 글을 참고하여 엑셀 다운로드 모듈을 작성해보겠습니다.


목표


목표는 간단합니다. 엑셀 파일 랜더링 기능을 수행하는 SimpleExcelFile.class를 작성하여 엑셀 다운로드시

SimpleExcelFile simpleExcelFile = new SimpleExcelFile(bookDto, BookDto.class);
simpleExcelFile.write();

위와 같이 인스턴스를 생성하고, write() 메소드를 호출하기만 하는 방식으로 원하는 엑셀 파일을 생성하는 것입니다.

가장 먼저 해볼 것은 SimpleExcelFile객체로 전달될 Dto.class에 필드의 엑셀 헤더 정보를 입력받을 Annotation을 만드는 것 입니다.

@ExcelColumn

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
    String headerName() default "";
}

ExcelColumn이라는 이름의 Annotation을 생성합니다.
@Target으로 Annotation을 사용할 수 있는 곳을 필드 요소로 설정합니다.
@Retention으로 Annotation의 Life cycle을 Runtime으로 설정합니다.
@ExcelColumn은 headerName이라는 값을 가질 수 있습니다.

BookExcelDto.class

이전에 작성했던 BookDto를 수정한 BookExcelDto를 작성해봅시다.

public class BookExcelDto {
	
    //constructor()
    public BookExcelDto(){}
    
    //constructor(all args[])
    public BookExcelDto(String title, String publisher, int price, String author, int stock) {
        this.title = title;
        this.publisher = publisher;
        this.price = price;
        this.author = author;
        this.stock = stock;
    }

	@ExcelColumn(headerName = "제목")
    private String title;		// 제목
    
    @ExcelColumn(headerName = "출판사")
    private String publisher;	// 출판사
    
    @ExcelColumn(headerName = "가격")
    private int price;			// 가격
    
    @ExcelColumn(headerName = "저자")
    private String author;		// 저자
    
    @ExcelColumn(headerName = "재고")
    private int stock;			// 재고
        
    
    //getter, setter
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    // ... 이하 생략
}

BookDto와 동일한 구성이지만 @ExcelColumn을 활용하여 각 필드에 엑셀 다운로드 시 입력될 헤더의 값을 지정해주었습니다.

이제 SimpleExcelFile.class를 생성하기 전에 BookExcelDto의 @ExcelColumn Annotation값을 가져오는 방법을 먼저 알아보겠습니다.

BookExcelDto book1 = new BookExcelDto("소설1", "출판사1", 1000, "김아무개", 10);

Class<T> clazz = book1.getClass();
Field[] fields = clazz.getDeclaredFields(); //BookExcelDto에 정의되어있는 필드 배열 조회

//반복문을 돌며 선언된 필드의 Annotation값을 출력
for (Field field : fields){
	ExcelColumn ec = field.getAnnotation(ExcelColumn.class);
    if(ec != null){
    	System.out.println(ec.headerName());
	}
}

//실행 결과(console)
제목
출판사
가격
저자
재고

SimpleExcelFile.class

public class SimpleExcelFile<T> {

    private static final SpreadsheetVersion supplyExcelVersion = SpreadsheetVersion.EXCEL2007;
    private static final int ROW_START_INDEX = 0;
    private static final int COLUMN_START_INDEX = 0;

    private SXSSFWorkbook wb;
    private Sheet sheet;
    private Field[] fields;


    public SimpleExcelFile(List<T> data, Class<T> type){
        validateMaxRow(data);
        this.wb = new SXSSFWorkbook();
        setFields(type);

        if(data.isEmpty()){
            throw new RuntimeException("excel data is empty");
        }

        renderExcel(data);
    }

    //최대 행 수 검증
    private void validateMaxRow(List<T> data){
        int maxRows = supplyExcelVersion.getMaxRows();
        if (data.size() > maxRows){
            throw new IllegalArgumentException(
                    String.format("does not support over %s rows", maxRows));
        }
    }

    //엑셀 랜더링
    private void renderExcel(List<T> data){
        sheet = wb.createSheet();
        renderHeaders(sheet, ROW_START_INDEX, COLUMN_START_INDEX);

        int rowIndex = ROW_START_INDEX + 1 ;
        for (Object renderData : data){
            renderBody(sheet, renderData, rowIndex++, COLUMN_START_INDEX);
        }
    }

    private void renderHeaders(Sheet sheet, int rowStartIndex, int columnStartIndex) {
        Row row = sheet.createRow(rowStartIndex);
        int columnIndex = columnStartIndex;

        for (Field field : this.fields){
            ExcelColumn ec = field.getAnnotation(ExcelColumn.class);
            if(ec != null){
                Cell cell = row.createCell(columnIndex++);
                cell.setCellValue(ec.headerName());
            }
        }
    }

    private void renderBody(Sheet sheet, Object data, int rowIndex, int columnStartIndex){
        Row row = sheet.createRow(rowIndex);
        int columnIndex = columnStartIndex;
        for (Field field : fields){
            Cell cell = row.createCell(columnIndex++);
            field.setAccessible(true);
            try {
                renderCellValue(cell, field.get(data));
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }

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

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

    public void writeFile() throws IOException{
        File outputFile = new File(filePath);
        FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
        wb.write(fileOutputStream);
        wb.close();
        wb.dispose();
        fileOutputStream.close();
    }

    private void setFields(Class<T> clazz){
    	this.fields = clazz.getDeclaredFields();
    }
}
  • SimpleExcelFile(List data, Class type)
    • 생성자를 이용하여 인스턴스를 생성할 때, 인자로 데이터리스트와 데이터 타입을 전달 받습니다.
      인스턴스 생성시 데이터 값 검증 후 renderExcel()를 호출하여 엑셀 Workbook을 생성합니다.
  • renderExcel(List data)
    • workbook에 새로운 sheet를 생성합니다.
    • renderHeaders()를 호출하여 @ExcelColumn값으로 헤더값을 입력합니다.
    • 반복문을 돌며 renderBody(Object data)를 호출하여 데이터를 입력합니다.

사용 예

profile
3년차 백앤드 개발자입니다.

0개의 댓글