티스토리 뷰

개발일지

엑셀 파일 유연하게 읽어보기

땅속 디그다 2022. 11. 7. 11:24

프로젝트 진행 도중 excel 파일에 대해 요청을 받고 그 엑셀 파일을 객체에 매핑시켜주는 기능을 작성해야하는 일이 생겼습니다.

아파치 에서 제공하는 excel xml 파서기인 아파치 poi 를 통해 해당 기능을 작성해보겠습니다.

 

다음과 같이 엑셀 파일이 존재할 경우 다음 객체에 매핑을 시켜줄 것입니다.

엑셀 파일을 처리하는 결과 값으로는 ExcelDAO 의 List 형태로 반납하게 됩니다.

또한 Reflection 과 제네릭을 활용하여 단순하게 하나의 객체에만 매핑하는 것이 아닌 다른 객체에도 매핑이 되게끔 만들어 보겠습니다.

 

사전지식을 알아가보겠습니다.

 

리플렉션 이란?

리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API 입니다.

여기서 로드된 클래스는 JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후 해당 클래스의 정보를 담은 Class 타입의 객체를 생성하여 메모리의 힙 영역에 저장해 둔것을 의미 합니다.

 

제네릭이란?

데이터의 타입을 일반화 하여 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법입니다.

일반화를 시켰기 때문에 특정 클래스에서만 사용할 수 있는 메서드가 아닙니다. 따라서 공통화된 처리를 시킬 수 있게 됩니다.

예제에서는 제네릭 메서드를 사용할 것이기 때문에 해당 선언에 대한 내용이 필요합니다. 간단하게 다음과 같이 선언 할 수 있습니다

접근제어자 [static] <T> 반환타입 메서드명(...)

현재 목표는 다음과 같습니다.

1. 엑셀파일을 읽어온다.

2. 각 row (행)을 ExcelDAO 의 필드에 매칭 시킨다. (이 과정에서 제네릭, 리플렉션이 사용되며 부가적으로 Annotation 을 활용하여 더욱 유연하게 매핑을 시도해 본다.)


 

 

아파치 poi 에 대해 의존성을 추가해보도록 하겠습니다.

 

// poi lab 의 의존성 추가
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
implementation group: 'org.apache.poi', name: 'poi', version: '5.2.0'
implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.0'

 

이제 본격적으로 poi 를 통해 엑셀을 객체로 매핑 해보겠습니다. 먼저 흐름은 다음과 같습니다.

1. 엑셀파일이 맞는지 여부 확인

2. 엑셀파일의 최상단 구분행을 읽어들이고 DAO 객체와 정보 매핑 (셀의 row index 번호를 알아야 한다.)

3. 정보를 가지고 있는 나머지 행들을 읽어 DAO 객체를 생성하고 set 한다.

 

구분행은 위의 엑셀 그림의 id, kind, inOut, superCategoryType, categoryType 입니다.

 

아파치 poi 의 경우 다음과 같은 자료구조를 가지고 있습니다.

엑셀 파일 (Workbook) -> 여러개의 시트 (Sheet) -> 여러개의 row(엑셀 행 하나하나) -> 여러개의 cell (칸 하나하나)

즉 위에서 말씀드린 각 row 의 cell 들을 DAO 필드에 하나하나 매핑시킵니다.

 

각 클래스들에 대한 세부 메서드는 직접 살피시는 편이 좋아보입니다.


엑셀 파일이 맞는지 여부부터 확인 해보겠습니다.

엑셀 파일의 경우 xls, xlsx 의 확장자를 가지고 있습니다. 따라서 String 비교를 통해 시도해보겠습니다.

 

아파치 poi 는 xls 형식의 경우 HSSFWorkbook 을 , xlsx 형식의 경우 XSSFWorkbook 생성자를 사용해야 합니다. 이 두개의 상위 타입인 Workbook을 반환하겠습니다.

 

자 이제 시트에서 구분 행을 불러들이겠습니다.

 

 

convertToDAO 가 결국 최종 완성할 함수입니다.

 

