Dangil project(17)

Junyoung·2024년 5월 24일

Dangil Project

목록 보기
17/20

앞 포스팅에서 학습한 spring Batch를 프로젝트에 적용해보고자 한다.

라이브러리 등록

implementation 'org.springframework.boot:spring-boot-starter-batch'

  • 배치 5.0이론

    5.0 이후

    @EnableBatchProcessing 과

      private final JobLauncher jobLauncher;
      private final JobBuilderFactory jobBuilderFactory;
      private final StepBuilderFactory stepBuilderFactory;

    선언없이 사용한다

    → JobBuilder, StepBuilder 로 대체

    JobLauncher jobLauncher; 는 해당 배치를 호출하는 부분에서 사용

    @EnableBatchProcessing or extends DefaultBatchConfiguration

    를 사용하게 되면 Spring Boot Auto Configure에서 자동으로 메타 데이터를 만들어 주지 않는다 ! 기본 설정을 막아버린다.

  • 실제 코드

    package com.ssafy.today.global.batch;
    
    import lombok.extern.slf4j.Slf4j;
    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.job.builder.JobBuilder;
    import org.springframework.batch.core.repository.JobRepository;
    import org.springframework.batch.core.step.builder.StepBuilder;
    import org.springframework.batch.core.step.tasklet.Tasklet;
    import org.springframework.batch.repeat.RepeatStatus;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.PlatformTransactionManager;
    
    @Configuration
    @EnableBatchProcessing
    @Slf4j
    public class BatchConfig {
        @Bean
        public Job simpleJob1(JobRepository jobRepository, Step simpleStep1) {
            return new JobBuilder("simpleJob", jobRepository)
                    .start(simpleStep1)
                    .build();
        }
        @Bean
        public Step simpleStep1(JobRepository jobRepository, Tasklet testTasklet, PlatformTransactionManager platformTransactionManager){
            return new StepBuilder("simpleStep1", jobRepository)
                    .tasklet(testTasklet, platformTransactionManager).build();
        }
        @Bean
        public Tasklet testTasklet(){
            return ((contribution, chunkContext) -> {
                log.info("step1 지납니다~");
                return RepeatStatus.FINISHED;
            });
        }
        
    }
    
    
    이 스프링 배치 구성 코드는 간단한 작업(Job)과 단계(Step)를 생성하고, Tasklet을 사용해 특정 작업을 수행합니다. 여기서는 세 가지 주요 컴포넌트를 설정하고 있습니다:
    
    Job: JobBuilder를 사용하여 simpleJob이라는 이름의 Job을 생성하고,Job이 simpleStep1 단계를 수행하도록 설정합니다.
    Step: StepBuilder를 사용하여 simpleStep1이라는 Step을 생성하고,Step에서 testTasklet Tasklet을 실행하도록 구성합니다. 이 때, 트랜잭션 관리는 PlatformTransactionManager를 통해 이루어집니다.
    Tasklet: Tasklet은 실제 비즈니스 로직을 수행하는 단위로, 여기서는 로그를 출력하는 간단한 로직을 포함하고 있습니다. TaskletRepeatStatus.FINISHED를 반환하여 작업의 완료를 알립니다.
    package com.ssafy.today.global.scheduler;
    
    import lombok.RequiredArgsConstructor;
    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.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    import lombok.extern.slf4j.Slf4j;
    
    @Component
    @Slf4j
    @RequiredArgsConstructor
    public class SchedulerConfig {
      private final JobLauncher jobLauncher;
      private final Job job;
    
      @Scheduled(cron = "0 0 19 * * ?")
      public void findYetUser() {
        try {
          // JobParameters 생성
          JobParameters params = new JobParametersBuilder()
                  .addLong("time", System.currentTimeMillis(), true)
                  .toJobParameters();
          // Job 실행
          jobLauncher.run(job, params);
          log.info("스케줄러 실행한다 ~");
        } catch (Exception e) {
          log.error("배치 작업 실행 중 에러 발생", e);
        }
      }
    }
    

    해당 스케줄러에서 JobLauncher를 가져와 정의한 Job 을 실행한다 !


    그렇다면 Job 이 여러개라면?

    • 구분법
      @Configuration
      @EnableBatchProcessing
      public class BatchConfig {
      
          @Autowired
          private JobBuilderFactory jobBuilderFactory;
      
          @Autowired
          private StepBuilderFactory stepBuilderFactory;
      
          @Bean
          public Job jobOne(Step stepOne) {
              return jobBuilderFactory.get("jobOne")
                      .start(stepOne)
                      .build();
          }
      
          @Bean
          public Job jobTwo(Step stepTwo) {
              return jobBuilderFactory.get("jobTwo")
                      .start(stepTwo)
                      .build();
          }
      }
      
      두개의 잡을 생성해봤다.
      @Component
      public class SchedulerConfig {
      
          @Autowired
          private JobLauncher jobLauncher;
      
          @Autowired
          @Qualifier("jobOne")
          private Job jobOne;
      
          @Autowired
          @Qualifier("jobTwo")
          private Job jobTwo;
      
          @Scheduled(cron = "0 0 19 * * ?") // 매일 19시에 JobOne 실행
          public void runJobOne() {
              executeJob(jobOne);
          }
      
          @Scheduled(cron = "0 30 19 * * ?") // 매일 19시 30분에 JobTwo 실행
          public void runJobTwo() {
              executeJob(jobTwo);
          }
      
          private void executeJob(Job job) {
              try {
                  JobParameters params = new JobParametersBuilder()
                          .addLong("time", System.currentTimeMillis(), true)
                          .toJobParameters();
                  jobLauncher.run(job, params);
              } catch (Exception e) {
                  log.error("Error executing job", e);
              }
          }
      }
      
      @Qualifier 를 사용해 Job 이름을 명시해준다면, 원하는 Job을 가져와 사용할수 있다.


