[Springboot] 엑셀 파일 다운로드 하기

Bobby·2021년 8월 28일
11

즐거운 개발일지

목록 보기
4/22
post-thumbnail

0. 엑셀 파일 다운로드 하는 과정

  1. 엑셀 데이터를 만든다.

  2. 응답의 contentType을 지정한다.

    xls : application/vnd.ms-excel
    xlsx : application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

    • .xls 는 최대 65535 행까지 가능
    • .xlsx 는 최대 1048576 행까지 가능
  3. header에 Content-Disposition 추가한다.

    attachment; filename={filename}
    -> 해당 파일 이름으로 바디 데이터를 다운 받으라는 의미이다.


1. 프로세스

  • 스프링의 View 를 통해서 쉽게 구현할 수 있다. 엑셀 다운로드를 위해 추상화 된 View를 제공한다. (poi 라이브러리 사용)
    (AbstractXlsView, AbstractXlsxView, AbstractXlsxStreamingView)

    • AbstractXlsView : .xls 파일 생성 -> HSSFWorkbook 객체 생성
    • AbstractXlsxView : .xlsx 파일 생성 -> XSSFWorkbook 객체 생성
    • AbstractXlsxStreamingView : .xlsx 파일 생성 -> SXSSFWorkbook 객체 생성
  • 각 추상화된 View는 다음과 같은 구조를 가진다.

  • 예로 AbstractXlsxView를 살펴보면 Content-Type과 XSSFWorkbook 를 사용하는 것을 알 수 있다.

  • 예제는 간단하게 다음과 같은 구조 이다.

  1. 엑셀 다운로드 요청
  2. DB(혹은 어디든)에서 데이터 조회
  3. 서비스에서 뷰에 담을 모델객체를 생성(엑셀 파일에 들어갈 데이터)
  4. 컨트롤러에서 모델과 뷰를 리턴하면 뷰 리졸버가 동작
    -> ex) AbstractXlsxView, ...
  5. 렌더링 시 엑셀파일 생성
  6. 응답

2. 프로젝트 생성

  • springboot initializer
  • poi라이브러리 사용
  • 의존성
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation group: 'org.apache.poi', name: 'poi', version: '5.0.0' // xls
    implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.0.0' // xlsx
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

뷰 생성

  • AbstractXlsxView를 상속받아 뷰를 생성한다.
  • 컨트롤러에서 해당 뷰를 리턴하면 buildExcelDocument를 실행한다.
@Component
public class ExcelXlsxView extends AbstractXlsxView {

    @Override
    protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 엑셀 파일 생성 로직
        new ExcelWriter(workbook, model, response).create();
}
  • 컨트롤러에서 View를 넘기면 View의 render 메소드 실행
  • AbstractView : View의 render 오버라이딩, renderMergedOutputModel 메소드 실행
  • AbstractXlsView : AbstractView의 renderMergedOutputModel 오버라이딩, buildExcelDocument메소드 실행
  • AbstractXlsxView : .xlsx를 위한 추가 기능
  • ExcelXlsxView : AbstractXlsView의 buildExcelDocument 오버라이딩

컨트롤러

  • 모델(엑셀 파일을 만들 데이터)과 뷰를 리턴한다.
@GetMapping("/excel/download")
    public ModelAndView excelDownload(HttpServletRequest request) {
        Map<String, Object> excelData = excelService.excelDownload(request);
        return new ModelAndView(new ExcelXlsxView(), excelData);
    }

엑셀 다운로드 유틸 클래스

< ExcelWriter.java >

  • ExcelXlsxView에서 사용
  • 모델객체는 Map<String, Object>의 형태로 넘긴다.
  • 모델 객체의 데이터는 filename, head, body
    • filename : String (파일 이름)
    • head : List<String> (각 컬럼의 제목 )
    • body : List<List<String>> ( 데이터 )
public class ExcelWriter {

    private final Workbook workbook;
    private final Map<String, Object> data;
    private final HttpServletResponse response;

    // 생성자
    public ExcelWriter(Workbook workbook, Map<String, Object> data, HttpServletResponse response) {
        this.workbook = workbook;
        this.data = data;
        this.response = response;
    }
    
    // 엑셀 파일 생성
    public void create() {
        setFileName(response, mapToFileName());

        Sheet sheet = workbook.createSheet();

        createHead(sheet, mapToHeadList());

        createBody(sheet, mapToBodyList());
    }

    // 모델 객체에서 파일 이름 꺼내기
    private String mapToFileName() {
        return (String) data.get("filename");
    }

    // 모델 객체에서 헤더 이름 리스트 꺼내기
    @SuppressWarnings("unchecked")
    private List<String> mapToHeadList() {
        return (List<String>) data.get("head");
    }

    // 모델 객체에서 바디 데이터 리스트 꺼내기
    @SuppressWarnings("unchecked")
    private List<List<String>> mapToBodyList() {
        return (List<List<String>>) data.get("body");
    }

    // 파일 이름 지정
    private void setFileName(HttpServletResponse response, String fileName) {
        response.setHeader("Content-Disposition",
                "attachment; filename=\"" + getFileExtension(fileName) + "\"");
    }
    
    // 넘어온 뷰에 따라서 확장자 결정
    private String getFileExtension(String fileName) {
        if (workbook instanceof XSSFWorkbook) {
            fileName += ".xlsx";
        }
        if (workbook instanceof SXSSFWorkbook) {
            fileName += ".xlsx";
        }
        if (workbook instanceof HSSFWorkbook) {
            fileName += ".xls";
        }

        return fileName;
    }

