[스프링부트] Spring Batch와 적용방법
배경
프로젝트 진행 중, Spring Batch를 적용해야 했다.
정확히는 매 0시마다 DB에서 특정 데이터를 삭제해야 했다. (soft delete 된 데이터 영구삭제 구현)
Spring Batch란?
- 배치 프레임워크
- 대용량 데이터 처리에 필수적인 기능 제공 (로깅, 추적, 트랜잭션 관리, job 프로세싱 통계, job 재시작, 스킵, 리소스 관리 등)
Sprinb Batch Architecture
JobLauncher
Job을 실행하는 주체
Job
전체 배치 프로세스를 캡슐화한 엔티티
여러 개의 step으로 구성
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob")
.start(playerLoad())
.next(gameLoad())
.next(playerSummarization())
.end()
.build();
}
football 이라는 job을 실행하기 위해
playerLoad(), gameLoad(), playerSummarization() step 수행
JobInstance: Job의 논리적인 실행
예를 들어 1월 1일 job이 실행되었다면, 1월 1일의 JobInstance가 존재하는 것
(이때, 1월 1일이 JobParameter)
JobExecution: Job이 실제로 한번 실행되었을 때 나온 작업물
예를 들어 1월 1일 job이 여러번 실행되었다면, JobInstance는 하나 + JobExecution은 여러번
Step
- ItemReader, ItemProcessor, ItemWriter를 한개씩 가짐
- 실제 로직 (개발자 작성) ex) DB에서 데이터를 읽음
JobRepository
- 실행중인 프로세스에 대한 메타 정보 저장
- DB에 접근
구현
1. build.gradle 변경 (implementation 추가)
dependencies {
// Spring Batch
implementation 'org.springframework.boot:spring-boot-starter-batch'
}
2. application.yml 변경
spring:
batch:
jdbc:
initialize-schema: always # batch 테이블 자동 생성
job:
enabled: true
3. batch 패키지 생성
── batch
├── CustomBatchConfig.java
└── CustomBatchScheduler.java
CustomBatchConfig.java
@Configuration
@EnableScheduling
public class CustomBatchConfig {
private final MemberRepository memberRepository;
private final DataSource dataSource;
public CustomBatchConfig(DataSource dataSource, MemberRepository memberRepository) {
this.dataSource = dataSource;
this.memberRepository = memberRepository;
}
@Bean
@Primary
@Qualifier("transactionManager")
public JpaTransactionManager JpaTransactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
public JobRepository customJobRepository(JpaTransactionManager jpaTransactionManager) throws Exception {
JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTransactionManager(jpaTransactionManager);
factoryBean.afterPropertiesSet(); // 설정 후 초기화
return factoryBean.getObject();
}
@Bean
public Job deleteExpiredMembersJob(JpaTransactionManager jpaTransactionManager, Step deleteExpiredMembersStep) throws Exception {
return new JobBuilder("deleteExpiredMembersJob", customJobRepository(jpaTransactionManager))
.start(deleteExpiredMembersStep)
.build();
}
@Bean
public Step deleteExpiredMembersStep(JpaTransactionManager jpaTransactionManager, MemberService memberService) throws Exception {
return new StepBuilder("deleteExpiredMembersStep", customJobRepository(jpaTransactionManager))
.tasklet((contribution, chunkContext) -> { //deleteExpiredMembersTasklet
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
// 만료된 멤버 조회
List<Member> expiredMembers = memberService.findExpiredMembers(cutoffDate);
// 만료된 멤버 삭제
int deletedCount = memberService.deleteExpiredMembers(cutoffDate);
System.out.println("# " + deletedCount + " expired members deleted.");
if (!expiredMembers.isEmpty()) {
for (Member member : expiredMembers) {
// 삭제 대상 멤버 정보 출력
System.out.println(" - ID: " + member.getMemberToken() + ", Name: " + member.getNickname() + ", email: " + member.getEmail() + ", emailType: " + member.getLoginType());
}
}
return RepeatStatus.FINISHED;
}, jpaTransactionManager)
.allowStartIfComplete(true) // 이미 완료된 Step도 재실행
.build();
}
}
step에서 ItemReader, ItemProcessor, ItemWriter 없이 단순 처리했다
CustomBatchScheduler.java
@Component
public class CustomBatchScheduler {
private final JobLauncher jobLauncher;
private final Job deleteExpiredMembersJob;
public CustomBatchScheduler(JobLauncher jobLauncher, Job deleteExpiredMembersJob) {
this.jobLauncher = jobLauncher;
this.deleteExpiredMembersJob = deleteExpiredMembersJob;
}
@Scheduled(cron = "0 0 0 * * ?") // 매일 자정에 실행
public void runDeleteExpiredMembersJob() throws Exception {
jobLauncher.run(deleteExpiredMembersJob, new JobParameters());
}
}
구현 중 주의사항
batch table
Job Repository에서는 batch처리와 관련된 메타 정보를 저장하기 위해 DB에 접근한다.
이 DB는 batch job과 관련된 DB이다.
생성되는 DB Table은 이 6가지라 한다.
- BATCH_JOB_INSTANCE
- BATCH_JOB_EXECUTION
- BATCH_JOB_EXECUTION_PARAMS
- BATCH_JOB_EXECUTION_CONTEXT
- BATCH_STEP_EXECUTION
- BATCH_STEP_EXECUTION_CONTEXT
이 DB Table을 자동으로 생성하기 위해서는 설정을 추가해야 한다.
그 작업이 바로 ((2. application.yml 추가)) 였던 것이다.
spring:
batch:
jdbc:
initialize-schema: always # batch 테이블 자동 생성
job:
enabled: true
@EnableBatchProcessing
블로그를 보면 이 어노테이션을 추가하라 한다.
하지만 이 어노테이션을 추가하면 application.yml로 작성한 spring batch 속성이 효력을 잃는다
(자동으로 spring batch를 구성하기 때문이다)
즉, application yml로 batch table을 자동생성하라 했는데
그것이 반영되지 않는 에러가 생겼다.
나의 경우에는
@EnableBatchProcessing를 삭제해,
batch table이 정상적으로 자동 생성되도록 설정했다.
DataSourceTransactionManager Bean cannot found 에러
Parameter 0 of method deleteExpiredMembersStep
in solverz.business_card.batch.BatchConfig required a bean of type
'org.springframework.jdbc.datasource.DataSourceTransactionManager'
that could not be found.
에러가 생성되었다.
분명 application.yml로 batch config 설정을 완료했는데, Bean이 생성되지 않은 문제가 생긴 것이다.
이 문제는 명시적으로 DataSourceTransactionManager Bean을 등록해 해결했다.
@Configuration
@EnableScheduling
public class CustomBatchConfig {
@Bean
@Primary
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
... (생략)
}
InvalidDataAccessApiUsageException: Executing an update/delete query 에러
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query
(중략)
at jdk.proxy2/jdk.proxy2.$Proxy156.deleteExpiredMembers(Unknown Source)
이번에는 update/delete query가 실행되지 않는다고 한다.
수많은 블로그에서는 @Transactional 어노테이션을 붙이라 말한다.
하지만 나는 여전히 해결할 수 없었다
이 stack overflow의 글에 따르면,
DataSourceTransactionManager 대신 JpaTransactionManager를 사용하라 말한다.
@Configuration
@EnableScheduling
public class CustomBatchConfig {
@Bean
@Primary
@Qualifier("transactionManager")
public JpaTransactionManager JpaTransactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
... (생략)
}
JpaTransactionManager를 사용하고,
@Qualifier("transactionManager") 를 통해 Bean을 transactionManager로 등록시켰다
restart
배치 job을 실행했을 때, 동일한 JobInstance의 JobExecution이 존재한다면 재시작으로 간주된다
나의 코드에서는 항상 같은 JobInstance을 실행할 것이다.
하지만 배치 job에서는 기본적으로 이미 완료된 스텝의 재시작을 허용하지 않는다.
그렇기에 아래와 같은 에러가 생성되었다.
Step already complete or not restartable,so no action to execute:
StepExecution: id=55, version=3, name=deleteExpiredMembersStep,
status=COMPLETED, exitStatus=COMPLETED, readCount=0,
(생략)
allowStartIfComplete(true) 통해 이미 완료된 step의 재실행을 허용해 해결할 수 있다.
(또는, timestamp 등 고유한 job parameter를 추가해 해결할 수 있다)
@Configuration
@EnableScheduling
public class CustomBatchConfig {
// (생략)
@Bean
public Step deleteExpiredMembersStep(JpaTransactionManager jpaTransactionManager, MemberService memberService) throws Exception {
return new StepBuilder("deleteExpiredMembersStep", customJobRepository(jpaTransactionManager))
.tasklet((contribution, chunkContext) -> { //deleteExpiredMembersTasklet
// (생략)
}
return RepeatStatus.FINISHED;
}, jpaTransactionManager)
.allowStartIfComplete(true) // 이미 완료된 Step도 재실행
.build();
}
}
여담
이번에는 아주 간단하게 job을 생성해 보았다.
다음에는 chunk방식도 도입해보고,
ItemReader, ItemProcessor, ItemWriter도 도입해보며 방식을 비교해보고 싶다!
참고
스프링배치 공식문서 한글화 https://godekdls.github.io/Spring%20Batch/introduction/
스프링배치 예제, 설명 https://jojoldu.tistory.com/347
스프링배치 예제, 설명 https://github.com/prodo-developer/spring-batch-example
스프링배치 예제, 설명 https://seodeveloper.tistory.com/entry/Spring-Batch-환율-정보-API-를-간단한-배치-스케줄러-추가-예제편
@EnableBatchProcessing 설명 https://curiousjinan.tistory.com/entry/spring-boot-3-batch-5-table-creation-fix
스프링배치 공식 예제 https://github.com/spring-projects/spring-batch/tree/main/spring-batch-samples
Batch Job 관련 DB Table 설명 https://velog.io/@s2moon98/JobRepository와-메타-데이터
스프링배치 Tasklet 방식, Chunk 방식 설명 https://jgrammer.tistory.com/entry/Spring-Batch-실패를-다루는-기술-ItemStream
Item Stream https://jgrammer.tistory.com/entry/Spring-Batch-실패를-다루는-기술-ItemStream