개발 일기

[Spring Batch] Spring Batch에서 Tasklet 방식과 Chunk 방식 비교 본문

Back-End/Spring

[Spring Batch] Spring Batch에서 Tasklet 방식과 Chunk 방식 비교

개발 일기장 주인 2024. 9. 19. 21:07

Spring Batch에서 데이터를 처리하는 방법에는 Tasklet방식과 Chunk방식이 있다.

이 두 가지 비교를 통해 각각 어떤 상황에 쓸 수 있을지 알아 두어야 적절한 데이터 처리 방식을 채택하여 배치 처리를 할 수 있을 것 같아 정리하게 됐다.

 

Tasklet 처리 방식

Tasklet

  • Tasklet은 Spring Batch에서 사용되는 인터페이스로, 배치 작업에서 단일 작업(task)을 수행하기 위한 구성 요소이다.
  • 해당 데이터 처리 방식에서 Step은 하나의 Tasklet으로 구성된다.
  • Tasklet을 사용하면 하나의 작업을 간단하게 실행할 수 있으며, 특정 로직을 정의하여 배치 작업 중 반복적으로 실행시킬 수 있다.
  • Tasklet의 execute() 메서드가 호출될 때마다 단일 트랜잭션이 생성되며 Tasklet의 전체 실행이 하나의 트랜잭션으로 묶인다.
    즉, execute()이 한번 실행될때마다 하나의 트랜잭션 범위가 되는 것이다.
  • 아무래도 처리 메소드(execute())가 하나로 되어 있고 그 직접 구현한 메소드를 반복적으로 실행하기 때문에 처리 로직이 복잡하지 않은 경우 적합하고 대용량 데이터를 다루는 경우 적합하지 않다.
  • 데이터 읽기/처리/쓰기와 같은 반복적인 작업보다는 파일 삭제, 데이터 초기화와 같은 비반복적이고 명확한 작업 그리고 대용량 데이터가 아닌 배치 처리에 적합
즉, Tasklet 지향 처리란, 단일 작업을 수행하는 Tasklet을 사용하여 한 번에 한 작업을 처리하고, 그 작업이 끝나면 트랜잭션이 완료되는 구조를 의미

Tasklet Interface

위 코드가 Tasklet 인터페이스이며 단일 메소드인 execute()를 제공하고 있다. execute()의 응답 객체인 RepeatStatus는 Enum 타입으로 RepeatStatus.FINISHED와 RepeatStatus.CONTINUABLE 이렇게 두 가지가 있다.
예외가 터지거나 RepeatStatus.FINISHED를 반환할때까지 반복적으로 execute()를 반복한다.

@Bean
public Step step() {
    return stepBuilderFactory.get("step")
            .tasklet((contribution, chunkContext) -> {
                // 비즈니스 로직 작성
                return RepeatStatus.FINISHED;
            })
            .build();
}

위와 같이 익명 클래스와 람다 표현식을 통해 Step 선언 시에 tasklet을 함께 선언할 수 있으며 아래와 같이 별도 클래스로 따로 정의한 후 끌어다 쓰는 방법이 있다. 위의 코드가 더 간결해보이기 때문에 로직만 단순하다면 위의 방식으로 처리하는 것이 좋겠지만 로직이 복잡하다면 다소 코드의 가독성을 해칠 수 있으므로 아래와 같이 따로 정의하여 가독성, 재사용성, 확장성을 가져오는 방식으로 구현하는 것이 좋을 것 같다.

@Bean
public Step step() {
    return stepBuilderFactory.get("step")
            .tasklet(new CustomTasklet())
            .build();

}

public class CustomTasklet implements Tasklet {

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        // 비즈니스 로직 작성
        return RepeatStatus.FINISHED;
    }
}

람다식 vs 클래스 따로 정의


두번째로는 Chunk기반 처리 방식이다. 

Chunk 기반 처리 방식

Chunk

  • 대략적으로 Chunk의 뜻은 '하나의 덩어리(묶음)'라는 뜻으로 생각하면 된다.
  • 스프링 배치에서의 Chunk란? 데이터 덩어리로 작업 할 때 각 커밋 사이에 처리되는 row 수
  • 즉, 전체 처리해야할 Task를 여러 개의 묶음(Chunk)들로 분리하여 처리하는 것이다.
  • 하나의 Chunk가 하나의 트랜잭션 범위가 된다.
    실패할 경우엔 해당 Chunk 만큼만 롤백이 되고, 이전에 커밋된 트랜잭션 범위까지는 반영이 되는 것이다.
  • Step은 ItemReader, ItemProcessor, ItemWriter 이렇게 3가지로 구성된다.
    ItemReader와 ItemProcessor에서 데이터는 1건씩 처리되고 ItemWriter에선 Chunk 단위로 한번에 처리되는 것이다.
  • 대용량 데이터에 대해 읽기/처리/쓰기와 같은 반복적이며 상대적으로 복잡한 배치 작업을 처리할때 적합하다.

Chunk-oriented Processing

즉, Chunk 지향 처리란 한 번에 하나씩 데이터를 읽어 Chunk라는 덩어리를 만든 뒤, Chunk 단위로 트랜잭션을 다루는 것을 의미한다!
@Configuration
@RequiredArgsConstructor
public class ChunkBatchConfig {
    private final JobRepository jobRepository;
    private final PlatformTransactionManager platformTransactionManager;
    private final BeforeEntityRepository beforeEntityRepository;
    private final AfterEntityRepository afterEntityRepository;

    @Bean
    public Job transferDataJob() {
        System.out.println("transfer data job");
        return new JobBuilder("transferDataJob", jobRepository)
                .start(transferDataStep())
                .build();
    }

    @Bean
    public Step transferDataStep() {
        System.out.println("transferData");
        return new StepBuilder("transferData", jobRepository)
                .<BeforeEntity, AfterEntity> chunk(10, platformTransactionManager)
                .reader(beforeReader())
                .processor(middleProcessor())
                .writer(afterWriter())
                .build();
    }

    @Bean
    public RepositoryItemReader<BeforeEntity> beforeReader() {
        return new RepositoryItemReaderBuilder<BeforeEntity>()
                .name("beforeReader")
                .pageSize(10)
                .methodName("findAll")
                .repository(beforeEntityRepository)
                .sorts(Map.of("id", Sort.Direction.ASC))
                .build();
    }

    @Bean
    public ItemProcessor<BeforeEntity, AfterEntity> middleProcessor() {
        return new ItemProcessor<BeforeEntity, AfterEntity>() {
            @Override
            public AfterEntity process(BeforeEntity item) throws Exception {
                AfterEntity afterEntity = AfterEntity.builder()
                        .username(item.getUsername())
                        .build();
                return afterEntity;
            }
        };
    }

    @Bean
    public RepositoryItemWriter<AfterEntity> afterWriter() {
        return new RepositoryItemWriterBuilder<AfterEntity>()
                .repository(afterEntityRepository)
                .methodName("save")
                .build();
    }
}