들어가기에 앞서

관리자 혹은 유지보수 툴에서는, 화면에 보여지는 데이터를 Excel 파일 형식으로 다운받을 일이 많다.

흔히 Excel Controller (API Gateway의 일종으로 이름은 그냥 관용적인 표현인 듯)에서 요청을 받으면, 주어진 조건에 맞는 데이터를 xlsx 파일로 만들어 응답해주는 형태로 구성된다.

현재 진행하는 프로젝트에서도 관리자 페이지 구현시, 스프레드 시트 형태의 데이터를 다운받도록 하는 엔드페이지가 많다.

문제는 지금까지 주먹구구식으로 하드코딩 느낌나는 Excel Service 를 만들어 왔다.

이를 좀 더 나이스한 방법으로 개선해보고자 작업을 진행하였다.

 


AbstractXlsxView 상속

Excel Service 를 만들다 보면 반복되는 부분이 상당히 많다.

셀 속성 지정이라든지, 다른 칼럼 다른 데이터지만 같은 데이터 입출 방식 이라든지.

 

따라서 이런 공통 부분을 처리해주는 부분이 필요했다.

먼저 spring framework 에서 제공해주는 AbstractXlsxView 추상클래스를 상속한 또 다른 추상클래스를 하나 만들기로 하였다.


( ColumnEnum 과 FileDescriptorEnum 는 1번 파트 하단부에 나온다. )

우리 팀에 맞게끔 피팅을 시작했다.

 

다음과 같은 추상 함수들을 선언했다.

initDataSetConsumer 는 dataSetConsumerList 를 초기화 하기 위한 메소드이다. 자식클래스에서 이 작업을 해주어야 하는 이유는, 가변적인 Column 갯수와 실제 데이터를 알고 있는 건 자식클래스이기 때문이다.

getDataSize 메소드는 6번 하단부에 설명이 나온다.

 


1. 상수로 사용할 필드 선언

 

각 값들의 역할은 다음과 같다.

Variable Type Description
columns E1[] 자식 클래스에서 받을 값으로, Header Row 에 매핑할 Column 명 및 column width 같은 메타데이터를 가지고 있다.
fileDescriptors E2[] 자식 클래스에서 받을 값으로,

EXPORT_FILE_NAME,
EXCEL_SHEET_NAME,
FROM_DATE_FORMAT,
TO_DATE_FORMAT

값들을 가지고 있다.

dataSetConsumerList List<Function> 실제 Data Record Rows 를 채워줄 값들이 필요하다. 이 값들을 가져오는 Function 객체들의 List 객체이다.

역시 자식 클래스에서 받을 값으로, 각 열에 해당하는 Data 들을 어떻게 지정할건지 명세해주어야한다.

FILE_DESCRIPTOR_LENGTH int fileDescriptors 의 length 유효성 검사를 위해 사용한다. 자식 클래스에서 필요한 값들을 전부 명시하였는지를 판별할 때 사용
EXPORT_FILE_NAME String 출력할 xlsx 파일의 이름
EXCEL_SHEET_NAME String Sheet 에 매핑할 이름
FROM_DATE_FORMAT String 입력 DateTimeFormatter 객체인 inputDateTimeFormatter 변수에 할당할 DateTime 포맷
TO_DATE_FORMAT String 출력 DateTimeFormatter 객체인 outputDateTimeFormatter 변수에 할당할 DateTime 포맷

 

 

columns 와 fileDescriptors 는 처음에 선언만 돼있고 정의하지 않는다. 이 배열의 값은 자식 클래스에서 채워준다.

 

E1 E2 제네릭은 구현하는 개발자 입장에서 조금이나마 이해하기 쉽도록 보조해놓은 것이다. 절대 필수사항이 아니다. 편하게 구현하고자 하면 아래 내용은 생략하면된다.

 

본 추상 클래스에서 E1 E2 는 각각 다음 인터페이스를 상속받은  Enum 타입만 받을 수 있다.

 

 

 


 

2. buildExcelDocument 메소드 Overriding

AbstractXlsxView 의 buildExcelDocument 추상함수를 구현해야 한다. 넘어오는 workbook 객체에 Header Row 나 Data Record Rows 를 매핑해주면 된다.

 

그 전에, init 메소드를 하나 만들어 buildExcelDocument 함수가 시작할 때 init 함수를 호출하도록 하자.

init 함수의 역할

중복되는 부분을 최소화 하기 위해 AbstractXlsxView 을 wrapping 한 추상 클래스를 만든다는 점을 생각해보자.

각 엔드페이지에서 요구하는 Header Row 의 Column 명이나, Sheet 명, 파일명등이 다를 수 있다.

init 함수에서는 이런 정보를 자식 클래스로 부터 받아 동적으로 매핑한다.


먼저 Column 갯수와 dataSetConsumerList 의 Column 갯수가 같은지 유효성을 검사한다.

다음으로 fileDescriptors 의 갯수가 명시한 FILE_DESCRIPTOR_LENGTH 만큼 들어왔는지 유효성을 검사한다.