앞서 살펴번 getWorkbook 을 통해 workbook 을 가져왔고 workbook.sheetIterator를 통해 각 시트별로 구분행을 가져오게 됩니다. 규모를 크게 가져가게 된다면 각시트별로 맞는 DAO 객체를 만들고 매핑을 시켜줘야 하지만 예제에서는 하나의 시트만을 사용하는 엑셀파일이므로 넘어가도록 하겠습니다.

 

sheet.getFirstRowNum() 을 통해 값이 채워져 있는 행의 index 번호를 가져옵니다. (행의 index 는 0부터 시작)

createFinder 라는 메서드를 통해 ExcelDAO 필드가 엑셀 행의 몇번째 index 에 있는지 알 수 있는 Map 자료구조를 생성하겠습니다.

 

추가적으로 ExcelColumn 이라는 어노테이션을 만들어 유연하게 처리해보겠습니다.

 

이 어노테이션을 DAO에 매핑해보고 예제를 위해서 엑셀파일에 없는 DAO 필드인 person 또한 작성해 보겠습니다.

 

 

결론부터 말하면 createFinder는 다음과 같은 형태의 map 을 반환하게 됩니다.

만약 excel 구분자에 없는 필드 였다면 -1 을 만약 있는 행이라면 필드가 매핑해야 할 row의 cell index를 매핑해주는 것입니다.

또 @ExcelColumn 에 있는 inOut 부분으로 매핑을 시켜주게 됩니다.

 

create Finder 를 보게 되면 다음과 같습니다.

 

Class<T> daoClass 를 통해서 리플렉션 api 를 이용해 Field 객체 배열을 가져오고 각 Field 의 어노테이션 존재 유무에 따라서 어노테이션의 name 으로 엑셀을 매핑할 것인지, 필드 변수 이름으로 엑셀을 매핑할 것인지 정하게 됩니다.

 


자 이제 마지막 3번 부분 작성을 통해 실제 DAO 객체에 매핑을 해보도록 하겠습니다.

rowToDAO 메서드를 사용하여 해당 row 에 맞는 ExcelDAO 객체를 반환하게 됩니다.

 

rowToDAO 함수를 살펴보면 다음과 같습니다.

 

매개변수가 없는 기본생성자를 통해 T 타입 객체를 생성하고 finder 를 통해 현재 필드 변수가 row의 몇번째 cell을 읽어야하는 지를 알고 set 을 하게 됩니다.

 

주목해야 할 점은 private, protected 접근제어자의 경우 field.setAccessible 을 통해 접근 true 를 하고 set 을 해야합니다.

해당 내용은 Spring 에서 제공하는 @RequestBody, @ResponseBody 와 같은 원리라고 볼 수 있습니다.

 

두개의 어노테이션 모두 Spring 에서 제공하는 메시지 컨버터를 사용하여 (내부적으로 reflection 동작) 객체를 생성, set 을 하게 됩니다.

엑셀 파일의 경우에 이 mapping 과정이 가장 까다로운데, cell 별 타입에 따라 사용해야하는 함수가 다르기 때문입니다. 따라서 matchFields 함수를 통해서 cell 의 값을 가져오고 가공하여 반환해보겠습니다.

 

switch - case 문을 통해 각 Cell의 type 에 따라 result 를 반환 하겠습니다. result는 문자열, 숫자, 소수(숫자) 등 다양한 형태로 반환이 되어야 하기 때문에 result 의 타입은 Object 입니다.

 

추가적으로 아파치 poi 의 경우 NUMERIC 의 경우 무조건 double 로 반환하기 때문에 필드에 Long, 이나 Integer 같이 포함 되어 있을 경우 별도로 정수 타입으로 형변환이 필요합니다.

 

이제 마지막으로 테스팅을 하여 올바르게 매핑이 되었는지 확인해 보겠습니다.

아주 깔끔하게 잘 매핑된것을 확인 할 수 있습니다.

DAO 객체가 변경이 되어도 올바르게 작동하는지 살펴보겠습니다.

 

 

댓글
05-20 11:54
Total
Today
Yesterday
링크