표로 만든 엑셀 쉽게 읽어서 사용하기(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 덕분에 엑셀을 다룰 수 있지만 너무 낮은 레벨의 라이브러리다보니 사용하기 불편한 점이 있다. 이런건 이렇게 간단하고 빠르게 한번 감싸서 사용하는 것이 더 편리한 것 같다.