디지안의 개발일지
표로 만든 엑셀 쉽게 읽어서 사용하기(with Apache POI) 본문
개요
자바 진영에서 엑셀을 다루는 대표적인 라이브러리는 Apache POI다. 사용법을 보니 굉장히 보기 싫다. 전능하신 ChatGPT님께 기본 사용법을 물어보니 기본적인 코드는 아래와 같다.
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ExcelReader {
public static void main(String[] args) {
String excelFilePath = "example.xlsx";
try (InputStream inputStream = new FileInputStream(new File(excelFilePath))) {
Workbook workbook = new XSSFWorkbook(inputStream);
Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트를 가져옴
for (Row row : sheet) {
for (Cell cell : row) {
printCellValue(cell);
}
System.out.println();
}
workbook.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void printCellValue(Cell cell) {
switch (cell.getCellType()) {
case STRING:
System.out.print(cell.getStringCellValue() + "\t");
break;
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
System.out.print(cell.getDateCellValue() + "\t");
} else {
System.out.print(cell.getNumericCellValue() + "\t");
}
break;
case BOOLEAN:
System.out.print(cell.getBooleanCellValue() + "\t");
break;
case FORMULA:
System.out.print(cell.getCellFormula() + "\t");
break;
case BLANK:
System.out.print("\t");
break;
default:
System.out.print("\t");
break;
}
}
}
코드를 보면 원하는 시트를 읽어서 row에 있는 각 cell의 값을 가져와 타입이 무엇인 체크하여 콘솔에 출력하는 형태로 되어 있다.
문제점
그렇다면 우리는 또 우리가 원하는대 형태에 데이터 클래스에 값을 넣기 위해 0번은 이름, 1번은 나이 등등 다시 넣는 작업을 코드로 다시 작성해야겠지? 그 과정이 너무 비효율적인 반복이라는 생각이 들어 빠르게 Apache POI를 감싸서 사용해야겠다고 생각이 들었다. 실제로 만드는데 3시간 정도 걸린거 같다.
요구사항
내가 원했던 것은 엑셀로 아래와 같은 데이터가 있을 때
번호 이름 나이
1 김씨 20
2 이씨 51
3 최씨 44
4 민씨 32
5 안씨 34
아래와 같은 객체로 바로 매핑되면서 리스트로 만들어주는 것이었다.
data class Person (
val no: String, // 1
val name: String, // 이씨
val age: String, // 나이
)
코드
의존성
의존성을 아래와 같이 추가한다.
/**
* for excel
*/
implementation("org.apache.poi:poi:5.3.0")
implementation("org.apache.poi:poi-ooxml:5.3.0")
메타데이터
엑셀에서 몇번째 시트를 사용할지 row을 몇번째부터 시작할지에 대한 값이 필요하다. 그렇기 때문에 메타 정보를 저장하고 있을 클래스가 필요하다.
/**
* 엑셀에 대한 메타 정보
*
* @property skip 보통 엑셀의 row의 첫번째 줄이 각 컬럼이 어떤 값을 의미하는지를 나타낸다.
* 그래서 그 값을 skip이 필요한데 몇번째 skip인지 알기 위한 값이다.
* @property sheetNo 엑셀에서 몇번째 sheet에 존재하는지 알려주는 값이다.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExcelMeta(
val skip: Int,
val sheetNo: Int,
)
엑셀 인터페이스
수행하려는 로직이 모든 클래스가 할 수 있으면 안되니 클래스에 대한 제한이 들어가야된다. 그래서 특정 인터페이스를 지정한다.
/**
* 엑셀을 객체로 변경할 대표적인 인터페이스
*
* 주의할 점
* 1. 프로퍼티 순서가 곧 엑셀 컬럼 순서가 된다.
* 2. 모든 프로퍼티 값은 타입을 String으로 선언해야한다.
*/
interface Excel
엑셀 클래스로 변환
코틀린 리플렉션을 사용하여 각 row의 값을 프로퍼티에 삽입할 수 있도록 코드를 구성한다.
class ExcelConverter {
fun <T : Excel> convert(byteArray: ByteArray, kClass: KClass<T>): List<T> {
// 프로퍼티의 수를 세서 프로퍼티 수만큼 가져온다.
val propertyTotal = kClass.memberProperties.size
val result = mutableListOf<T>()
ByteArrayInputStream(byteArray).use { inputStream ->
val excelMeta = kClass.findAnnotation<ExcelMeta>()
?: throw RuntimeException("not found excelMeta")
val sheet = getSheet(inputStream, excelMeta)
sheet.forEachIndexed { rowNo, row ->
// 엑셀의 row을 사용할 때 skip 이상의 row만 사용하도록 설정
if (rowNo >= excelMeta.skip) {
result.add(rowToClass(row, propertyTotal, kClass))
}
}
}
return result.toList()
}
private fun getSheet(inputStream: ByteArrayInputStream, excelMeta: ExcelMeta): Sheet {
val workBook = WorkbookFactory.create(inputStream)
return workBook.use {
workBook.getSheetAt(excelMeta.sheetNo)
}
}
private fun <T : Excel> rowToClass(
row: Row,
propertyTotal: Int,
kClass: KClass<T>
): T {
val params = mutableListOf<String>()
row.forEachIndexed { cellNo, cell ->
// 프로퍼티 수만큼만 row의 column 값을 가져온다.
if (cellNo < propertyTotal) {
params.add(cell.toString())
}
}
return kClass.primaryConstructor?.call(*params.toTypedArray()) ?: throw RuntimeException("create fail excel")
}
}
테스트
더 많은 예외 로직이 필요하겠지만 일단 되는 것만 보자면 아래와 같이 구성하면 된다.
@ExcelMeta(skip = 1, sheetNo = 0)
data class PersonExcel(
val no: String,
val name: String,
val age: String,
): Excel
@Test
fun `엑셀을_클래스로_변경할_수_있다`() {
val resource = ClassPathResource("person/person.xlsx")
val bytes = resource.file.readBytes()
val convert = excelConverter.convert(bytes, PersonExcel::class)
convert.size shouldBe 3
}
결론
Apache POI 덕분에 엑셀을 다룰 수 있지만 너무 낮은 레벨의 라이브러리다보니 사용하기 불편한 점이 있다. 이런건 이렇게 간단하고 빠르게 한번 감싸서 사용하는 것이 더 편리한 것 같다.
'Kotlin' 카테고리의 다른 글
Kotlin으로 AWS Lambda 실행하기 (0) | 2023.06.10 |
---|---|
[TIL] 이펙티브 코틀린 - 3 장 (0) | 2022.03.20 |
[TIL] 코틀린에서 Unit이란 타입을 만든 이유가 무엇일까? (1) | 2022.03.17 |
[TIL] Effective Kotlin을 읽어보자 - 2 장 (0) | 2022.03.07 |
[TIL] Effective Kotlin을 읽어보자 - 책 소개편 (0) | 2022.03.04 |