잘 정리해보자
Spring Batch - excel 파일 생성 (apache poi) 본문
spring batch에서 엑셀파일을 저장하는 기능을 구현하는데,
spring boot의 extension 라이브러리에서 excel 기능을 제공하지만, git을 clone받고 사용해야 해서 폐쇄망 처럼 제한된 환경에서 구현하기 위해 apache poi로 write 하는 소스를 기록한다.
(spring boot extension으로 하려는 경우
spring-boot-extension git 주소)
: https://github.com/spring-projects/spring-batch-extensions
구현하려는 Job 기능
: DB 데이터를 페이징으로 읽고 엑셀에 write 하는 배치
1. apache poi 를 받기 위해 gradle dependency에 적용.
build.gradle
implementation 'org.apache.poi:poi:3.15'
implementation 'org.apache.poi:poi-ooxml:3.15'
2. DB 데이터를 읽기 위한 pagingItemReader step 정의
Jpa - JpaPagingItemReader
Mybatis - MybatisPagingItemReader
@Bean
@JobScope
public Step excelStep1() throws Exception{
return stepBuilderFactory.get(JOB_ID + "_step1")
.<User, User>chunk(chunkSize)
.reader(excelReader1()) //DB reader
.writer(excelWriter1()) //excel writer
.listener(excelListener) //step listener (workbook open/close)
.build();
}
//엑셀 저장할 데이터 read
private ItemReader<? extends User> excelReader1() throws Exception{
JpaPagingItemReader reader = new JpaPagingItemReaderBuilder<User>()
.pageSize(chunkSize)
.entityManagerFactory(entityManagerFactory)
.queryString("select id,username from User")
.name("getTargetReader")
.build();
reader.afterPropertiesSet();
return reader;
}
> User 테이블의 id,username만 읽어오는 reader 정의
3. writer를 구현하기 전에, poi의 workbook의 open/close를 Step의 before/after 에 기능하도록 StepListener 를 별도로 구현한다.
StepExecutionListener 인터페이스로 step listener 구현.
(WriterListener로 write before/after로 구현해도 되지만
write 할 때마다 workbook을 open close 하는 과정이 계속 반복되어 속도가 저하되는 부분이 있었다.)
@Slf4j
@Component
public class TestExcelStepListener implements StepExecutionListener {
public XSSFWorkbook workbook;
public XSSFSheet sheet;
private String fileName = "test.xlsx";
private String outPath = "output/" + fileName;
//step 실행 전
@Override
public void beforeStep(StepExecution stepExecution) {
workbook = new XSSFWorkbook();
sheet = workbook.createSheet();
}
//step 실행 후
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
try{
FileOutputStream out = new FileOutputStream(outPath);
workbook.write(out);
workbook.close();
out.close();
return ExitStatus.COMPLETED;
}catch (IOException e){
log.error("IOException error : "+e.getMessage());
return ExitStatus.FAILED;
}
}
}
4. writer 구현
ItemWriter
private ItemWriter<User> excelWriter1() {
return items -> {
Row row = null;
Cell cell = null;
Iterator e = items.iterator();
while(e.hasNext()){
row = excelListener.sheet.createRow(rowIdx++);
Object[] t = (Object[]) e.next();
for(int i=0; i<Arrays.stream(t).count(); i++){
cell = row.createCell(i);
cell.setCellValue(t[i].toString());
}
}
};
}
Row, Cell은 for/while문 안에서 구현하면 메모리 증가 이슈로 인해서 상단에 null로 객체 구현.
(poi에 대한 설명은 생략)
실행 결과
resource/output/test.xlsx 파일
- 구현하면서의 이슈 사항
1. [Ljava.lang.Object; cannot be cast to
entity cast 에러 해결
https://choisblog.tistory.com/90
2. 메모리/ Out Of Memory 에러 (위의 예제가 아닌 대규모 배치 상황의 경우)
GC overhead limit exceeded
: 조회컬럼 6개 정도의 20만건 이상의 데이터를 write하는 과정에서 발생. (20만건 write하고 발생)
해결중인 방법
1. Row/Cell 을 4번과 같이 수정 (이전에는 for문안에 선언했었다.)
2. step before/after로 엑셀파일을 열고 닫는게 아니라, writer 할 때마다 열고 닫게 수정
-> 결과는 writer 속도가 저하되고 도중에
thread starvation or clock leap detected(메모리 에러와 동일) 에러 발생으로 중단.
3. 메모리를 늘린다.
-> 우선 최후의 방법으로 보류
4. 메모리 사용이 가능한 최대 (우선 10만건 정도) 에서 엑셀파일을 열고 닫는 작업으로 변경.
ex ) 페이징 처리된 데이터를 10만건이전까지 계속 write 하고 close.
그 이후 데이터는 이전 엑셀파일 데이터 뒤에 다시 write 반복 작업.
git 소스 (job>TestExcelJobConfig.java)
: https://github.com/wowchois/batchproject