본문 바로가기
공부 기록

[JAVA x Apache POI] 전략 패턴과 리플렉션을 활용하여 컬럼 자동 생성 엑셀 다운로드 구현하기

by 타태 2022. 4. 16.

2022.04.12 - [문제 해결 기록] - [QueryDSL] 페이징 직접 처리하기

 

[QueryDSL] 페이징 직접 처리하기

2022.04.09 - [문제 해결 기록] - [IntelliJ + Maven + QueryDSL] Failed to execute goal com.mysema.maven:apt-maven-plugin:1.1.3:process 해결 [IntelliJ + Maven + QueryDSL] Failed to execute goal com.my..

ktae23.tistory.com

 

*** 2022 04 18 필드 제외 기능 추가

 

 

엑셀 생성부 코드 출처 - Dion.Ko 티스토리

필드명 찾기 메서드 코드 출처 - 우아한 형제들 기술 블로그

 

 

DB에서 읽어온 데이터를 엑셀에 매핑하여 다운로드 하는 기능을 구현해보자.

구글링을 하다 Dion.Ko님 블로그에서 예제를 찾아 구현했다.

구현 끝~

 

 

 

 

근데 나중에 다른 리소스도 엑셀 다운로드하고 싶으면 어쩌지?

하고 싶은 클래스마다 또 저걸 만들어?

 

아니야 리플렉션으로 필드명을 찾아서 동적 생성을 하자!

 

그러고 찾은 [아 엑셀다운로드 개발... 쉽고 빠르게 하고 싶다]글.

해당 글에서는 심지어 어노테이션을 만들어서 컬럼 스타일까지 지정해준다;;

난 그렇게까지 필요한건 아니어서 그냥 엑셀 컬럼과 너비 정도만 지정해주는걸 만들었다.

이넘을 활용했기 때문에 설정을 추가하려면 할수 있는 구조인것 같다.

 

 

📌 먼저, 컨트롤러

@Requiredargconstructor
public class ExcelController{

    private final ExeclUtils excelUtils;

	@GetMapping("/excel/download")
    public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {
        List<UserDto> allUser = userService.findAll();
        if (isNotEmptyOrNull(allUser)) {
            XSSFWorkbook userListExcel = excelUtils.excelDownload(UserExcel.class, UserDto.class, allUser, "사용자");
       		
            String userAgent = request.getHeader("User-Agent");
            String encodedName = fileUtils.fileNameEncoder("파일테스트.xlsx", userAgent);
            response.addHeader("Content-Disposition", "attachment;filename=" + encodedName);
            response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
            response.addHeader("Pragma", "no-cache");
            response.addHeader("Expires", "0");
            response.addHeader("Content-Type", "application/vnd.ms-excel");
            response.setStatus(HttpStatus.OK.value());
            application.write(response.getOutputStream());
        }
    }
}

엑셀 유틸에 다운로드를 호출할 때 (엑셀전략, 타켓클래스, 데이터 리스트, sheet 이름)을 전달한다.

 

📌 전략 패턴 사용을 위한 인터페이스 선언

컬럼 명, 컬럼 너비 외에도 원하는 설정을 추가 할 수 있다.

public interface ColumnEnums {
    String colName();
    Integer colWidth();
}
public interface ExcelColumns {

    <T extends ColumnEnums> T valueOf(String name);
    
    String[] getExcludes();
}

 

📌 UserDto에 매핑 되는 컬럼 클래스

솔직히 좋은 설계인지는 아직 모르겠다. 하지만 내가 원하는 기능을 정확히 제공해준다.

조회 결과를 엑셀로 생성하길 원하면 타겟 클래스의 필드명을 복사해서 아래와 같은 형식의 설정 클래스를 만들고 지정 된 컬럼 패키지에 추가하면 엑셀 유틸을 사용 할 수 있다.

* enum 클래스의 메서드 valuOf를 통해 가져오기 때문에 컬럼 설정에 누락 된 필드가 있을 경우 예외를 발생시킨다.

컨트롤러 단에서 사용하기 때문에 Dto 클래스에 있는 모든 클래스를 사용한다고 가정했다.

엑셀 설정 클래스에 있는 필드만 넣고 싶을 경우 유틸에서 예외처리를 추가하고 데이터 입력부분을 수정해야 한다.

@Component
public class UserExcel implements ExcelColumns {

    public UserExcel() {}

    private UserColumns columns;
    private String[] excludes = {"password"};

    @Override
    public <T extends ColumnEnums> T valueOf(String name) {
        return (T) columns.valueOf(name);
    }


