2번의 프로젝트에서 둘다 Spring batch 를 사용했습니다.
첫번째 프로젝트에선 Tasklet 으로만 진행했지만 과연 성능의 차이가 있을까? 라는 생각을 갖게 되었고 두번째에선 Tasklet으로 먼저 구현하는데 시간이 얼마 걸리지 않아서 chunk processing 과 비교해보기로 했습니다.
절대 절대적인 지표가아니고 제가 만든 로직 차이일 수도있습니다. 아직 청크 프로세싱에대한 이해도가 낮아 생긴 성능이슈가 있을 수 있고 중간 foreach 같은 것들을 잘못돌려 생긴 차이가 클것이라 생각합니다.
@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));
}
@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개로 동일합니다.
@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 부분때문에 더 느려졌을거라고 예상하였습니다. 따라서 다음과 같이 리팩토링 진행
@RequiredArgsConstructor
public class JobProcess implements ItemProcessor<JobResponseDto, JobResponseDto> {
@Override
public JobResponseDto process(JobResponseDto item) throws Exception {
return item;
}
}
@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와 나눠서 갖는것은 맞는것인지 검토를 해봐야겠습니다.