본문 바로가기
Language/Java

엑셀 생성 기능 리팩토링하기

by codeyaki 2023. 9. 8.
반응형

엑셀을 다운로드하는 기능을 들었는데 구현에만 급급하다 보니 공통로직으로 빼지 못하고 아래의 코드에서 조금씩(필드에 따른 칼럼)만 변경하여 구현하게 되었다.

public Workbook makeUserListExcel(UserCriteriaVO criteria) {
        List<CustomUser> userList = userRepository.findUserListByCriteria(criteria);

        String[] colNames = {
                "no", "등급", "ID", "PWD", "이름", "핸드폰 번호", "비고", "생성자 ID", "생성자 이름", "생성일", "마지막 수정자 ID", "마지막 수정자 이름", "마지막 수정일"
        };

        XSSFWorkbook workbook = new XSSFWorkbook();

        int rowCnt = 0;

        XSSFSheet userListSheet = workbook.createSheet("인원 리스트");
        XSSFRow row = userListSheet.createRow(rowCnt++);

        for (int i = 0; i < colNames.length; i++) {
            XSSFCell cell = row.createCell(i);
            cell.setCellValue(colNames[i]);
        }

        XSSFCellStyle dateCellStyle = workbook.createCellStyle();
        dateCellStyle.setDataFormat(workbook.getCreationHelper().createDataFormat().getFormat("yyyy-MM-dd"));

        for (CustomUser user : userList) {
            int cellCnt = 0;
            row = userListSheet.createRow(rowCnt++);

            // seq
            XSSFCell cell = row.createCell(cellCnt++);
            cell.setCellValue(rowCnt - 1);

            // 등급
            cell = row.createCell(cellCnt++);
            cell.setCellValue(String.valueOf(user.getLevel()));

            // ID
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getUserSeq());

            // PWD
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getPassword());

            // 이름
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getName());

            // 핸드폰 번호
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getMobile());

            // 비고
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getEtc());

            // 생성자 ID
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getRegUser().getId());

            // 생성자 이름
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getRegUser().getName());

            // 생성일
            cell = row.createCell(cellCnt++);
            cell.setCellValue(user.getRegDatetime());
            cell.setCellStyle(dateCellStyle);

            // 마지막 수정자 ID
            cell = row.createCell(cellCnt++);
            if (user.getUptUser() != null) cell.setCellValue(user.getUptUser().getId());

            // 마지막 수정자 이름
            cell = row.createCell(cellCnt++);
            if (user.getUptUser() != null) cell.setCellValue(user.getUptUser().getName());

            // 마지막 수정일
            cell = row.createCell(cellCnt);
            cell.setCellValue(user.getUptDatetime());
            cell.setCellStyle(dateCellStyle);

        }

        for (int i = 0; i < colNames.length; i++) {
            userListSheet.autoSizeColumn(i);
            userListSheet.setColumnWidth(i, (userListSheet.getColumnWidth(i)) + 512);
        }
        return workbook;
    }

다 만들고 나서 보니 이런 비슷한 코드가 3개가 되어있었고  이 중복되는 코드들을 줄여서 하나의 코드로 변경해버리고 싶었다.

리팩토링 시작

엑셀을 만드는 과정은 workbook 생성 -> 시트 생성-> 컬럼 헤더 생성 -> 데이터 추가 (스타일 적용) -> 모든 칼럼 너비 수정 순으로 이뤄진다. 

모든 로직이 이러한 동작순서를 갖는데 차이점은 엑셀로 변환할 객체의 필드가 다르다는 점이었다. 그래서 헤더 또한 유동적으로 추가할 수 있어야 했다.

이를 공통 로직으로 변경하기 위해서는 엑셀로 변환시킬 객체의 필드가 어떤 것들이 있는지 알아야만 했다.

그래서 어떻게 접근할 수 있을까? 고민한 결과 자바 리플렉션을 이용하면 해결할 수 있겠다 싶었다.

또한 이때 각 필드의 컬럼 헤더명을 알아야 했기에 이는 어노테이션을 달아서 해결할 수 있도록 해봤다.
(덤으로 어노테이션을 단 필드만 엑셀에 추가시킬 수 있도록 짜봤다.. 나중에 엔티티에다가도 쓸 수 있지 않을까? 싶어서)

 

어노테이션 제작

엑셀로 저장시킬 필드를 고르고, 칼럼 명을 나타내기 위해서 어노테이션을 제작해 주었다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MakeExcelColumn {
    public String name();
    public boolean isDate() default false;
}

 

간단하게 설명해 보자면

name과 isDate 속성을 가지고 있는데 name은 헤더명으로 변환될 것이고 isDate타입은 엑셀에 Date타입만 포맷을 적용시켜야 했기에 분리해서 사용했다.

 

 

클래스 생성

import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.List;


public class ExcelFactory<T> {
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet;
    XSSFRow row;
    XSSFCell cell;