    protected enum UserColumns implements ColumnEnums {
        USERID("회원 아이디", 5000),
        NAME("이름", 3000),
        EMAIL("이메일 주소", 5000),
        STATUS("상태", 2000),
        ROLE("회원 유형", 2000),
        REGDATE("등록일", 5000)
        
        private String colName;
        private Integer colWidth;
        
        UserColumns(String colName, Integer colWidth) {
            this.colName = colName;
            this.colWidth = colWidth;
        }

        @Override
        public String colName() {
            return colName;
        }

        @Override
        public Integer colWidth() {
            return colWidth;
        }
    }

}

필드명을 Key로 갖는 HashMap을 사용해보려고도 시도 했으나 필드명을 반복하여 중복 사용하는 점과

컬럼 명, 컬럼 너비를 한 곳에서 볼 수 있는 enum 방식과 달리 관점이 흩어지는 점 때문에 트레이드 오프를 했다.

 

 

필드명 조회 유틸

아래 코드는 getAllFieldNames만 내가 눈꼽만큼 수정해서 추가했고,

모든 코드는 필드명 찾기 메서드 코드 출처 - 우아한 형제들 기술 블로그 에 올라온 코드를 그대로 사용했다.

public class classFieldUtils {

    private classFieldUtils(){}

    public static <T> Field getFieldByName(T t, String fieldName) {
        Objects.requireNonNull(t);

        Field field = null;
        for (Field f : getAllFields(t)) {
            if (f.getName().equals(fieldName)) {
                field = f;
                break;
            }
        }
        if (field != null) {
            field.setAccessible(true);
        }
        return field;
    }

    public static <T> T getFieldValue(T t, String fieldName) {
        Objects.requireNonNull(t);
        try {
            Field field = getFieldByName(t, fieldName);
            return (T) field.get(t);
        } catch (IllegalAccessException e) {
            return null;
        }
    }

    public static <T> List<Field> getAllFields(T t) {
        Objects.requireNonNull(t);

        Class<?> clazz = t.getClass();
        List<Field> fields = new ArrayList<>();
        while (clazz != null) {
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            clazz = clazz.getSuperclass();
        }
        return fields;
    }

    public static  <T> List<String> getAllFieldNames(Class<?> clazz) {
        Objects.requireNonNull(clazz);

        List<Field> fields = new ArrayList<>();
        while (clazz != null) {
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            clazz = clazz.getSuperclass();
        }
        return fields.stream().map(Field::getName).collect(Collectors.toList());
    }

}

 

엑셀 유틸 생성 부 ( 전략 패턴 사용)

아래 코드는 엑셀 생성부 코드 출처 - Dion.Ko 티스토리에 올라온 코드를 가져와 타겟 엑셀 동적 생성 로직 추가, 메서드 분리 정도만 진행하여 사용했다.

@Slf4j
@Getter
@Component
public class ExcelUtils {

    private ExcelColumns excelColumns;
    private ConcurrentHashMap<String, ExcelColumns> strategyMap = new ConcurrentHashMap<>();

    @PostConstruct
    void init() {
    	// 엑셀 컬럼 설정 클래스를 모아 둔 패키지 하위의 모든 컬럼 설정 클래스 조회 
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        provider.addIncludeFilter(new AssignableTypeFilter(ExcelColumns.class));

        Set<BeanDefinition> components = provider.findCandidateComponents("com/excel_test/excel/excel_columns");

		// 찾아 온 설정 클래스 모두를 전략으로 등록
        for (BeanDefinition component : components) {
            Class<?> clazz = null;
            try {
                clazz = Class.forName(component.getBeanClassName());
                Constructor<?> constructor = clazz.getConstructor();
                Object instance = constructor.newInstance();
                strategyMap.put(component.getBeanClassName(), (ExcelColumns) instance);
            } catch (Exception e) {
            	// 상황에 맞게 예외 추가 +
                log.warn("엑셀 전략 등록 중 클래스 조회 예외 발생 {}", e.getMessage());
            }

        }
    }

    public void setExcelColumns(String strategy) {
        this.excelColumns = strategyMap.get(strategy);
    }