    // 엑셀 헤더 생성
    private void createHead(Sheet sheet, List<String> headList) {
        createRow(sheet, headList, 0);
    }

    // 엑셀 바디 생성
    private void createBody(Sheet sheet, List<List<String>> bodyList) {
        int rowSize = bodyList.size();
        for (int i = 0; i < rowSize; i++) {
            createRow(sheet, bodyList.get(i), i + 1);
        }
    }

    // 행 생성
    private void createRow(Sheet sheet, List<String> cellList, int rowNum) {
        int size = cellList.size();
        Row row = sheet.createRow(rowNum);

        for (int i = 0; i < size; i++) {
            row.createCell(i).setCellValue(cellList.get(i));
        }
    }

    // 모델 객체에 담을 형태로 엑셀 데이터 생성
    public static Map<String, Object> createExcelData(List<? extends ExcelDto> data, Class<?> target) {
        Map<String, Object> excelData = new HashMap<>();
        excelData.put("filename", createFileName(target));
        excelData.put("head", createHeaderName(target));
        excelData.put("body", createBodyData(data));
        return excelData;
    }

    // @ExcelColumnName에서 헤더 이름 리스트 생성
    private static List<String> createHeaderName(Class<?> header) {
        List<String> headData = new ArrayList<>();
        for (Field field : header.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(ExcelColumnName.class)) {
                String headerName = field.getAnnotation(ExcelColumnName.class).headerName();
                if (headerName.equals("")) {
                    headData.add(field.getName());
                } else {
                    headData.add(headerName);
                }
            }
        }
        return headData;
    }

    // @ExcelFileName 에서 엑셀 파일 이름 생성
    private static String createFileName(Class<?> file) {
        if (file.isAnnotationPresent(ExcelFileName.class)) {
            String filename = file.getAnnotation(ExcelFileName.class).filename();
            return filename.equals("") ? file.getSimpleName() : filename;
        }
        throw new RuntimeException("excel filename not exist");
    }

    // 데이터 리스트 형태로 가공
    private static List<List<String>> createBodyData(List<? extends ExcelDto> dataList) {
        List<List<String>> bodyData = new ArrayList<>();
        dataList.forEach(v -> bodyData.add(v.mapToList()));
        return bodyData;
    }
}

출력할 데이터

  • 동물의 숲 api를 사용해 본다.
    https://acnhapi.com/doc
  • 간단한 background music 정보를 사용했다.

< ExcelDto.java >

  • 엑셀 데이터 인터페이스
public interface ExcelDto {

    List<String> mapToList();
}

< ExcelFileName.java >

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelFileName {

    String filename() default "";
}

< ExcelColumnName.java >

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumnName {

    String headerName() default "";
}

< BackgroundMusic.java >

  • @ExcelFileName : 엑셀 파일 이름 지정(기본값은 클래스명)
  • @ExcelColumnName : 각 컬럼의 이름(기본값은 필드명)
  • mapToList : 객체를 리스트 형태로 바꾸는 메소드(ExcelDto 인터페이스 에서 오버라이딩)
@Data
@AllArgsConstructor
@NoArgsConstructor
@ExcelFileName
public class BackgroundMusic implements ExcelDto{
    @ExcelColumnName(headerName = "music id")
    private int id;

    @ExcelColumnName
    @JsonProperty("file-name")
    private String filename;

    @ExcelColumnName
    private String hour;

    @ExcelColumnName
    private String weather;

    @ExcelColumnName(headerName = "music uri")
    @JsonProperty("music_uri")
    private String musicUri;

    @Override
    public List<String> mapToList() {
        return Arrays.asList(String.valueOf(id), filename, hour, weather, musicUri);
    }
}

데이터 가져오기

< ExcelService.java >

  • 외부 api 를 사용하여 데이터를 가져와서 사용.
  • DB에서 데이터 조회를 할 경우에는 repository 생성.
@Service
public class ExcelService {
    public Map<String, Object> excelDownload(HttpServletRequest request) {

        // 데이터 가져오기
        String uri = "https://acnhapi.com/v1a/backgroundmusic";
        RestTemplate restTemplate = new RestTemplate();
        BackgroundMusic[] result = restTemplate.getForObject(uri, BackgroundMusic[].class);
        List<BackgroundMusic> backgroundMusics = Arrays.asList(result);

        return ExcelWriter.createExcelData(backgroundMusics, BackgroundMusic.class);
    }
}

3. 테스트

  • 다운로드

  • 뷰를 변경해서 테스트
@Component
public class ExcelXlsView extends AbstractXlsView {

    @Override
    protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception {
        new ExcelWriter(workbook, model, response).create();
    }
}
@GetMapping("/excel/download")
    public ModelAndView excelDownload(HttpServletRequest request) {
        Map<String, Object> excelData = excelService.excelDownload(request);
        return new ModelAndView(new ExcelXlsView(), excelData);
    }
  • 확장자 변경 확인

코드

profile
물흐르듯 개발하다 대박나기

2개의 댓글

comment-user-thumbnail
2021년 11월 9일

정말 좋은 자료 고맙습니다. 빠르게 적용되네요!

답글 달기
comment-user-thumbnail
2022년 5월 20일

이거 스타일은 어떻게 변경 하나요?

답글 달기