달려LIKE추추
내 머리 속의 지우개🦶
달려LIKE추추
전체 방문자
오늘
어제
  • 분류 전체보기
    • Java
    • 끄적임
    • 네트워크
    • Spring
      • JPA
      • Security
      • WebFlux
      • Cloud
    • Web

블로그 메뉴

  • 홈
  • 방명록

공지사항

인기 글

태그

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
달려LIKE추추

내 머리 속의 지우개🦶

JAVA POI 라이브러리에 Reflection을 더해보자!
Java

JAVA POI 라이브러리에 Reflection을 더해보자!

2023. 6. 3. 11:23

최근 사내 프로젝트 중 특정 데이터를 엑셀 파일로 받는 기능을 구현해야 했습니다. Java POI 라이브러리에서 지원해주고 있는 기능인데 해당 라이브러리에서 지원하는 기능을 그대로 사용하면 중복된 코드들이 많이 발생할 수 있어 생산성이 다소 떨어질 수 있다는 생각을 했습니다. 방법을 찾던 중 여기서 Java Reflection 을 사용하여 해당 문제를 해결하는 글을 읽어보았는데, 평소에 Reflection 기능의 구현에 대해 궁금함도 있었고, 위에서 언급한 문제도 해결하고자 포스팅하게 되었습니다.

위 블로그의 코드를 분석해본 글이라 코드를 많이 참고하여 글을 작성하였습니다. Reflection 코드에 집중하고 싶어 위 블로그에서의 다수의 추상화 및 디자인 패턴을 생략하였습니다.

기존 POI 라이브러리를 사용할 경우

@GetMapping("/api/v1/excel/car")
public void downloadCarInfo(HttpServletResponse response) throws IOException {
    // 엑셀 파일 하나 생성
    Workbook workbook = new SXSSFWorkbook();
    Sheet sheet = workbook.createSheet();

    // 엑셀 렌더링에 필요한 DTO를 가져옵니다
    List<CarExcelDto> carExcelDtos = carService.getCarInfo();

    // 헤더 생성
    int rowIndex = 0;
    Row headerRow = sheet.createRow(rowIndex++);
    Cell headerCell1 = headerRow.createCell(0);
    headerCell1.setCellValue("회사");

    Cell headerCell2 = headerRow.createCell(1);
    headerCell2.setCellValue("차종");

    Cell headerCell3 = headerRow.createCell(2);
    headerCell3.setCellValue("가격");

    Cell headerCell4 = headerRow.createCell(2);
    headerCell3.setCellValue("평점");

    // 바디에 데이터를 넣어줍니다
    for (CarExcelDto dto : carExcelDtos) {
        Row bodyRow = sheet.createRow(rowIndex++);

        Cell bodyCell1 = bodyRow.createCell(0);
        bodyCell1.setCellValue(dto.getCompany());

        Cell bodyCell2 = bodyRow.createCell(1);
        bodyCell2.setCellValue(dto.getName());

        Cell bodyCell3 = bodyRow.createCell(2);
        bodyCell3.setCellValue(dto.getPrice());

        Cell bodyCell4 = bodyRow.createCell(3);
        bodyCell4.setCellValue(dto.getRating());
    }
    response.setContentType("application/vnd.ms-excel");
    workbook.write(response.getOutputStream());
}

위 과정에서의 문제점은 헤더와 바디 값을 매번 지정해주어야 한다는 점입니다. 반복적인 작업이라 생산성이 많이 떨어질 수 있고 실제로 오타로 인한 에러도 발생하였습니다. 또한 위에서는 CarExcelDto 값을 엑셀파일로 받으려 하지만 만약 BikeExcelDto라는 새로운 데이터를 받고자 한다면 위와 같은 과정을 또 반복해야 하는 문제점이 있습니다.

 

POI에 Java Reflection 적용

위의 문제들을 해결하기 위해 Java Reflection 을 적용해 보겠습니다.

먼저 조회하려는 Dto에 커스텀 어노테이션을 적용합니다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
    String headerName() default "";
}
@AllArgsConstructor
@Getter
@Builder
public class CarExcelDto {
    @ExcelColumn(headerName = "회사")
    private final String company;
    @ExcelColumn(headerName = "차종")
    private final String name;
    @ExcelColumn(headerName = "가격")
    private final int price;
    @ExcelColumn(headerName = "평점")
    private final double rating;
}

 

ExcelRenderResource는 엑셀에 그려져야 하는 필드들의 이름(필드명)과 엑셀에 보여야 하는 헤더 이름을 맵으로 관리합니다.

@ExcelColumn 이 적용된 필드들을 맵에 담아줍니다.

public class ExcelRenderResource {