이후 FCM에 해당 Batch process를 직접 적용했다.

public class BatchConfig {
    private final MemberRepository memberRepository;
    private final PushMessageService pushMessageService;
    @Bean
    public Job Job1(JobRepository jobRepository, Step Step1) {
        return new JobBuilder("Job1", jobRepository)
                .start(Step1)
                .build();
    }
    @Bean
    public Step Step1(JobRepository jobRepository, Tasklet Tasklet, PlatformTransactionManager platformTransactionManager){
        return new StepBuilder("Step1", jobRepository)
                .tasklet(Tasklet, platformTransactionManager).build();
    }
    @Bean
    public Tasklet Tasklet() {
        return ((contribution, chunkContext) -> {
            log.info("Job1 시작한다 ~");
            // 금일 일기를 작성하지 않은 멤버들을 뽑아온다.
            List<Member> members = memberRepository.findYetWrite();

            // 알림을 호출한다.
            List<PushMessageRequest> pushMessageRequestList = new ArrayList<>();
            int cnt = 0;
            for (Member member : members) {
                cnt++;
                pushMessageRequestList.add(
                        PushMessageRequest.builder()
                                .token(member.getDeviceToken())
                                .title("당일의 일기는 아직 인가요?")
                                .body("오늘의 일기를 작성해보세요")
                                .diaryId(0L)
                                .build()
                );
                // 100 명씩 끊어서 보낸다
                if ((cnt % 100) == 0) {
                    cnt = 0;
                    pushMessageService.sendPushMessageBulk(pushMessageRequestList);
                    pushMessageRequestList = new ArrayList<>();
                    Thread.sleep(10000);
                }
            }
            // 남은 member 들의 알림을 보낸다
            pushMessageService.sendPushMessageBulk(pushMessageRequestList);
            return RepeatStatus.FINISHED;
        });
    }
}

FCM 알림을 보낼때 최대 100개의 알림만 처리가 가능하다고 한다.

때문에 가져온 사용자의 정보를 100개씩 끊어서 FireBase에 요청을 보내도록 처리했다.

이후 일기를 작성하지 않았고, 오후 8시에 해당 알림을 받는것을 확인했다.

profile
라곰

0개의 댓글