Spring Boot + Apache POI를 이용한 엑셀 다운로드

DevSeoRex·2023년 2월 2일
2
post-thumbnail

🎈 갑자기 엑셀 업로드와 다운로드 로직을 만들어야 한다..?

항상 같은 일상이 반복되다 보면 새롭고 재밌는 일이 있으면 좋겠다는 바램을 가질때가 있습니다.
아.. 요즘 하는 일이 비슷하네.. 뭐하나 새로 만들어보면 좋겠다. 그런일 어디 없나? 하는 제 마음을 아신것같이 팀장님이 일을 주셔서.. 어쩌다보니 새로운 일을 맡게 되었습니다..

그것은 바로 수기로 하던 작업을 엑셀을 업로드 하면 알아서 해주는 프로그램을 만드는 것입니다. 좀 고급스럽게 표현하면 업무 자동화(?) 라고 표현할 수 있겠네요.

기존에 정산과 관련한 데이터를 엑셀로 뽑아서 현재 데이터와 맞춰보는 시즌이 있는데요
그 시즌에 원래는 사람이 DB에 접속에서 데이터를 뽑은뒤에 하나하나 체크를 했었다고 합니다.

하지만 여기서 간과했던것이.. 넵! 하고 돌아서 보니 저는 한번도 엑셀을 Spring으로 다뤄본 적이 없었다는 것입니다..

🎨 엑셀 다운로드를 어떻게 만들어볼까?

기존 시스템을 보면, 엑셀 다운로드에 관한 로직이 컨트롤러 특정 조건을 줘서 수행되도록 코드가 작성되어 있었습니다. 설계에 관심이 많은 요즘 엑셀 파일의 형식, 셀의 개수(컬럼의 수)와 상관없이 동적으로 실행되는 로직을 만들고 싶어졌습니다.

그래서 저는 아래와 같은 순서로 코드 작성을 시작했습니다.

🤗 필요한 것들은 무엇일까? - Domain & Repository & Service

기술을 선정할때, 회사에서는 마이바티스를 사용하지만 현재 제가 만들고 있는 데모버전에서 데이터베이스에 조회만 한다고 해도 접근하는 것보다는 편하게 데이터를 버리고 다시 넣을수 있도록
H2 Database + JPA 조합으로 설정을 진행했습니다.

  • application.properties
# Server Port
server.port=80

#h2 DB 설정
spring.datasource.url=jdbc:h2:~/test;
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create

#hibernate 설정
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true

일단 Entity를 만들어야 하는데, 학생의 반과 이름을 엑셀파일로 만든다고 생각을하고, 학생 엔티티를 먼저 만들었습니다.

  • Student Entity
package com.example.demo.domain.entity;

import com.example.demo.domain.dto.StudentDto;
import lombok.*;

import javax.persistence.*;

@Entity @Getter @Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public class Student {
    
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "BAN")
    private Long id;
    
    @Column(name = "NAME")
    private String name;


    // Student Entity를 StudentDto로 변경해 반환하는 메서드
    public StudentDto toDto() {
        return new StudentDto(id, name);
    }
}

아직 Test를 진행중인 코드라 콘솔에 빨리 찍어보기 위해 @ToString을 사용중이지만 정상적인 기능이 동작하는 것이 확인되면, 반드시 떼어낼 예정입니다.

엔티티를 조회해서 엑셀파일로 가공할때 커스텀 애너테이션을 사용하도록 설계했기때문에, DTO를 따로 만들었습니다. 따라서 엔티티를 DTO로 변환해주는 메서드를 넣어주었습니다.

  • StudentDto
package com.example.demo.domain.dto;

import com.example.demo.annotation.ExcelColumn;
import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@Builder
public class StudentDto {

    @ExcelColumn(headerName = "반")
    private Long ban;

    @ExcelColumn(headerName = "이름")
    private String name;
}

DTO이기 때문에 생성자와 Getter & Setter를 적용하였습니다. Builder 패턴을 사용해서 유연하게 객체를 생성할 수 있도록 했습니다.
@ExcelColumn은 제가 만든 커스텀 애너테이션입니다. 이 부분은 엑셀 다운로드 로직에 대해 자세히 설명할때 다루도록 하겠습니다.
@ExcelColumn이 가지고 있는 headerName이 첫 행의 각 셀의 값이 됩니다.
즉 엑셀파일에서 첫번째 행은, 컬럼이름들로 채워집니다. 예시를 보면 아래와 같습니다.

