참고자료
우선 사전적 의미는 “일정 시간 동안 대량의 데이터를 한 번에 처리하는 방식”을 의미합니다.
이때 프레임워크를 사용하는 이유는? 아주 많은 데이터를 처리 하는 중간에 프로그램이 멈출 수 있는 상황을 대비해 안전 장치를 마련해야 하기 때문입니다.
10만개의 데이터를 복잡한 JOIN을 걸어 DB간 이동 시키는 도중 프로그램이 멈춰버리면 처음부터 다시 시작할 수 없기 때문에 작업 지점을 기록해야하며,
급여나 은행 이자 시스템의 경우 특정 일 (7월, 오늘, 2020년, 등등)에 했던 처리를 또 하는 중복 불상사도 막아야하는 이유가 있습니다.
Spring Batch 내부구조
JobLauncer: 하나의 배치 작업(Job)을 실행 시키는 시작점Job: “읽기 → 처리 → 쓰기” 과정을 정의한 배치 작업Step: 실제 하나의 “읽기 → 처리 → 쓰기” 작업을 정의한 부분으로, 1개의 Job에서 여러 과정을 진행할 수 있기 때문에 1 : N의 구조를 가진다.ItemReader: 읽어오는 부분ItemProcessor: 처리하는 부분ItemWriter: 쓰는 부분JobRepository: 얼만큼 했는지, 특정 일자 배치를 이미 했는지 “메타 데이터”에 기록하는 부분
spring.batch.job.enabled=false
spring.batch.jdbc.initialize-schema=always
spring.batch.jdbc.schema=classpath:org/springframework/batch/core/schema-mysql.sql
spring.datasource-meta.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource-meta.jdbc-url=jdbc:mysql://localhost:3306/DB이름
spring.datasource-meta.username=유저이름
spring.datasource-meta.password=비밀번호
spring.datasource-data.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource-data.jdbc-url=jdbc:mysql://localhost:3306/DB이름
spring.datasource-data.username=유저이름
spring.datasource-data.password=비밀번호
@Entity
@Getter
@Setter
public class BeforeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
}
@Entity
@Getter
@Setter
public class AfterEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
}
public interface BeforeRepository extends JpaRepository<BeforeEntity, Long> {
}
public interface AfterRepository extends JpaRepository<AfterEntity, Long> {
}
@Configuration
public class MetaDBConfig {
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource-meta")
public DataSource metaDBSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean
public PlatformTransactionManager metaTransactionManager() {
return new DataSourceTransactionManager(metaDBSource());
}
}
@Configuration
@EnableJpaRepositories(
basePackages = "com.study.batch.repository", // repository 폴더위치
entityManagerFactoryRef = "dataEntityManager",
transactionManagerRef = "dataTransactionManager"
)
public class DataDBConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource-data")
public DataSource dataDBSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean dataEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataDBSource());
em.setPackagesToScan(new String[]{"com.study.batch.entity"});
em. setJpaVendorAdapter(new HibernateJpaVendorAdapter());
HashMap<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.show_sql", "true");
em.setJpaPropertyMap(properties);
return em;
}
@Bean
public PlatformTransactionManager dataTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(dataEntityManager().getObject());
return transactionManager;
}
}
@Configuration
@RequiredArgsConstructor
public class FirstBatch {
private final JobRepository jobRepository;
private final PlatformTransactionManager platformTransactionManager;
private final BeforeRepository beforeRepository;
private final AfterRepository afterRepository;
@Bean
public Job firstJob() {
System.out.println("first job");
return new JobBuilder("firstJob", jobRepository)
.start(firstStep())
// .next()
.build();
}
@Bean
public Step firstStep() {
return new StepBuilder("firstStep", jobRepository)
.<BeforeEntity, AfterEntity> chunk(10, platformTransactionManager)
.reader(beforeReader())
.processor(middleProcessor())
.writer(afterWriter())
.build();
}
@Bean // BeforeEntity 테이블에서 읽어오는 Reader
public RepositoryItemReader<BeforeEntity> beforeReader() {
return new RepositoryItemReaderBuilder<BeforeEntity>()
.name("beforeReader")
.pageSize(10)
.methodName("findAll")
.repository(beforeRepository)
.sorts(Map.of("id", Sort.Direction.ASC))
.build();
}
@Bean // 읽어온 데이터를 처리하는 Process (큰 작업을 수행하지 않을 경우 생략 가능, 지금과 같이 단순 이동은 사실 필요 없음)
public ItemProcessor<BeforeEntity, AfterEntity> middleProcessor() {
return new ItemProcessor<BeforeEntity, AfterEntity>() {
@Override
public AfterEntity process(BeforeEntity item) {
AfterEntity afterEntity = new AfterEntity();
afterEntity.setUsername(item.getUsername());
return afterEntity;
}
};
}
@Bean // AfterEntity 에 처리한 결과를 저장하는 Writer
public RepositoryItemWriter<AfterEntity> afterWriter() {
return new RepositoryItemWriterBuilder<AfterEntity>()
.repository(afterRepository)
.methodName("save")
.build();
}
}
(API 호출시 Job Run)@Controller
@ResponseBody
@RequiredArgsConstructor
public class BatchController {
private final JobLauncher jobLauncher;
private final JobRegistry jobRegistry;
@GetMapping("/first")
public String firstApi(@RequestParam("value") String value) throws JobExecutionException {
JobParameters jobParameters = new JobParametersBuilder()
.addString("date", value)
.toJobParameters();
jobLauncher.run(jobRegistry.getJob("firstJob"), jobParameters);
return "ok";
}
}
(일정 시간마다 Job Run)@Configuration
@RequiredArgsConstructor
public class BatchSchedule {
private final JobLauncher jobLauncher;
private final JobRegistry jobRegistry;
@Scheduled(cron = "10 * * * * *", zone = "Asia/Seoul")
public void runFirstJob() throws JobExecutionException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh-mm-ss");
String date = dateFormat.format(new Date());
JobParameters jobParameters = new JobParametersBuilder()
.addString("date", date)
.toJobParameters();
jobLauncher.run(jobRegistry.getJob("firstJob"), jobParameters);
}
}
JobExecutionExceptionjobLauncher.run(jobRegistry.getJob()); // run()과 getJob()에 필요한 Exception의 // 공통 상위 Exception : JobExecutionException