   private Map<String, String> excelHeaderNames;
   private List<String> dataFieldNames;

   public static ExcelRenderResource prepareRenderResource(Class<?> type) {
      Map<String, String> headerNamesMap = new LinkedHashMap<>();
      List<String> fieldNames = new ArrayList<>();

      for (Field field : getAllFields(type)) {
         if (field.isAnnotationPresent(ExcelColumn.class)) {
            ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
            fieldNames.add(field.getName());
            headerNamesMap.put(field.getName(), annotation.headerName());
         }
      }

      return new ExcelRenderResource(headerNamesMap, fieldNames);
   }

   public ExcelRenderResource(Map<String, String> excelHeaderNames, List<String> dataFieldNames) {
      this.excelHeaderNames = excelHeaderNames;
      this.dataFieldNames = dataFieldNames;
   }

   public String getExcelHeaderName(String dataFieldName) {
      return excelHeaderNames.get(dataFieldName);
   }

   public List<String> getDataFieldNames() {
      return dataFieldNames;
   }

}

 

@ExcelColumn 어노테이션이 달린 DTO 객체를 받아 자동으로 엑셀을 그려줄 ExcelFile을 생성합니다. 

Reflection으로 Field 값을 가져와 해당 Data 의 타입에 따라 각각 셀에 입력해줍니다.

public final class OneSheetExcelFile<T> {

   private static final int ROW_START_INDEX = 0;
   private static final int COLUMN_START_INDEX = 0;
   private int currentRowIndex = ROW_START_INDEX;

   private SXSSFWorkbook wb;
   private Sheet sheet;
   private ExcelRenderResource resource;


   public OneSheetExcelFile(List<T> data, Class<T> type) {
      this.wb = new SXSSFWorkbook();
      this.resource = ExcelRenderResource.prepareRenderResource(type);
      renderExcel(data);
   }

   public void renderExcel(List<T> data) {
      // 1. Create sheet and renderHeader
      sheet = wb.createSheet();
      renderHeadersWithNewSheet(sheet, currentRowIndex++, COLUMN_START_INDEX);

      if (data.isEmpty()) {
         return;
      }

      // 2. Render Body
      for (Object renderedData : data) {
         renderBody(renderedData, currentRowIndex++, COLUMN_START_INDEX);
      }
   }

   private void renderHeadersWithNewSheet(Sheet sheet, int rowIndex, int columnStartIndex) {
      Row row = sheet.createRow(rowIndex);
      int columnIndex = columnStartIndex;
      for (String dataFieldName : resource.getDataFieldNames()) {
         Cell cell = row.createCell(columnIndex++);
      }
   }

   private void renderBody(Object data, int rowIndex, int columnStartIndex) {
      Row row = sheet.createRow(rowIndex);
      int columnIndex = columnStartIndex;
      for (String dataFieldName : resource.getDataFieldNames()) {
         Cell cell = row.createCell(columnIndex++);
         try {
            Field field = getField(data.getClass(), (dataFieldName));
            field.setAccessible(true);
            Object cellValue = field.get(data);
            renderCellValue(cell, cellValue);
         } catch (Exception e) {
            throw new ExcelInternalException(e.getMessage(), 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();
   }

}

 

위의 과정을 거치면 Controller Layer에서의 코드도 상당히 간단해집니다.

@GetMapping("/api/v2/excel/car")
public void downloadCarInfo2(HttpServletResponse response) throws IOException {
    response.setContentType("application/vnd.ms-excel");
    List<CarExcelDto> result = carService.getCarInfo();

    OneSheetExcelFile excelFile = new OneSheetExcelFile(result, CarExcelDto.class);
    excelFile.write(response.getOutputStream());

}

 

마치며

Reflection을 어떤 식으로 구현해야 하던 중에 정말 좋은 글을 읽어 도움이 많이 되었던 것 같습니다. 위의 블로그에 가보시면 엑셀의 스타일에도 적용하셨고, 추상화 및 디자인 패턴을 사용하셔서 코드를 작성하셨는데 많이 감탄하면서 코드를 봤던 것 같습니다. 관심 있으신 분들은 참고해 보시면 좋을 것 같습니다.

'Java' 카테고리의 다른 글

Enum 에 대해 알아보자!  (0) 2023.05.07
Java 직렬화(Serialization)에 대해 알아보자!  (0) 2022.06.10
Java Stream API를 사용해보자!  (0) 2022.05.29
    'Java' 카테고리의 다른 글
    • Enum 에 대해 알아보자!
    • Java 직렬화(Serialization)에 대해 알아보자!
    • Java Stream API를 사용해보자!
    달려LIKE추추
    달려LIKE추추
    풋내기 백엔드 개발자의 기록 창고

    티스토리툴바