위와 같이 엑셀 파일을 만든다고 할때, "반", "이름"이 headerName 입니다.

  • StudentRepository
package com.example.demo.repository;

import com.example.demo.domain.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentRepository extends JpaRepository<Student, Long> {

}

굳이 이걸 올려야하나..? 싶을 정도로 정말 아무것도 없습니다. JpaRepository를 상속받기만 하면 사용할 수 있는 save와 findAll & deleteAll 메서드만 사용하기 때문에 따로 메서드를 정의하지 않았습니다.

  • StudentService
package com.example.demo.service;

import com.example.demo.domain.entity.Student;
import com.example.demo.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class StudentService {

    private final StudentRepository studentRepository;

    /*
    *   학생 한명 저장
    *   @param Student
    *   @return Student
    * */
    @Transactional
    public Student saveStudent(Student student) {
        return studentRepository.save(student);
    }

    /*
    *   학생 전부 조회
    *   @return List<Student>
    * */
    @Transactional(readOnly = true)
    public List<Student> findAllStudent() {

        // 학생 전체 조회
        List<Student> studentList = studentRepository.findAll();

        // 학생 전체 조회 후 리스트가 비어있다면, 예외를 던진다.
        if(CollectionUtils.isEmpty(studentList)) {
            log.error("조회된 학생이 없어서 예외 발생!");
            throw new IllegalStateException("조회된 학생 데이터가 없습니다. 확인 후 다시 진행해주시기 바랍니다.");
        }

        return studentList;
    }

}

서비스 메서드에서 따로 특별한 로직은 가지고 있지 않습니다.
다만 학생 리스트를 조회했을때 리스트가 비어 있다면 예외를 던지도록 작성하였습니다.
나중에 headerName의 정보를 가져오기 위해 List에 접근하게 되는데, 그때 list가 비어 있으면 예외가 발생할 것이므로, 선제조치를 해놓았습니다.

  • DBInit
package com.example.demo.utils;

import com.example.demo.domain.dto.StudentDto;
import com.example.demo.domain.entity.Student;
import com.example.demo.repository.StudentRepository;
import com.example.demo.service.StudentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

@Component
@RequiredArgsConstructor
@Slf4j
public class DBInit {

    private final StudentService studentService;
    private final StudentRepository repository;

    // Bean이 초기화 될때 한 번 실행되는 메서드
    @PostConstruct
    public void init() {

        // 있는 데이터를 전부 삭제
        repository.deleteAll();

        // 30명의 학생을 insert
        insertDummyStudent();

        // 학생 리스트를 조회
        List<Student> list = studentService.findAllStudent();

        // 찾은 학생을 DTO로 변환 후 log로 출력
        for(Student s : list) {
            StudentDto dto = s.toDto();
            log.info("찾은 학생 = {}", dto);
        }
    }

    public void insertDummyStudent() {
        for(int i=1; i<30; i++) {
            studentService.saveStudent(Student.builder().name("학생"+i).build());
        }
    }
}

매번 새로 데이터를 입력하거나 주석을 풀었다 지웠다 하는 것이 굉장히 번거롭기 때문에 Bean이 생성되는 초기에 한번(서버가 실행되면) 더미데이터를 넣어주도록 DBInit 클래스를 만들었습니다.

학생 데이터를 넣거나 조회하기 위한 클래스는 여기까지 입니다. 아래부터는 이제 실제 엑셀을 만들기 위한 핵심 비즈니스 로직을 가진 클래스들을 다루겠습니다.

🤗 필요한 것들은 무엇일까? - ExcelUtils, Interface, Controller, View

View는 정말 단순하게 엑셀 다운 버튼 하나만 놓았습니다.

  • excelDownLoadForm.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <button id="download_excel">엑셀 다운</button>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
  $(document).ready(function () {
    $('#download_excel').on('click', function () {
       location.href = '/excel/download';
    });
  });
</script>
</html>

제가 자주 사용하던 jQuery를 사용해서 간단하게 동작하도록 구현했습니다.

  • ExcelController