    public <T> XSSFWorkbook excelDownload(Class<?> strategy, Class<T> target, List<T> dataList, String sheetName) {
        if (isNotEmptyOrNull(dataList)) {
            // 컬럼 전략 지정
            setExcelColumns(strategy.getName());
            XSSFWorkbook workbook = new XSSFWorkbook();
            XSSFSheet sheet = null;
            XSSFCell cell = null;
            XSSFRow row = null;
            int rowCnt = 0;
            int cellCnt = 0;

            // 엑셀 헤더 설정
            CellStyle headerStyle = setHeaderStyle(workbook);
            // 엑셀 바디 설정
            CellStyle bodyStyle = setBodyStyle(workbook);

            // 엑셀 시트명 설정
            sheet = workbook.createSheet(sheetName);
            row = sheet.createRow(rowCnt++);

			// 엑셀 생성 타겟 클래스의 필드명을 조회
            List<String> fieldNames = LcFieldUtils.getAllFieldNames(target);

            // 제외 필드 제거
            excludeFields(fieldNames, excelColumns.getExcludes());
            
			// 해당 전략에서 타겟 클래스의 필드와 일치하는 컬럼 설정 꺼내 옴
            List<ColumnEnums> columnEnums = fieldNames.stream()
            	.map(String::toUpperCase)
                .map(excelColumns::valueOf)
                .map(o -> (ColumnEnums) o)
                .collect(Collectors.toList());
            List<Integer> colWidths = columnEnums.stream().map(ColumnEnums::colWidth).collect(Collectors.toList());
            List<String> colNames = columnEnums.stream().map(ColumnEnums::colName).collect(Collectors.toList());

            // 인덱싱
            cell = row.createCell(0);
            cell.setCellStyle(headerStyle);
            cell.setCellValue("No");
            sheet.setColumnWidth(0, 2000);

            //헤더 정보 구성
            for (int i = 0; i < colNames.size(); i++) {
                cell = row.createCell(i + 1);
                cell.setCellStyle(headerStyle);
                cell.setCellValue(colNames.get(i));
                sheet.setColumnWidth( i + 1, colWidths.get(i));
            }

            for (T data : dataList) {
                cellCnt = 0;
                row = sheet.createRow(rowCnt++);

                // 인덱싱
                cell = row.createCell(cellCnt++);
                cell.setCellStyle(bodyStyle);
                cell.setCellValue(dataList.indexOf(data));
                sheet.setColumnWidth(0, 2000);
                // 데이터 입력
                for (String fieldName : fieldNames) {
                    Object value = getFieldValue(data, fieldName);
                    cell = row.createCell(cellCnt++);
                    cell.setCellStyle(bodyStyle);
                    cell.setCellValue(String.valueOf(value));
                }
            }
            return workbook;
        }
        return null;
    }

    private void excludeFields(List<String> fieldNames, String[] excludeFields) {
        if (isNotEmptyOrNull(excludeFields)) {
            List<String> tmp = new ArrayList<>();
            for (String excludeField : excludeFields) {
                if (fieldNames.contains(excludeField)) {
                    tmp.add(excludeField);
                }
            }
            fieldNames.removeAll(tmp);
        }
    }

    private static CellStyle setHeaderStyle(XSSFWorkbook workbook) {
        Font fontHeader = workbook.createFont();
        fontHeader.setFontName("맑은 고딕");    //글씨체
        fontHeader.setFontHeight((short) (9 * 20));    //사이즈
        fontHeader.setBold(true);  //볼드

        CellStyle headerStyle = workbook.createCellStyle();
        headerStyle.setAlignment(HorizontalAlignment.CENTER);
        headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        headerStyle.setBorderRight(BorderStyle.THIN);
        headerStyle.setBorderLeft(BorderStyle.THIN);
        headerStyle.setBorderTop(BorderStyle.THIN);
        headerStyle.setBorderBottom(BorderStyle.THIN);
        headerStyle.setFillForegroundColor(HSSFColorPredefined.GREY_25_PERCENT.getIndex());
        headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        headerStyle.setFont(fontHeader);
        return headerStyle;
    }

    private static CellStyle setBodyStyle(XSSFWorkbook workbook) {
        Font font9 = workbook.createFont();
        font9.setFontName("맑은 고딕");    //글씨체
        font9.setFontHeight((short) (9 * 20));    //사이즈

        CellStyle bodyStyle = workbook.createCellStyle();
        bodyStyle.setAlignment(HorizontalAlignment.CENTER);
        bodyStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        bodyStyle.setBorderRight(BorderStyle.THIN);
        bodyStyle.setBorderLeft(BorderStyle.THIN);
        bodyStyle.setBorderTop(BorderStyle.THIN);
        bodyStyle.setBorderBottom(BorderStyle.THIN);
        bodyStyle.setFont(font9);
        return bodyStyle;
    }


}
반응형

댓글