다음으로는 값들을 꺼내오고 필요한 곳에 매핑해주면 된다.

 

 


 

3. File Description 파트

이 파트에서는 출력할 xlsx 파일명, HttpServletResponse 객체에 지정할 header 등의 설정을 진행한다.

우리 팀에서는 엔드 페이지마다 고정되는 prefix 가 있고, 거기에 timestamp 를 붙이는 방식을 사용하고 있다.

 

 


4. Sheet Initializing 파트

이 파트에서는 Sheet 명, Header Row 에 지정할 CellStyle 객체, Data Record Rows 에 지정할 CellStyle 객체를 선언및 정의한다.

 

 


 

 

5. Header Row Initializing 파트

이 파트에서는 Header Row 에 사용할 Row 객체를 만들고, 사용할 Column 명 등을 지정해준다.

 

이 파트에서는 Header Row 의 Column 을 지정해주는 이 setSheetHeader 함수가 핵심이다.

 

 

본 추상 클래스를 상속받은 클래스는 Column 정보가 들어있는 Enum 객체를 구현해서 파라미터로 클래스타입토큰을 전달해주어야한다. (해당 Enum 의 constants 를 추출해 columns 에 매핑하는 것이다)

이에 대한 내용은 아래쪽에서 기술할 예정이다.

 

아무튼, 자식 클래스로부터 받은 columns 정보를 추출해 각 index 에 맞는 cell 에 Header value 를 매핑한다.

말이 어렵지 헤더가 될 Row 를 받아 index 별로 값을 넣어주는 것이다.

나는 columns 가 가져야 할 필드로 index, column width, column key 로 지정하였다.

 

 


 

 

6. Data Record Initializing 파트

먼저 row 에 새로운 Row 를 할당한다. ( createRow(인덱스) )

 

다음으로 dataSetConsumerList 로부터 값을 받아온다. dataSetConsumerList  에는 각 column index 별로 Function을 가지고 있다.

해당 Function 은 row index 를 받게 돼있다. row index 값을 주면 자식 클래스가 가지고 있는 data 를 파싱해 값을 return 해주도록 한다.

return 받은 cellData 변수를 row 의 column index 에 맞게 매핑해주면 끝이다.

 

 

위 코드를 보면 For loop 내에서 getDataSize() 메소드를 호출하는 부분이 있다.

이 메소드는 자식클래스에서 구현해야할 추상메소드이다. 왜냐하면 dataSetConsumerList 의 size 는 column 갯수이기 때문이다.

실제 data를 들고있는 건 자식클래스이기 때문에 자식클래스에서 몇 개의 rows(records) 를 가지고 있는지 알려주어야 한다.

 


자식클래스 구현

1. 클래스 정의

먼저 CustomAbstractXlsxView  상속받는다.

이후 실제 데이터를 쥐고 있을 List 객체를 선언하고, 생성자를 통해 전달받는다.

 

setColumnAndFileDescriptors 은 추상클래스 1번 항목 중반부에 설명을 참조하자.

 

Column 과 FileDescriptor Enum 상수를 선언 및 정의하고 해당 클래스 타입을 넘겨주면 된다.

Column 은 아래 2번, FileDescriptor 는 아래 3번, initDataSetConsumer 는 아래 5번 항목을 참조하자.

 

 


 

2. Column 정보를 가진 Enum 선언 및 정의

 

 


 

3. FileDescriptor 정보를 가진 Enum 선언 및 정의

 

 


 

4. getDataSize 구현

 

 


 

5. initDataSetConsumer 구현

2,3,4번 항목은 대충 눈대중으로 보면 이해가 간다.

initDataSetConsumer 부분은 조금 더 복잡하다.

 

 

아까 CustomAbstractXlsxView 의 6번 항목에 다음과 같이 설명하였다.

다음으로 dataSetConsumerList 로부터 값을 받아온다. dataSetConsumerList  에는 각 column index 별로 Function을 가지고 있다.

 

설명 그대로 주어진 index 를 가지고 data 객체에 접근하여 필요한 field 를 반환해주는 Function 객체를 column 순서대로 add 하면 된다.

 

 

 


 

 

마치며

간단하게 쓰려고 했는데 배보다 배꼽이 더 커진 것 같은 기분이 든다.

복잡해 보이는 내용이지만 사실 구현하다보면 당연한 내용들 뿐이라는 것을 알 수 있다.

 

시간이 되면 어느정도 필드명들을 검열한 뒤 gist snippet 으로 올릴 예정이다.

설명이 너무 장황해 보는사람이 불편할 것으로 추정되기에…

 

 

p.s. 사실 중간에 파트가 하나 빠진게 있는데, 이건 우리 회사 특성상 필요한 파트였다.

글로벌 서비스를 운영하기 때문에 localizing 매핑해주는 파트가 필요했다. 일반적인 내용은 아니므로 본문에서는 제외하였다.

 

 

 

 

 


댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

This site uses Akismet to reduce spam. Learn how your comment data is processed.