잘 정리해보자

Spring Batch - excel 파일 생성 (apache poi) 본문

Spring/Spring Boot

Spring Batch - excel 파일 생성 (apache poi)

토마토오이 2022. 2. 12. 11:05

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

 

GitHub - spring-projects/spring-batch-extensions: Spring Batch Extensions

Spring Batch Extensions. Contribute to spring-projects/spring-batch-extensions development by creating an account on GitHub.

github.com

 

 

 

 

구현하려는 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

 

 

JPA Error : [Ljava.lang.Object; cannot be cast to

에러메시지 : [Ljava.lang.Object; cannot be cast to .. 원인 : query로 entity를 가져와 형변환 하는 경우 발생. JPA에서 native query로 데이터를 가져올 때 흔히 발생하는 에러라고 한다. 반환 타입은 Object[..

choisblog.tistory.com

 

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

 

GitHub - wowchois/batchproject: init commit

init commit. Contribute to wowchois/batchproject development by creating an account on GitHub.

github.com

 

Comments