    public Workbook makeExcelWorkBook(Class<T> targetClass, List<T> objectList, String sheetName) throws IllegalAccessException {
        int rowCnt = 0;
        sheet = workbook.createSheet(sheetName);
        row = sheet.createRow(rowCnt++);
        int cellCnt = 0;
        row.createCell(cellCnt);
        int ColumCnt = 0;
        /* 컬럼 헤드 작성 */
        cell = row.createCell(cellCnt++);
        cell.setCellValue("no");
        ColumCnt++;
        for (Field field : targetClass.getDeclaredFields()) {
            field.setAccessible(true);
            for (Annotation annotation : field.getDeclaredAnnotations()) {
                if (annotation instanceof MakeExcelColumn) {
                    MakeExcelColumn makeExcelColumnAnnotation = (MakeExcelColumn) annotation;
                    cell = row.createCell(cellCnt++);
                    cell.setCellValue(makeExcelColumnAnnotation.name());
                    ColumCnt++;
                }
            }
        }

        XSSFCellStyle dateCellStyle = workbook.createCellStyle();
        dateCellStyle.setDataFormat(workbook.getCreationHelper().createDataFormat().getFormat("yyyy-MM-dd"));

        // 엑셀 value 채우기
        for (Object object : objectList) {
            Field[] fields = object.getClass().getDeclaredFields();
            row = sheet.createRow(rowCnt++);
            cellCnt = 0;
            cell = row.createCell(cellCnt++);
            cell.setCellValue(rowCnt - 1);
            for (Field field : fields) {
                field.setAccessible(true);
                for (Annotation annotation : field.getDeclaredAnnotations()) {
                    if (annotation instanceof MakeExcelColumn) {
                        MakeExcelColumn makeExcelColumnAnnotation = (MakeExcelColumn) annotation;
                        cell = row.createCell(cellCnt++);
                        if (makeExcelColumnAnnotation.isDate()) {
                            cell.setCellValue((LocalDateTime) field.get(object));
                            cell.setCellStyle(dateCellStyle);
                        }else {
                            cell.setCellValue(String.valueOf(field.get(object)));
                        }
                    }
                }
            }
        }
        
        // 셀 정렬
        for (int i = 0; i < ColumCnt; i++) {
            sheet.autoSizeColumn(i);
            sheet.setColumnWidth(i, (sheet.getColumnWidth(i)) + 1024);
        }
        return workbook;
    }
}

특별히 추가된 부분이라면

for (Annotation annotation : field.getDeclaredAnnotations()) {
    if (annotation instanceof MakeExcelColumn) {
        MakeExcelColumn makeExcelColumnAnnotation = (MakeExcelColumn) annotation;
        cell = row.createCell(cellCnt++);
        cell.setCellValue(makeExcelColumnAnnotation.name());
        ColumCnt++;
    }
}

이 부분인데 각 field의 annotation들을 가져와서 MakeExcelColumn 어노테이션이 달린 필드만 골라서 만들어주는 방법으로 변경하였다. 

 

사용하기

먼저 엑셀로 변경시킬 객체를 만들어준다

 

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public static class Excel {
    @MakeExcelColumn(name = "등급")
    private String level;
    @MakeExcelColumn(name = "ID")
    private String Id;
    @MakeExcelColumn(name = "PWD")
    private String pwd;
    @MakeExcelColumn(name = "이름")
    private String name;
    @MakeExcelColumn(name = "핸드폰 번호")
    private String mobile;
    @MakeExcelColumn(name = "비고")
    private String etc;
    @MakeExcelColumn(name = "생성자 ID")
    private String regUserId;
    @MakeExcelColumn(name = "생성자 이름")
    private String regUserName;
    @MakeExcelColumn(name = "생성일", isDate = true)
    private LocalDateTime regDatetime;
    @MakeExcelColumn(name = "마지막 수정자 ID")
    private String uptUserId;
    @MakeExcelColumn(name = "마지막 수정자 이름")
    private String uptUserName;
    @MakeExcelColumn(name = "마지막 수정일", isDate = true)
    private LocalDateTime uptDatetime;

    public static Excel of(CustomUser user) {
        return new Excel(
                String.valueOf(user.getLevel()),
                user.getId(),
                user.getPwd(),
                user.getName(),
                user.getMobile(),
                user.getEtc(),
                user.getRegUser().getId(),
                user.getRegUser().getName(),
                user.getRegDatetime(),
                user.getUptUser() != null ? user.getUptUser().getId() : "",
                user.getUptUser() != null ? user.getUptUser().getName() : "",
                user.getUptDatetime()
        );
    }
}

그 후 factory를 생성하고 workbook 생성 메서드를 호출해 준다.

예시에는 레파지토리에서 유저 목록을 가져오는 메서드를 통해서 엔티티를 조회한 후 dto로 변환해 주었다.

엔티티에서 바로 사용해도 되지만 엔티티에 연관관계가 맺어져 있는 엔티티가 있어 dto로 변환해서 해주었다. (다음에 시간이 되면 어노테이션을 통해서 객체 안에 객체가 있는 경우도 가능하도록 만들어봐야겠다.)

public Workbook makeUserListExcel(UserCriteriaVO criteria) throws IllegalAccessException {
    List<UserDTO.Excel> userList = userRepository.findUserListByCriteria(criteria).stream().map(UserDTO.Excel::of).collect(Collectors.toList());
    ExcelFactory<UserDTO.Excel> excelFactory = new ExcelFactory<>();
    return excelFactory.makeExcelWorkBook(UserDTO.Excel.class, userList, "사용자 목록");
}

 

야호 보기 불편했던 service에서의 코드들이 많이 줄어들어 보기 좋게 바뀐 모습을 확인할 수 있다.

 

반응형