[Spring] 스프링 배치(Spring Batch)란? Spring Batch + Scheduler 예제
스프링 배치 (Spring Batch)
Batch의 사전적 의미는 "일괄 처리"로, 사용자와 상호작용 없이 여러 개의 작업을 미리 정해진 순서에 따라 중단 없이 처리하는 것을 의미한다.
스프링 배치(Spring Batch)는 Spring 환경에서 대용량 데이터를 일괄 처리할 수 있도록 지원하는 프레임워크로, 정해진 로직에 따라 일련의 작업을 수행하는 기능을 제공한다.
Batch는 Scheduler인가?
Batch와 Scheduler는 모두 자동화 작업을 수행하기 때문에 혼동하기 쉽다.
하지만, Batch는 대용량 데이터를 일괄 처리하는 데 초점을 맞추고, Scheduler는 특정 시간 또는 주기적으로 로직을 실행하는 역할을 수행하기 때문에 Batch와 Scheduler는 별개의 기능이다.
즉, 스케줄러를 사용해서 배치를 정해진 시간에 실행할 수 있다.
Spring Batch 아키텍처
스프링 배치는 Application, Batch Core, Batch Infrastructure로 구성되어 있다.
- Application : 개발자가 작성한 모든 배치 작업과 사용자 정의 코드
- Batch Core : 배치 작업을 시작하고 제어하는데 필요한 핵심 Runtime 클래스 (JobLauncher, Job, Step 등)
- Batch Infrastructure : 배치 실행을 위한 기반 기능을 제공하는 컴포넌트 (Reader/Writer, RetryTemplate 등)
Spring Batch 구성 요소
✔️ Job
Job은 배치 처리 과정을 하나의 단위로 묶어놓은 객체이다. (배치 실행 단위)
Job Launcher에 의해 실행되며, N개의 Step을 실행할 수 있다. Job에는 작업 이름, Step 인스턴스 정의 및 순서, 작업 재시작 가능 여부 등의 내용이 들어간다.
✔️ JobRepository
JobRepository는 Job에 대한 모든 정보를 담고 있는 저장소 역할을 한다. Job 실행 이력(언제 수행되고, 언제 끝났는지, 몇 번 실행되었는지, 실행 결과 등) 배치와 관련된 메타데이터가 저장된다. 또한, 배치 실행을 추적하고, 실패한 Job을 재시작하는 기능을 제공한다.
✔️ JobLauncher
JobLauncher는 `Job.execute()`를 호출하여 Job을 실행하는 역할을 담당한다. 이때, Job 재실행 가능 여부, Job 실행 방법, 파라미터 유효성 등을 검증한다.
✔️ Step
Step은 Job의 세부 실행 단위로, Job을 단계별로 나누어 효율적이고 안정적으로 처리할 수 있도록 한다.
Step의 실행 방식은 크게 Tasklet, Chunk 기반으로 나뉜다.
- Tasklet 기반
- Tasklet은 execute() 메서드 하나로 구성된 인터페이스로, 하나의 작업을 수행하는 단위이다.
- 주로 간단한 작업이나 전체 데이터를 한 번에 처리해야 할 때 사용한다.
- Chunk 기반
- 데이터를 일정 크기(Chunk Size)로 나누어 처리하는 방식이다.
- ItemReader → ItemProcessor → ItemWriter의 흐름으로 동작하며, Chunk Size만큼 데이터를 모아 한 번에 처리한다.
- ItemReader : DB 또는 파일에서 데이터를 읽어와 ItemProcessor, ItemWriter에게 전달한다.
- ItemProcessor : ItemReader에서 읽어온 데이터를 가공 및 변환한다.
- ItemWriter : 가공된 데이터를 DB 또는 파일에 저장한다.
예제 코드
미인증 사용자 조회, 인증 처리, 인증 처리된 사용자 조회 3가지의 tasklet으로 구성된 간단한 Spring Batch 예제 코드를 작성했다.
각각의 Tasklet은 순차적으로 실행되며, 작성된 Batch는 Scheduler를 사용해서 실행시키도록 구현했다.
개발 환경
- Spring Boot 3.4.3
- Java 17
- H2 Database
✔️ build.gradle
Spring Batch와 사용할 DB를 추가한다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'com.h2database:h2'
}
✔️ application.yml
H2를 사용하는 경우, 아래와 같이 port와 path를 지정하면 애플리케이션 실행 후 `localhost/h2-console` 에 접속하여 확인할 수 있다.
server:
port: 80
spring:
datasource:
driver-class-name: org.h2.Driver
url: 'jdbc:h2:mem:batch-test'
username: username
password: password
h2:
console:
enabled: true
path: /h2-console
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
format_sql: true
show_sql: true
id:
new_generator_mappings: false
batch:
job:
enabled: false
✔️ SpringApplication
Batch와 Scheduling을 활성화하기 위해 메인 클래스에 @EnableScheduling , @EnableBatchProcessing 어노테이션을 달아준다.
@EnableScheduling // 추가
@EnableBatchProcessing // 추가
@SpringBootApplication
public class SpringBatchStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBatchStudyApplication.class, args);
}
}
✔️ Member
Member Entity는 간단하게 구성했다.
‼️ H2를 사용하는 경우, user가 예약어로 사용되기 때문에 user 이름으로 테이블을 생성할 수 없다.
@Entity
@Getter @Builder
@AllArgsConstructor @NoArgsConstructor
@Table(name = "member")
public class Member implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private VerifyType status;
public void setStatus(VerifyType status) {
this.status = status;
}
}
✔️ VerifyType
인증 타입을 간단하게 Enum으로 작성했다.
@Getter
@AllArgsConstructor
public enum VerifyType {
VERIFIED("인증"),
UNVERIFIED("미인증");
private final String description;
}
✔️ MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findAllByStatus(VerifyType status);
}
✔️ UnverifiedMemberTasklet
미인증 사용자를 조회하는 첫 번째 Tasklet이다.
- `ExecutionContext` 는 Step 간 데이터를 공유하는 데 사용된다. 첫 번째 Tasklet에서 memberList를 조회한 후, `ExecutionContext` 에 저장하여 다음 Step에서 사용할 수 있다.
- Tasklet 방식은 한 번 실행되고 끝나기 때문에 실행 후, `RepeatStatus.FINISHED` 를 반환하면 종료된다.
@Component
@RequiredArgsConstructor
public class UnverifiedMemberTasklet implements Tasklet {
private final MemberRepository memberRepository;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
List<Member> memberList = memberRepository.findAllByStatus(VerifyType.UNVERIFIED);
chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext().put("memberList", memberList);
memberList.forEach(member -> System.out.println("미인증 사용자 : " + member.getName() + " (" + member.getStatus() + ")"));
return RepeatStatus.FINISHED;
}
}
✔️ UpdateStatusTasklet
인증 처리를 하는 두 번째 Tasklet이다.
@Component
@RequiredArgsConstructor
public class UpdateStatusTasklet implements Tasklet {
private final MemberRepository memberRepository;
@Transactional
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
List<Member> memberList = Optional.ofNullable((List<Member>)
chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext().get("memberList"))
.orElse(Collections.emptyList());
for (Member member : memberList) {
member.setStatus(VerifyType.VERIFIED);
}
memberRepository.saveAll(memberList);
System.out.println("미인증 사용자 인증 처리 완료");
return RepeatStatus.FINISHED;
}
}
✔️ VerifiedMemberTasklet
인증 처리가 된 사용자를 조회하는 세 번째 Tasklet이다.
@Component
public class VerifiedMemberTasklet implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
List<Member> memberList = Optional.ofNullable((List<Member>) chunkContext.getStepContext().getStepExecution()
.getJobExecution().getExecutionContext().get("memberList"))
.orElse(Collections.emptyList());
memberList.forEach(member -> System.out.println("사용자 : " + member.getName() + " (" + member.getStatus() + ")"));
return RepeatStatus.FINISHED;
}
}
✔️ MemberJobConfig
Job을 설정하는 Config 클래스이다.
@Configuration
@RequiredArgsConstructor
public class MemberJobConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final UnverifiedMemberTasklet unverifiedMemberTasklet;
private final UpdateStatusTasklet updateStatusTasklet;
private final VerifiedMemberTasklet verifiedMemberTasklet;
@Bean
public Job job() {
return new JobBuilder("job", jobRepository)
.start(unverifiedMemberStep())
.next(updateStatusStep())
.next(verifiedMemberStep())
.build();
}
@Bean
public Step unverifiedMemberStep() {
return new StepBuilder("unverifiedMemberStep", jobRepository)
.tasklet(unverifiedMemberTasklet, transactionManager)
.build();
}
@Bean
public Step updateStatusStep() {
return new StepBuilder("updateStatusStep", jobRepository)
.tasklet(updateStatusTasklet, transactionManager)
.build();
}
@Bean
public Step verifiedMemberStep() {
return new StepBuilder("verifiedMemberStep", jobRepository)
.tasklet(verifiedMemberTasklet, transactionManager)
.build();
}
}
✔️ BatchScheduler
작성한 Batch를 실행시켜 줄 Scheduler이다. 나는 빠른 테스트를 위해 1분마다 실행되도록 구성했다.
@Slf4j
@Component
@RequiredArgsConstructor
public class BatchScheduler {
private final JobLauncher jobLauncher;
private final MemberJobConfig job;
@Scheduled(cron = "0 */1 * * * ?") // 1분마다 실행
public void runJob() {
try {
JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(job.job(), jobParameters);
log.info("Batch 자동 실행 성공");
} catch (Exception e) {
log.error("Batch 자동 실행 실패 : " + e.getMessage());
}
}
}
✔️ 테스트 결과
Batch를 실행하기 전에는 영희와 길동이가 UNVERIFIED 상태이다.
첫 번째 Tasklet이 실행되고 미인증 상태의 Member 상태가 출력되었다.
두 번째 Tasklet이 실행되고 처리 완료가 출력되었다.
마지막 Tasklet이 실행되고 인증 처리된 Member가 출력되고, 스케줄러 동작 성공 로그가 찍혔다.
Batch 실행 후, 영희와 길동이 상태가 정상적으로 VERIFIED 처리됨을 확인할 수 있다.
출처
긴 글 읽어주셔서 감사합니다 🍀
잘못 작성된 내용은 피드백 주시면 반영하겠습니다 😎