package com.example.demo.controller;

import com.example.demo.domain.dto.StudentDto;
import com.example.demo.domain.entity.Student;
import com.example.demo.service.StudentService;
import com.example.demo.utils.ExcelUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.stream.Collectors;

@Controller
@Slf4j
@RequiredArgsConstructor
public class ExcelController {

    private final StudentService studentService;
    private final ExcelUtils excelUtils;


    /*
    *   엑셀 다운로드 FORM 출력
    *   @return String(View Name)
    * */
    @GetMapping("/excel/downloadForm")
    public String excelDownloadForm() {
        return "excelDownloadForm";
    }

    /*
    *   엑셀 다운로드
    *   @param HttpServletResponse
    *   @throws IOException
    *   @throws RuntimeException
    * */
    @GetMapping("/excel/download")
    public void excelDownLoad(HttpServletResponse response) {
        log.info("/excel/download 요청 도착!!");

        // 엑셀로 출력할 학생 리스트 조회
        List<Student> studentList = studentService.findAllStudent();

        // 학생 EntityList를 DtoList로 변환
        List<StudentDto> studentDtoList = studentList.stream()
                        .map(s -> s.toDto())
                        .collect(Collectors.toList());

        // 엑셀 다운로드 로직 실행
        excelUtils.studentExcelDownload(studentDtoList, response);
    }

ExcelController는 엑셀 다운로드 페이지로 이동하는 로직과, 엑셀 다운로드를 실제로 수행하는 요청을 처리하는 로직 두가지를 가지고 있습니다.

Excel을 읽거나 쓰는 작업을 하는 ExcelUtils 클래스에 의존하고 있고, Excel을 쓰거나 다운로드 하는 일은 ExcelUtils의 책임으로 할 수 있도록 책임 분리에 집중했습니다.

  • ExcelUtilMethodFactory
package com.example.demo.utils;

import com.example.demo.domain.dto.StudentDto;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

public interface ExcelUtilMethodFactory {

    void studentExcelDownload(List<StudentDto> data, HttpServletResponse response);
    void renderStudentExcelBody(List<StudentDto> data, Sheet sheet, Row row, Cell cell);
}

ExcelUtils를 만들면서 생각을 하게 되었던 첫 번째 문제가 학생 엔티티만, 즉 한 종류의 데이터만 엑셀로 출력하는 모듈을 만들것인지의 문제였습니다.

그래서 각 엔티티별로 메서드를 따로 사용하기 위해서, ExcelUtilMethodFactory라는 인터페이스로 분리하게 되었습니다.

  • ExcelColumn
package com.example.demo.annotation;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
/*
*  엑셀 헤더 구분용 Custom Annotation
* */
public @interface ExcelColumn {

    String headerName() default "";
}

엑셀 헤더의 이름을 구분하기 위한 커스텀 애너테이션입니다.
DTO에 엑셀 헤더의 이름을 애너테이션으로 설정해주고, ExcelUtils 클래스안에서 reflection을 사용하여 각 @ExcelColumn이 가진 값을 뽑아서내서 Header의 이름으로 뿌려주는 로직을 작성해보겠습니다.

  • ExcelUtils

가장 중요한 핵심 로직인 ExcelUtils 입니다.

ExcelUtils는 내용이 많아서 메서드 하나하나 뜯어서 살펴보겠습니다.

  • studentExcelDownload

가장 먼저 학생의 반,이름을 엑셀 파일로 다운로드해주는 핵심 메서드인 studentExcelDownload입니다. 이 메서드는 ExcelUtilMethodFactory에 정의되어 있고, ExcelUtils에 오버라이딩 되어 있습니다.

첫줄부터 한줄 한줄 살펴보도록 하겠습니다.

