728x90
요구사항
- 대용량 데이터 엑셀 다운로드
- 파라미터에 따라 동적으로 조건이 바뀌게 해야 함
- DB: MongoDB
방법 구상: 스트림 방식으로 데이터를 뽑아오고 ➜ 엑셀을 스트림으로 채우고 ➜ 엑셀 내보내기
구현
알아보니 스프링부트에서는 엑셀기능을 구현할 때 poi라는 기능을 사용할 수 있다고 한다.
// build.gradle
implementation('org.apache.poi:poi-ooxml:5.2.2')
Repository
interface CustomRepository {
fun streamSearch(requestDto: RequestDto): Stream<Example>
}
@Repository
class CustomRepositoryImpl(@Autowired private val mongoTemplate: MongoTemplate): CustomRepository {
private fun createQuery(requestDto: RequestDto): Query {
val query = Query()
val sort = Sort.by(Sort.Direction.DESC, "createdAt")
// 동적 쿼리 예시
addCriteria(query, "exam1", requestDto.exam1)
addCriteria(query, "exam2", requestDto.exam2)
addCriteria(query, "exam3", requestDto.exam3)
addCriteria(query, "exam4", requestDto.exam4)
addCriteria(query, "exam5", requestDto.exam5)
query.with(sort)
return query
}
private fun addCriteria(query: Query, field: String, value: String?) {
value?.let {
query.addCriteria(Criteria.where(field).`is`(it))
}
}
override fun streamSearch(requestDto: RequestDto): Stream<Example> {
val query = createQuery(requestDto)
val iterator = mongoTemplate.stream(query, Example::class.java)
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false)
.onClose { iterator.close() }
}
}
- streamSearch 함수는 MongoDB를 사용하여 데이터를 검색한다.
- 검색 조건은 RequestDto 객체를 통해 받아옵니다. 이 객체는 여러 필드(exam1, exam2, exam3, exam4, exam5)의 값을 포함할 수 있다.
- 이 검색 조건을 바탕으로, createQuery 함수를 통해 동적 쿼리를 생성한다.
- 이 쿼리는 mongoTemplate.stream을 사용하여 실행되며, 그 결과는 CloseableIterator로 반환된다.
- 마지막으로, 이 CloseableIterator를 java.util.stream.Stream으로 변환하고, 이 과정에서 Spliterators.spliteratorUnknownSize와 StreamSupport.stream을 사용한다.
- 그리고 스트림이 닫힐 때(onClose), CloseableIterator는 적절히 닫혀서 자원이 해제된다.
Service
@Service
@Transactional
class ServiceImpl(
private val customRepository: CustomRepository,
) : Service {
// 헤더 스타일을 생성하는 함수
private fun createHeaderStyle(workbook: XSSFWorkbook): CellStyle {
val headerStyle = workbook.createCellStyle()
val font = workbook.createFont()
font.bold = true
font.fontHeightInPoints = 12
font.color = IndexedColors.WHITE.getIndex()
// 스타일에 폰트를 설정
headerStyle.setFont(font)
headerStyle.fillForegroundColor = IndexedColors.SKY_BLUE.getIndex()
headerStyle.fillPattern = FillPatternType.SOLID_FOREGROUND
return headerStyle
}
// 헤더를 생성하는 함수
private fun createHeaders(sheet: XSSFSheet, headers: Array<String>, headerStyle: CellStyle) {
val headerRow = sheet.createRow(0)
headers.forEachIndexed { index, header ->
val cell = headerRow.createCell(index)
cell.setCellValue(header)
cell.cellStyle = headerStyle
}
}
// 각 열의 너비를 설정하는 함수
private fun setColumnWidths(sheet: XSSFSheet, headers: Array<String>, widths: Map<String, Int>) {
headers.forEachIndexed { index, header ->
val width = widths[header] ?: 10 // 만약 헤더가 맵에 없다면 기본값으로 10을 사용
sheet.setColumnWidth(index, width * 256)
}
}
// 데이터를 엑셀에 작성하는 함수
private fun writeDataToSheet(sheet: XSSFSheet, example: Stream<Example>) {
var index = 0
example.forEach { data ->
val row = sheet.createRow(++index)
// 여기에 데이터를 채우는 코드를 작성
row.createCell(0).setCellValue(data.exam1)
row.createCell(1)
.setCellValue(data.exam2.substring(0, data.exam2.length.coerceAtMost(32767))) // 엑셀의 글자수 제한 대응 예시
row.createCell(2).setCellValue(data.exam3)
row.createCell(3).setCellValue(data.exam4)
row.createCell(4).setCellValue(data.exam5)
// ...
row.createCell(n).setCellValue(data.createdAt)
}
}
// 응답으로 엑셀 파일을 보내는 함수
private fun writeWorkbookToResponse(response: HttpServletResponse, workbook: XSSFWorkbook) {
val out = ByteArrayOutputStream()
workbook.write(out)
val body = out.toByteArray()
response.contentType = "application/vnd.ms-excel"
response.setHeader("Content-Disposition", "attachment; filename=example.xlsx")
response.outputStream.write(body)
}
// 엑셀 파일로 저장하는 함수
override fun saveExcel(
requestDto: RequestDto,
response: HttpServletResponse
): ResponseEntity<Unit> {
// 데이터를 검색하여 스트림 형태로 받아옴
val streamData = customRepository.streamSearch(requestDto)
// 새로운 엑셀 워크북을 생성
val workbook = XSSFWorkbook()
// "Sheet1"라는 이름의 새로운 시트를 생성
val sheet = workbook.createSheet("Sheet1")
// 헤더 스타일을 생성
val headerStyle = createHeaderStyle(workbook)
// 헤더를 생성
val headers = arrayOf("컬럼1", "컬럼2", ... ,"생성일")
createHeaders(sheet, headers, headerStyle)
// 각 열의 너비를 설정
val widths = mapOf("컬럼1" to 10, "컬럼2" to 25, ... "생성일" to 25)
setColumnWidths(sheet, headers, widths)
// 데이터를 엑셀에 작성
writeDataToSheet(sheet, streamData)
// 응답으로 엑셀 파일을 보냄
writeWorkbookToResponse(response, workbook)
return ResponseEntity.ok().build()
}
}
요번에는 대용량 데이터 엑셀 다운로드를 한 경험을 기반으로 블로그에 작성해 봤습니다. 옮겨 적는 부분에서 오타나 빠뜨린 부분이 있을 수도 있습니다. 잘 안 되는 부분이나 오류가 나는 부분이 있으면 댓글로 공유 부탁드려요! 언제나 잘못된 설명이나 부족한 부분에 대한 피드백은 환영입니다🤍
728x90