Spring Batch) Chunk vs Tasklet

박우영·2023년 6월 7일
0

자바/코틀린/스프링

목록 보기
26/35

2번의 프로젝트에서 둘다 Spring batch 를 사용했습니다.

첫번째 프로젝트에선 Tasklet 으로만 진행했지만 과연 성능의 차이가 있을까? 라는 생각을 갖게 되었고 두번째에선 Tasklet으로 먼저 구현하는데 시간이 얼마 걸리지 않아서 chunk processing 과 비교해보기로 했습니다.

절대 절대적인 지표가아니고 제가 만든 로직 차이일 수도있습니다. 아직 청크 프로세싱에대한 이해도가 낮아 생긴 성능이슈가 있을 수 있고 중간 foreach 같은 것들을 잘못돌려 생긴 차이가 클것이라 생각합니다.

chunk

    @Bean
    @JobScope
    public Step chunkstep(JobRepository repository) {
        return new StepBuilder("chunk", repository)
                .<JobResponseDto, List<JobResponseDto>>chunk(1000, transactionManager)
                .reader(saraminReader())
                .processor(process())
                .writer(saraminWriter()).build();
    }

    @Bean
    @StepScope
    public ItemReader<JobResponseDto> saraminReader() {
        // 검색일 최소, 최대 값 설정으로 이후 값만 받아오기
        SaraminApiManager.saraminStatistic(84);
        SaraminApiManager.saraminStatistic(92);
        SaraminApiManager.saraminStatistic(2232);
        List<JobResponseDto> jobResponseDtos = SaraminApiManager.getJobResponseDtos();
        return new ListItemReader<>(jobResponseDtos);
    }

    @Bean
    @StepScope
    public JobProcess process() {
        List<JobResponseDto> list = new ArrayList<>();
        return new JobProcess(jobStatisticService, list);
    }

    @Bean
    @StepScope
    public ItemWriter<List<JobResponseDto>> saraminWriter() {
        return dto -> dto.getItems().forEach(d -> d.forEach(jobStatisticService::create));
    }

process

@RequiredArgsConstructor
public class JobProcess implements ItemProcessor<JobResponseDto, List<JobResponseDto>> {
    private final JobStatisticService service;
    private final List<JobResponseDto> jobResponseDtos;
    @Override
    public List<JobResponseDto> process(JobResponseDto item) throws Exception {
        jobResponseDtos.add(item);
        return jobResponseDtos;
    }
}

chunk는 100, 1000개로 테스트 해봤습니다. 별 차이가 없어서 바로 chunk과 테스크렛으로 넘어가겠습니다. 총 데이터는 272개로 동일합니다.

Tasklet

    @Bean
    @StepScope
    public Tasklet taskletSaramin() {
        return (contribution, chunkContext) -> {
            System.out.println("사람인 테스크렛");
            //TODO: 사람인 공고 기간 필터로직 추가
            /**
             * @param sectorCode 백엔드 84, 프론트 92, 풀스택 2232
             */
            SaraminApiManager.saraminStatistic(84);
            SaraminApiManager.saraminStatistic(92);
            SaraminApiManager.saraminStatistic(2232);
            return RepeatStatus.FINISHED;
        };
    }
    @StepScope
    @Bean
    public Tasklet taskletStatistic() {
        return (contribution, chunkContext) -> {
            List<JobResponseDto> pureSaram = SaraminApiManager.getJobResponseDtos();
            for (JobResponseDto dto : pureSaram) {
                try {
                    jobStatisticService.create(dto);
                } catch (IllegalStateException e) {
                    log.error(e.getMessage());
                }
            }
            return RepeatStatus.FINISHED;
        };
    }

위의 95번 인스턴스가 chunk, 96번 인스턴스가 Tasklet 방식입니다.

분명 청크프로세싱을 한다면 속도가 향상될 것이라고 생각했습니다. 배치로 일괄 작업을 하기때문에 성능이 더 나올거라는 예상과 다르게 너무 느렸습니다. foreach가 2번 돌고 한 차이라고 하더라도 272개밖에 안되는 데이터가 이렇게 차이나는것이 이해가 안가는데 아마 process 에서 add 로 더하고 jobResponseDtos 리턴한 것과 writer 부분때문에 더 느려졌을거라고 예상하였습니다. 따라서 다음과 같이 리팩토링 진행

process

@RequiredArgsConstructor
public class JobProcess implements ItemProcessor<JobResponseDto, JobResponseDto> {

    @Override
    public JobResponseDto process(JobResponseDto item) throws Exception {
        return item;
    }
}

chunk

    @Bean
    @JobScope
    public Step chunkstep(JobRepository repository) {
        return new StepBuilder("chunk", repository)
                .<JobResponseDto,JobResponseDto>chunk(1000, transactionManager)
                .reader(saraminReader())
                .processor(process())
                .writer(saraminWriter()).build();
    }

    @Bean
    @StepScope
    public ItemReader<JobResponseDto> saraminReader() {
        // 검색일 최소, 최대 값 설정으로 이후 값만 받아오기
        SaraminApiManager.saraminStatistic(84);
        SaraminApiManager.saraminStatistic(92);
        SaraminApiManager.saraminStatistic(2232);
        List<JobResponseDto> jobResponseDtos = SaraminApiManager.getJobResponseDtos();
        return new ListItemReader<>(jobResponseDtos);
    }

    @Bean
    @StepScope
    public JobProcess process() {
        return new JobProcess();
    }

    @Bean
    @StepScope
    public ItemWriter<JobResponseDto> saraminWriter() {
        return dto -> dto.getItems().forEach(jobStatisticService::create);
    }

Process 에서 List 형태였던것을 단일 객체로 변환해줬습니다. write 에서 쌓여서 한번에 처리하니 List로 하는건 사실 몇제곱의 시간이 걸리는 당연한 일이었는데 잘 못된 방식으로 접근했던거였습니다.


96번이 Tasklet 방식
98번이 개선된 chunk 방식입니다.

엄청난 차이는 아니지만 272개의 데이터 기준 0.02초 정도 차이가 나는것을 확인할 수있었습니다.

회고


적은 데이터에선 적게 차이나지만 더 많은 데이터에선 차이가 점점 날것이라고 생각합니다. 현재 프로젝트에선 약 3000개의 데이터를 가져와 속도가 엄청나게 향상될것 같진 않습니다. 1개의 job에 40~50분 정도 소요되는데 사실 크롤링을 하는것이 가장 오래걸리기 때문에 이 크롤링을 job 여러개로 만들어서 비동기방식으로 전환하고 db 에 insert into on duplicate key update 설정을 하여 db에서 처리하는방식도 고려해볼법 합니다. 하지만 cron 표현방식으로 새벽시간대에 1번을 실행하면 되는 것때문에 was 의 트래픽을 db와 나눠서 갖는것은 맞는것인지 검토를 해봐야겠습니다.

0개의 댓글