파트 3. Step 구현 방식: Tasklet과 Chunk 지향 처리
본 문서는 Step 구현 방식인 Tasklet과 Chunk 지향 처리에 대해 정리한 기록입니다.
스프링 배치(Spring Batch)를 활용하여 배치 인프라를 구축할 때, 개발자가 가장 먼저 내려야 하는 아키텍처적 결정은 "이 단계를 Tasklet으로 구현할 것인가, 아니면 Chunk 지향 방식으로 구현할 것인가?"입니다. 두 방식 모두 자바 코드로 배치를 돌린다는 점은 같지만, 내부적인 트랜잭션 관리, 메모리 활용법, 그리고 코드 구조가 완전히 다릅니다. 실무에서 올바른 선택을 할 수 있도록 두 방식의 작업 의미, 구성, 코드상 차이점을 명확하게 정리해 보겠습니다.
1. Tasklet 방식: 단발성 단일 작업
작업의 의미
Tasklet은 단일 비즈니스 로직을 단발성(One-shot)으로 수행하고 끝내는 구조입니다. 전체 데이터를 한 건씩 읽어서 가공할 필요 없이, 하나의 커다란 통짜 작업을 한 번에 처리해야 할 때 사용합니다. 대용량 데이터의 이동이나 변환보다는 시스템적인 제어나 초기화 작업에 적합합니다.
구성 요소
- Tasklet 인터페이스:
execute()메서드 하나만 존재하며, 이 메서드 내부에서 모든 로직이 실행됩니다. - RepeatStatus: 작업 완료 후 배치의 상태를 결정합니다.
RepeatStatus.FINISHED를 반환하면 단계가 종료되고,RepeatStatus.CONTINUABLE을 반환하면 조건이 충족될 때까지execute()를 계속 반복 실행합니다.
코드 구조
Tasklet 방식 코드 예시
@Bean
public Step fileDeleteStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("fileDeleteStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
File directory = new File("/data/tmp/");
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
return RepeatStatus.FINISHED; // 단 한 번 수행 후 종료
}, transactionManager)
.build();
}2. Chunk 지향 방식: 대용량 데이터 반복 처리
작업의 의미
Chunk는 대용량 데이터를 안전하고 효율적으로 처리하기 위해 고안된 구조입니다. 수만 건에서 수천만 건에 달하는 데이터를 한 번에 메모리에 올리면 즉시 OutOfMemoryError가 발생하므로, 데이터를 개발자가 지정한 일정 단위(Chunk Size)로 잘라서 읽고-가공하고-저장하는 루프를 반복합니다.
구성 요소
- ItemReader: 데이터 소스(DB, 파일, Queue 등)에서 데이터를 1건씩 읽어옵니다. 페이징 기술을 통해 메모리 버퍼를 활용합니다.
- ItemProcessor: 읽어온 데이터를 1건씩 비즈니스 로직으로 가공하거나 필터링합니다. (선택 사항)
- ItemWriter: Chunk Size만큼 데이터가 메모리(List)에 모이면, 이를 한 번에 전달받아 DB에 일괄 저장(Bulk Insert/Update)하고 트랜잭션을 커밋합니다.
코드 구조
Chunk 지향 방식 코드 예시
@Bean
public Step userGradingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("userGradingStep", jobRepository)
.<User, User>chunk(1000, transactionManager) // 1000건 단위로 트랜잭션 분할
.reader(userItemReader()) // 1건씩 1000번 읽기
.processor(userItemProcessor()) // 1건씩 1000번 가공
.writer(userItemWriter()) // 1000건을 모아서 한 번에 DB Write
.build();
}주의: Step 설정 외에 '개별 컴포넌트 코드'가 각각 독립적으로 필요합니다
설정 코드에 등록된 userItemReader(), userItemProcessor(), userItemWriter()는 단순히 이름만 선언하는 것이 아니라, 인터페이스 규격에 맞춰 각각 독립된 자바 코드로 비즈니스 로직을 구현해야 파이프라인이 완성됩니다.
1. ItemReader 개별 구현 (데이터 읽기)
@Bean
public RepositoryItemReader<User> userItemReader() {
return new RepositoryItemReaderBuilder<User>()
.repository(userRepository)
.methodName("findByStatus")
.pageSize(1000)
.sorts(Map.of("id", Sort.Direction.ASC))
.name("userItemReader")
.build();
}2. ItemProcessor 개별 구현 (1건씩 가공)
@Bean
public ItemProcessor<User, User> userItemProcessor() {
return user -> {
user.upgradeGrade(); // 등급 상향 비즈니스 로직 수행
return user;
};
}3. ItemWriter 개별 구현 (1000건씩 일괄 저장)
@Bean
public RepositoryItemWriter<User> userItemWriter() {
return new RepositoryItemWriterBuilder<User>()
.repository(userRepository)
.methodName("saveAll") // Bulk Insert/Update 수행
.build();
}3. 코드 및 메커니즘 관점의 결정적 차이점
두 방식의 차이점을 3가지 관점으로 쪼개어 보면 다음과 같습니다.
① 트랜잭션의 범위와 루프 제어
- Tasklet:
execute()메서드가 시작될 때 트랜잭션이 열리고, 메서드가 완전히 끝날 때 커밋됩니다. 즉, Tasklet 내부의 전체 작업이 하나의 거대한 트랜잭션으로 묶입니다. - Chunk:
chunk(1000)으로 지정했다면, Reader와 Processor가 1000번 돌 동안은 데이터가 메모리에 누적만 되다가, Writer가 실행되는 시점에 1000건에 대한 트랜잭션이 커밋됩니다. 이 과정이 데이터가 끝날 때까지 무한히 반복됩니다.
② 데이터 접근 추상화
- Tasklet: 개발자가 직접 DB 커넥션을 열거나 파일 스트림을 열어서 전체 데이터를 가져오는 로직을 짜야 합니다. 페이징이나 커서 제어도 직접 구현해야 하므로 대용량 처리 시 코드가 복잡해집니다.
- Chunk: 스프링 배치가 제공하는 강력한 ItemReader 구현체(예:
JpaPagingItemReader,MyBatisCursorItemReader)를 설정만 하면, 내부적인 페이징 및 버퍼링을 프레임워크가 알아서 관리해 줍니다.
③ 에러 발생 시 영향도 (Rollback)
- Tasklet: 만약 10,000번째 데이터를 처리하다가 에러가 나면, 앞서 처리한 9,999건의 데이터까지 전부 롤백됩니다. 처음부터 다시 실행해야 하는 부담이 큽니다.
- Chunk: 10,500번째 데이터에서 에러가 나면 해당 Chunk(10,001 ~ 11,000번)만 롤백됩니다. 이미 성공적으로 커밋된 앞선 10,000건의 데이터는 안전하게 보존되므로 배치의 재시작 효율성이 극대화됩니다.
4. 핵심 차이점 요약 (Comparison Table)
| 비교 항목 | Tasklet 방식 | Chunk 지향 방식 |
|---|---|---|
| 핵심 목적 | 단발성 제어 작업 및 관리성 태스크 | 대용량 데이터의 조회, 가공, 적재 |
| 처리 구조 | 단일 메서드 통짜 실행 (execute) | Reader ➡️ Processor ➡️ Writer 파이프라인 반복 |
| 트랜잭션 단위 | Step 전체가 하나의 트랜잭션 | Chunk Size 단위로 분할 커밋 |
| 메모리 효율성 | 개발자의 로직 구현에 따라 달라짐 | 지정된 Chunk 단위만큼만 소모하여 매우 안정적 |
| 주요 활용 예시 | 파일 삭제, 테이블 초기화, 프로시저 호출 | 통계 산출, 대량 메일 발송, DB 이관 마이그레이션 |
결론: 무엇을 선택해야 하는가?
두 방식 중 정답은 없으며 비즈니스 시나리오에 따라 선택해야 합니다.
- "데이터를 한 건씩 읽어서 가공할 필요가 있는가?" ➡️ YES라면 무조건 Chunk 방식을 선택하여 메모리 안전성을 확보해야 합니다.
- "단순히 특정 명령을 한 번 실행하고 끝내면 되는가?" ➡️ YES라면 Tasklet을 통해 불필요한 Reader/Writer 구현 없이 깔끔하게 단일 코드로 끝내는 것이 생산성 측면에서 유리합니다.
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.