  1. WorkBook 객체를 생성하고, sheet를 생성합니다. sheet는 이름을 지정해 줄 수 있습니다.
    엑셀 파일의 행과 열에 해당하는 Cell 객체와 Row 객체를 생성해 줍니다.
Workbook workbook =  new XSSFWorkbook();

/ 엑셀파일 sheet를 만들고, sheet의 이름을 지정해 줄 수 있다.
Sheet sheet = workbook.createSheet("첫 번째 시트");

// 엑셀의 열에 해당하는 Cell 객체 생성
Cell cell = null;

// 엑셀의 행에 해당하는 Row 객체 생성
Row row = null;
  1. 헤더(첫 번째 열)들의 이름을 List로 반환받습니다. 여기서 Header들의 이름이란 앞전에 보여드린 DTO의 @ExcelColumn을 사용해서 정의했던 값들을 말합니다.
// List가 아닌 DTO를 넘겨줘야 하므로 메서드를 통해 DTO의 class 정보가 담긴 class 객체를 넣어준다.
// Header의 내용을 List로 반환 받는다(엑셀의 Cell의 첫줄이 된다.)
List<String> excelHeaderList = getHeaderName(getClass(data));

excelHeaderList를 반환받으려면 두개의 메서드가 중첩되어 사용되는 것을 보실 수 있습니다.

  • getClass 메서드에 대해서 먼저 살펴보겠습니다.
   /*
    *   List(데이터 리스트)에 담긴 DTO의 클래스 정보를 반환하는 메서드
    *   @param List<?>
    *   @return Class<?>
    * */
    private Class<?> getClass(List<?> data) {
        // List가 비어있지 않다면 List가 가지고 있는 모든 DTO는 같은 필드를 가지고 있으므로,
        // 맨 마지막 DTO만 빼서 클래스 정보를 반환한다.
        if(!CollectionUtils.isEmpty(data)) {
            return data.get(data.size()-1).getClass();
        } else {
            log.error("리스트가 비어 있어서 예외 발생!");
            throw new IllegalStateException("조회된 리스트가 비어 있습니다. 확인 후 다시 진행해주시기 바랍니다.");
        }
    }

getClass 메서드는 List 하나를 매개변수로 받아서 List안에 들어가있는 DTO의 클래스 정보를 반환해주는 메서드입니다.
여기서 List가 비어있지 않으면 맨 마지막 DTO를 뽑아서 그 DTO의 클래스 정보를 뽑은 뒤 반환하는 이유는, 헤더(애너테이션)의 정보를 뽑는데는 DTO 안에 들어있는 데이터가 중요하지 않습니다.

중요한것은 DTO의 headerName값을 가져오는 것인데, 예를 들어 30건의 데이터가 조회되서 List에 30개의 DTO가 들어있다고 가정하면, 30개의 DTO 모두 headerName 값이 애너테이션으로 정의 되어 있으므로 예외가 발생하지 않는 선에서 List에 들어있는 DTO 한개의 class 정보만 반환해주면 되기 때문입니다.

  • getHeaderName 메서드에 대해서 살펴보겠습니다.
	/*
     *   엑셀 헤더 이름들을 반환해주는 로직
     *   @param Class<?>
     *   @throws IllegalStateException
     * */
    private List<String> getHeaderName(Class<?> type) {

        // 스트림으로 엑셀 헤더 이름들을 리스트로 반환
        // 1. 매개변수로 전달된 클래스의 필드들을 배열로 받아, 스트림을 생성
        // 2. @ExcelColumn 애너테이션이 붙은 필드만 수집
        // 3. @ExcelColumn 애너테이션이 붙은 필드에서 애너테이션의 값을 매핑
        // 4. LinkedList로 반환
        List<String> excelHeaderNameList =  Arrays.stream(type.getDeclaredFields())
                .filter(s -> s.isAnnotationPresent(ExcelColumn.class))
                .map(s -> s.getAnnotation(ExcelColumn.class).headerName())
                .collect(Collectors.toCollection(LinkedList::new));

        // 헤더의 이름을 담은 List가 비어있을 경우, 헤더 이름이 지정되지 않은 것이므로, 예외를 발생시킨다.
        if(CollectionUtils.isEmpty(excelHeaderNameList)) {
            log.error("헤더 이름이 조회되지 않아 예외 발생!");
            throw new IllegalStateException("헤더 이름이 없습니다.");
        }

        return excelHeaderNameList;
    }

getHeaderName 메서드는 reflection을 사용해서 필드에 접근하여, 애너테이션의 정보를 받아오고, 애너테이션의 headerName의 값을 뽑아서 List에 넣어 반환해주는 메서드입니다.

위 코드를 처음보거나 이해가 안되시는 부분이 있다면 Java Reflection에 대해서 공부해보시면 좋을 것 같습니다.

