이 글은 우아한형제들 기술블로그의 아 엑셀다운로드 개발,,, 쉽고 빠르게 하고 싶다 (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을 만드는 것 입니다.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
String headerName() default "";
}
ExcelColumn이라는 이름의 Annotation을 생성합니다.
@Target으로 Annotation을 사용할 수 있는 곳을 필드 요소로 설정합니다.
@Retention으로 Annotation의 Life cycle을 Runtime으로 설정합니다.
@ExcelColumn은 headerName이라는 값을 가질 수 있습니다.
이전에 작성했던 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)
제목
출판사
가격
저자
재고
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();
}
}