build.gradle
// quartz-scheduler
implementation 'org.springframework.boot:spring-boot-starter-quartz'
// Spring Batch
implementation 'org.springframework.boot:spring-boot-starter-batch'
QuartzConfig class
import kist.reward.api.scheduler.CitationUpdateJob;
import kist.reward.api.scheduler.InactiveUserJob;
import org.quartz.JobDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
@Configuration
public class QuartzConfig {
@Autowired
private ApplicationContext applicationContext;
@Bean
public JobDetailFactoryBean citationUpdateJobDetail() {
JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
jobDetailFactory.setJobClass(CitationUpdateJob.class);
jobDetailFactory.setDurability(true);
return jobDetailFactory;
}
// 매일 새벽 1시에 논문 인용수 외부 api 요청하는 로직 실행
@Bean
public CronTriggerFactoryBean citationUpdateTrigger(JobDetail citationUpdateJobDetail) {
CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
trigger.setJobDetail(citationUpdateJobDetail);
trigger.setCronExpression("0 0 1 * * ?"); // Cron 표현식
return trigger;
}
}
CitationUpdateJob class
@Autowired private Job citationUpdateJob;
citationUpdateJob
' 은 실제로는 의미가 없으며 타입이 ‘Job’인 빈을 찾아서 주입package kist.reward.api.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Slf4j
@Component
public class CitationUpdateJob extends QuartzJobBean {
/**
* @Autowired 어노테이션을 사용하여 JobLauncher와 Job 타입의 빈(citationUpdateJob)을 주입받고 있음.
*/
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job citationUpdateJob;
/**
* Quartz 스케줄러에 의해 호출되는 메서드
* 이 메서드에서 실제로 Spring Batch Job이 실행됨
*/
@Override
protected void executeInternal(JobExecutionContext context) {
LocalDateTime startTime = LocalDateTime.now();
log.info("Batch job started at {}", startTime);
try {
// JobParametersBuilder를 사용하여 Job의 매개변수를 설정
// 여기서는 현재 시간을 문자열로 변환하여 "citationUpdateJob"이라는 키에 할당합니다.
JobParameters params = new JobParametersBuilder()
.addString("citationUpdateJob", String.valueOf(System.currentTimeMillis()))
.toJobParameters();
// Spring Batch Job을 실행
jobLauncher.run(citationUpdateJob, params);
LocalDateTime endTime = LocalDateTime.now();
log.info("************ Batch job finished successfully at {} ************", endTime);
} catch (Exception e) {
LocalDateTime errorTime = LocalDateTime.now();
log.error("************ Batch job failed at {} ************", errorTime, e);
}
}
}
BatchConfig class
Job
**은 배치 처리 과정을 캡슐화한 객체입니다.Job
**은 하나 이상의 **Step
**을 포함할 수 있습니다.Job
**은 일반적으로 전체 배치 처리의 시작과 끝을 담당하며, 실패 또는 성공에 대한 로직을 포함할 수 있습니다.Step
**은 실제 배치 작업이 수행되는 단위입니다.Step
**은 ItemReader
, ItemProcessor
, **ItemWriter
**를 포함합니다.ItemReader
**는 데이터를 읽어오는 역할, **ItemProcessor
**는 읽어온 데이터를 가공하는 역할, **ItemWriter
**는 가공된 데이터를 저장하는 역할을 합니다.package kist.reward.api.config;
import kist.reward.api.batch.CitationItemProcessor;
import kist.reward.api.batch.CitationItemWriter;
import kist.reward.api.paper.domain.Paper;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.JpaPagingItemReader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManagerFactory;
@Configuration
@RequiredArgsConstructor
@EnableBatchProcessing
public class BatchConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final CitationItemProcessor citationItemProcessor;
private final CitationItemWriter citationItemWriter;
private final EntityManagerFactory entityManagerFactory;
private static final int chunkSize = 50;
/**
* 'citationUpdateBatchJob' 라는 이름의 'Job' 빈을 생성
* 이 'Job' 빈이 'CitationUpdateJob' 클래스에서 주입되어 실행되는 것
*
* Spring의 빈 이름 매칭과 타입 매칭을 통해 이 연결이 이루어짐
* 따라서 CitationUpdateJob 클래스의 citationUpdateJob 필드는
* BatchConfig에서 정의한 citationUpdateBatchJob 빈을 참조하게 됩니다.
*
*/
@Bean
public Job citationUpdateBatchJob(Step citationUpdateStep) {
return jobBuilderFactory.get("citationUpdateBatchJob")
.incrementer(new RunIdIncrementer())
.flow(citationUpdateStep)
.end()
.build();
}
@Bean
public Step citationUpdateStep() {
return stepBuilderFactory.get("citationUpdateStep")
.<Paper, Paper>chunk(chunkSize)
.reader(itemReader())
.processor(citationItemProcessor)
.writer(citationItemWriter)
.build();
}
@Bean
public JpaPagingItemReader<Paper> itemReader() {
JpaPagingItemReader<Paper> reader = new JpaPagingItemReader<>();
reader.setQueryString("쿼리문");
reader.setEntityManagerFactory(entityManagerFactory);
reader.setPageSize(chunkSize);
return reader;
}
}
CitationItemProcessor class
import kist.reward.api.paper.domain.Paper;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import reactor.core.publisher.Mono;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
@Component
public class CitationItemProcessor implements ItemProcessor<Paper, Paper> {
@Override
public Paper process(Paper paper) throws Exception {
// 내부 로직 수행
return paper;
}
}
CitationItemWriter class
import kist.reward.api.paper.domain.Paper;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import java.util.List;
@Component
public class CitationItemWriter implements ItemWriter<Paper> {
@Autowired
private EntityManager entityManager;
@Override
public void write(List<? extends Paper> papers) {
for (Paper paper : papers) {
entityManager.merge(paper);
}
}
}