  1. getHeaderName으로 뽑아온 헤더의 이름들을 첫번째 행(엑셀 파일의 첫째 줄)에 그려줍니다.
// Header - 열의 첫줄(컬럼 이름들)을 그리는 작업

// 첫 행을 생성해준다.
row = sheet.createRow(0);

// 헤더의 수(컬럼 이름의 수)만큼 반복해서 행을 생성한다.
for(int i=0; i<excelHeaderList.size(); i++) {

// 열을 만들어준다.
cell = row.createCell(i);

// 열에 헤더이름(컬럼 이름)을 넣어준다.
cell.setCellValue(excelHeaderList.get(i));

}

코드 자체는 단순합니다.
행을 하나 생성해주고, 헤더의 수만큼 반복문을 수행하면서 cell을 만들고, cell에 값을 채워주는 간단한 로직으로 구성되어 있습니다.

  1. 첫번째 행(헤더)을 제외한 엑셀의 내용을 그려주는 작업을 수행합니다.
// Body
// 헤더 밑의 엑셀 파일 내용부분에 들어갈 내용을 그리는 작업
renderStudentExcelBody(data, sheet, row, cell);

코드가 한줄밖에 없는 걸 보실 수 있습니다. 헤더 그리는 작업도 꽤나 많은 작업을 했는데 본문을 그리는게 코드가 한줄이라면 이상한게 맞습니다.

본문을 그려주는 부분은 메서드로 따로 분리를 했습니다.

  • renderStudentExcelBody 메서드를 살펴보겠습니다.
	/*
     *   엑셀의 본문에 내용을 그려주는 로직
     *   @param List<StudentDto>
     *   @param Sheet
     *   @param Row
     *   @param Cell
     * */
    @Override
    public void renderStudentExcelBody(List<StudentDto> data, Sheet sheet, Row row, Cell cell) {
        // 현재 행의 개수를 가지고 있는 변수 rowCount 선언(Header를 그리고 시작했으므로 1부터 시작)
        int rowCount = 1;

        // 조회해온 데이터 리스트(List<StudentDto>)의 크기만큼 반복문을 실행한다.
        for(StudentDto student : data) {

            // 헤더를 설정할때 0번 인덱스가 사용 되었으므로, i값에 1을 더해서 1번 로우(행)부터 생성한다.
            row = sheet.createRow(rowCount++);

            // TODO : 하드코딩 대신 추후 동적으로 처리 할 수 있도록 개선 예정
            // 첫 번째 cell(열)을 생성한다.
            cell = row.createCell(0);
            // 첫 번째 cell(열)의 값을 셋팅한다.
            cell.setCellValue(student.getBan());
            // 두 번째 cell(열)을 생성한다.
            cell = row.createCell(1);
            // 두 번째 cell(열)의 값을 셋팅한다.
            cell.setCellValue(student.getName());
        }
    }

이 메서드도 ExcelUtilsMethodFactory 인터페이스에 정의되어 있는 메서드로 오버라이딩 된 것입니다.
현재 0번 인덱스를 가진 Row는 헤더를 그리는 데 사용했으므로, rowConut는 1부터 시작합니다.

조회해온 데이터(학생의 반,이름을 담은 DTO 리스트)를 반복문을 돌면서 헤더를 채울때와 같이,
Row를 생성한뒤, 컬럼의 수(반, 이름 이므로 2개)만큼 cell을 생성하고 값을 넣어줍니다.

  1. 실제 다운로드가 일어나는 로직을 수행합니다.
// DownLoad
// 엑셀 파일이 완성 되면 파일 다운로드를 위해 content-type과 Header를 설정해준다.
// filename=파일이름.xlsx 가 파일의 이름이 된다.
        response.setContentType("ms-vnd/excel");
        response.setHeader("Content-Disposition", "attachment;filename=student.xlsx");

