1. build.gradle

    // quartz-scheduler
    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    
    // Spring Batch
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    
  2. 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;
        }
    }
    
  3. CitationUpdateJob class

    1. @Autowired private Job citationUpdateJob;
      1. 이 부분은 ‘Job’ 타입의 빈을 찾아서 주입.
      2. 변수 이름 ‘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);
            }
        }
    }
    
  4. BatchConfig class

    Job

    Step

    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;
        }
    }
    
  5. 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;
        }
    
       
    }
    
  6. 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);
            }
        }
    }