프로젝트

[Kotlin][SpringBoot Excel] 엑셀 다운로드 공통 서비스 가이드

nineDeveloper 2021. 8. 3.
728x90

필수 셋팅

엑셀 다운로드시 추가 설정 사항을 적용하여 엑셀 다운로드를 할 수 있다

옵션 설정

옵션설정을 하지 않으면 모두 default 값으로 설정 된다
header, fileName 은 설정을 해주는 것이 좋다

설정가능한 옵션

  • title: 엑셀 제목 (default: 제목없음)
  • header: 엑셀 헤더 (default: 조회된 컬럼명으로 헤더를 자동 생성함)
  • fileName: 엑셀 파일명 (default: export.xlsx)
  • columnWidth: 엑셀 컬럼 기본간격 설정값 엑셀 간격 폭을 넓힐때 설정 (default: 3000)
  • style: 기본적인 테두리 설정과 font 설정이 추가된다 다운로드시 속도가 저하된다 (default: false)
  • autoSize: 자동으로 컬럼을 리사이징 한다 (default: false)
package com.project.component.excel.domain.dto

/**
 * 엑셀 다운로드 확장 도메인
 */
class ReqExcelDownload (
    /* 엑셀 제목 */
    var title: String? = null,
    /* 엑셀 Header */
    var header: Array<String> = arrayOf(),
    /* 엑셀 fileName */
    var fileName: String? = null,
    /* 엑셀 컬럼 사이즈(지정하지 않으면 기본 사이즈(3000)가 지정된다) */
    var columnWidth: Int? = null,
    /* 스타일 여부 기본 false */
    var style: Boolean = false,
    /* 오토 컬럼 리사이징 사용 여부 기본 false */
    var autoSize: Boolean = false
) {
    override fun toString(): String {
        return "ReqExcelDownload(title=$title, header=${header.contentToString()}, fileName=$fileName, columnWidth=$columnWidth, style=$style, autoSize=$autoSize)"
    }
}

ExcelService

공통 엑셀 다운로드 서비스는 향상된 SXSSF 방식으로 엑셀 다운로드 서비스를 제공한다
별도의 flush 설정은 하지 않고 기본값을 사용한다

안정성: 30만건의 데이터까지 테스트 완료

SXSSF : XSSF의 Streaming Version으로 메모리를 적게 사용하여 대용량 엑셀 다운로드에 주로 사용되는 방식

SXSSF 방식 -> 생성자 방식에서의 기본 flush 크기는 100 이며, -1 지정시 무제한이다. 쓰기전용이며 읽기는 불가능하다

공통 엑셀 다운로드 서비스는 ObjectMap 형태를 모두 지원한다
엑셀 다운로드 서비스 사용시 엑셀 다운로드를 사용할 서비스에서 ExcelService 를 의존성을 주입 받고 download 메서드만 호출해서
List<Object> 또는 List<Map> 원하는 형태의 매개변수를 전달하여 사용하면 된다

소스코드

package com.project.component.excel.service

import com.project.component.excel.constant.ExcelConstant.Companion.EXCEL_XLSX_STREAMING_VIEW
import com.project.component.excel.domain.dto.ReqExcelDownload
import org.springframework.stereotype.Service
import org.springframework.web.servlet.ModelAndView

/**
 * 엑셀 서비스
 */
@Service
class ExcelService(var excelConverter: ExcelConverter) {

    /**
     * 엑셀 다운로드 서비스
     * Map 이나 객체 형태로 받아서 엑셀 파일로 만들어 리턴함
     * 엑셀 다운로드 서비스를 이용하기 위해서 요청 파라메터는
     * ReqExcel 엑셀 객체를 상속 받아서 파라메터로 전달해야함
     * @param list
     * @param <T>
     * @return
    </T> */
    fun <T> download(list: MutableList<T>): ModelAndView {
        return ModelAndView(EXCEL_XLSX_STREAMING_VIEW, excelConverter.convertList(list))
    }

    /**
     * 엑셀 다운로드 서비스
     * Map 이나 객체 형태로 받아서 엑셀 파일로 만들어 리턴함
     * 엑셀 다운로드 서비스를 이용하기 위해서 요청 파라메터는
     * ReqExcel 엑셀 객체를 상속 받아서 파라메터로 전달해야함
     * @param list
     * @param <T> 엑셀 변환 데이터
     * @param <E extends ReqExcel> 엑셀 파라메터
     * @return
    </E></T> */
    fun <T> download(list: List<T>, req: ReqExcelDownload): ModelAndView {
        return ModelAndView(EXCEL_XLSX_STREAMING_VIEW, excelConverter.convertList(list, req))
    }
}

사용법

엑셀 다운로드 서비스 사용시 엑셀 다운로드를 사용할 서비스에서 ExcelService 를 의존성을 주입 받고
download 메서드만 호출해서 List<Object> 또는 List<Map> 원하는 형태의 매개변수를 전달하여 사용

  • 엑셀 다운로드 서비스의 리턴 타입은 ModelAndView로 설정
  • headerfileName을 설정하고 excelService.download 를 호출

Sample 엑셀 다운로드 서비스 소스코드