        try {
            // 엑셀 파일을 다운로드 하기 위해 write() 메서드를 사용한다.
            workbook.write(response.getOutputStream());
        } catch (IOException e) {
            // checked 예외를 사용하면 추후 의존이나 예외 누수 문제가 생길 수 있으므로
            // RuntimeException으로 한번 감싸서, cause가 나올 수 있게 발생한 예외를 넣어준다.
            log.error("Workbook write 수행 중 IOException 발생!");
            throw new RuntimeException(e);
        } finally {
            // 파일 입출력 스트림을 사용한 후에는 예외 발생 여부와 관계없이 반드시 닫아 주어야 한다.
            closeWorkBook(workbook);
        }

파일 다운로드를 위해서 ContentType을 전달받은 response 객체를 사용해서 설정해주고,
헤더에 파일의 이름을 설정할 수 있습니다.
filename=student.xlsx로 현재 작성이 되어있는데, =뒤에 오는 이름을 파일이 다운로드 됩니다. 파일 이름을 변경하고 싶다면 이 부분을 수정하시면 됩니다.

WorkBook의 write 메서드를 사용하면 만들어진 Workbook 객체가 엑셀파일로 다운로드 됩니다.
입출력 스트림을 사용하기 때문에 IOException이 발생할 수 있는데, IOException은 chekced Exception이기 때문에 꼭 처리하거나, throws 키워드로 선언해주어야 하는 불편함이 있습니다.

위와 같이 런타임 예외에 감싸서 던져주면, throws 키워드를 사용하지 않아도 되는 장점과 런타임 예외가 발생하였고 원인 예외는 IOExcetion 이라는 메시지도 전달 받을 수 있습니다.

closeWorkBook 메서드가 이제 finally 블럭에서 실행이 됩니다.
closeWorkBook을 메서드로 따로 분리한 이유는, workbook을 닫는 close 메서드를 finally 블럭 안에서 수행하면 또 try ~ catch 블럭을 이용해서 IOException을 처리해주어야 합니다.

따라서 closeWorkBook 메서드를 만들고, 메서드 내부에서 try ~ catch 블럭을 사용해서 내부에서 처리하도록 메서드로 분리했습니다.

  • closeWorkBook 메서드
   /*
    *  WorkBook의 스트림을 닫아주는 로직
    *  @param Workbook
    *  @throws RuntimeException
    *  @throws IOException
    * */
    private void closeWorkBook(Workbook workbook) {
        try {
            if(workbook != null) {
                workbook.close();
            }
        } catch (IOException e) {
            // checked 예외를 사용하면 추후 의존이나 예외 누수 문제가 생길 수 있으므로
            // RuntimeException으로 한번 감싸서, cause가 나올 수 있게 발생한 예외를 넣어준다.
            throw new RuntimeException(e);
        }
    }

😂 대망의 테스트 시작


정말 버튼 하나만 있는 페이지입니다.

여기서 저 엑셀 다운 버튼을 클릭하면 엑셀 파일이 다운로드 되면 성공입니다.


엑셀 파일이 무사히 다운로드 되어있는 것을 보실수 있습니다.

이제 다운로드된 엑셀 파일이 원하는 양식으로 작성되어 있는지 확인하겠습니다.


데이터가 길어서 일부 잘랐지만, 반과 이름(헤더의 이름들)이 맨위에 위치하고 DB의 데이터를 읽어서 엑셀에 잘 뿌려진 것을 볼 수 있습니다.

😎 기뻐하지마 이제 시작인걸?

엑셀 다운로드만 해도 처음 해보는 것이라서 꽤 시간이 걸렸지만 아직 이것은 시작에 불과합니다.
앞으로 이어질 엑셀 업로드와 업로드 된 엑셀의 데이터를 Spring으로 가져가는 기능,
또 현재 엑셀 데이터와 DB의 데이터를 비교해서 다른 점을 찾아 엑셀로 다시 다운로드 해주는 기능의 포스팅도 이어집니다. 많이 기대해주세요!

모든 뉴비 개발자 동지 여러분들 화이팅!

엑셀 기능 만든 나.. 너무 잘..했어!!

위에서 다룬 코드는 아래 주소의 Repository에 업로드 되어 있습니다.
(계속 업데이트 중입니다) - 기준일 2023-02-02

GitHub : https://github.com/ch4570/spring-boot-excel

참고 : https://techblog.woowahan.com/2698/

(우아한 형제들 기술 블로그)

0개의 댓글