조회된 데이터가 null 인 경우 빈 excel 파일을 export.xlsx로 리턴해주므로 별도의 null 처리를 할 필요가 없다

package com.project.api.sample.service

@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
@Service
class SampleExcelService(
    val excelService: ExcelService
) {
    val log: Logger = LoggerFactory.getLogger(this::class.java)

    /**
     * Sample 엑셀 다운로드 API
     * @return
     * @throws IOException
     */
    @Throws(IOException::class)
    fun getSampleExcel(): ModelAndView {

        val dataList: List<SampleExcelDownload> = listOf(
            SampleExcelDownload(
                name="홍길동1",
                email="aaa@aaa.com",
                phone="010-1111-1111",
                dept="서버팀",
                workCode=1111,
                deptCode=2222,
                content="테스트 내용",
                ip="123.123.123.123",
                percent= BigDecimal("1.1234"),
                createDate="2019-10-10T00:00",
                updateDatetime="2019-03-10T06:30:30",
                empty= null),
            SampleExcelDownload(
                name="홍길동2", 
                email="bbb@aaa.com", 
                phone="010-1111-2222", 
                dept="서버팀", 
                workCode=1111, 
                deptCode=2222, 
                content="테스트 내용", 
                ip="1.1.1.1", 
                percent= BigDecimal(1234.123455), 
                createDate="2019-10-10T00:00", 
                updateDatetime="2019-03-10T06:30:30", 
                empty=null)
        )

        return excelService.download(dataList,
            ReqExcelDownload(
                header = arrayOf("이름", "이메일", "전화번호", "소속부서", "업무코드", "부서코드", "내용", "IP", "소수", "날짜", "일시", "빈데이터"),
                fileName = "샘플파일_${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}"
            )
        )
    }
}

Sample 엑셀 다운로드 컨트롤러 소스코드

package com.project.api.sample.controller

import com.project.api.sample.service.SampleExcelService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.core.io.Resource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.io.IOException

@RestController
@RequestMapping("/sample")
@Tag(name = "[Sample] 공통 Excel", description = "SampleExcelController")
class SampleExcelController(
    val sampleExcelService: SampleExcelService) {

    @Operation(summary = "Sample 엑셀 양식 다운로드")
    @GetMapping("/excelform")
    @Throws(IOException::class)
    fun getSampleExcelForm(): ResponseEntity<Resource> = sampleExcelService.getSampleExcelForm()

    @Operation(summary = "Sample 엑셀 다운로드")
    @GetMapping("/excel")
    @Throws(IOException::class)
    fun getSampleExcel() = sampleExcelService.getSampleExcel()
}

Sample 엑셀 다운로드 테스트코드

package com.project.api.sample.controller

import com.project.common.BaseMvcTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import javax.validation.Validator

/**
 * 엑셀 공통 서비스 테스트
 */
class SampleExcelControllerTest(val validator: Validator): BaseMvcTest() {

    @BeforeEach
    fun beforeSetUp() {
        applyEncodingFilter()
    }

    @Test
    fun `엑셀 다운로드 테스트`() {
        this.mockMvc
            .perform(get("/sample/excel"))
            .andExpect(status().isOk)
            .andDo(print())
    }

    @Test
    fun `엑셀 양식 다운로드 테스트`() {
        this.mockMvc
            .perform(get("/sample/excelform"))
            .andExpect(status().isOk)
            .andDo(print())
    }
}

테스트 코드 공통 클래스

package com.project.common

import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.commons.lang3.StringUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.mock.web.MockMultipartFile
import org.springframework.test.context.TestConstructor
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.springframework.web.filter.CharacterEncodingFilter
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

@SpringBootTest
@AutoConfigureMockMvc
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class BaseMvcTest {

    @Autowired
    lateinit var mapper: ObjectMapper

    @Autowired
    lateinit var mockMvc: MockMvc

    @Autowired
    lateinit var ctx: WebApplicationContext

    fun applyEncodingFilter() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.ctx)
            .addFilters<DefaultMockMvcBuilder>(
                CharacterEncodingFilter("UTF-8", true),
            )
            .alwaysDo<DefaultMockMvcBuilder>(print())
            .build()
    }

    /**
     * 파일 업로드 데이터 생성
     * @param originalFileName 원본파일명
     * @param reqFileName 요청파일명(API에서 받는 이름)
     * @return
     */
    fun getMultipartFile(originalFileName: String, reqFileName: String): MockMultipartFile {
        return getMultipartFile(null, originalFileName, reqFileName)
    }

    /**
     * 파일 업로드 데이터 생성
     * @param originalFileName 원본파일명
     * @param reqFileName 요청파일명(API에서 받는 이름)
     * @return
     */
    fun getMultipartFile(
        fileMiddlePath: String?,
        originalFileName: String?,
        reqFileName: String?
    ): MockMultipartFile {
        var filePath = Paths.get("src", "test", "resources", "file")
        if (StringUtils.isNotEmpty(fileMiddlePath)) filePath = Path.of(filePath.toString(), fileMiddlePath)
        filePath = Paths.get(filePath.toString(), originalFileName)
        return MockMultipartFile(reqFileName!!, originalFileName, null, Files.readAllBytes(filePath))
    }
}
728x90

댓글

💲 추천 글