server-tech-blog

Spring Batch 모집글 상태 자동 변환

1. 문제 상황

2. 해결 방안

3. 배치에 대해서

배치에서 Job이란 전체 배치 프로세스를 캡슐화한 도메인이면 Step의 순서를 정의하고 JobParameters를 받는다 그러면 JobInstance가 생기는데 JobInstance는 실질적으로 Job이 실행되는 객체이다 그리고 JobExecution이 실행된다

배치에서 Step이란 작업 처리의 단위이면 Chunk 기반 스탭, Tasklet 기반 스탭 두가지로 나뉜다

Chunk 기반 스탭은 대용량 데이터를 읽고 쓸때 사용되며 데이터를 읽고 쓰는거까지 하나의 트랜잭션에서 데이터를 처리한다 commitInterval만큼 데이터를 읽고 chunkSize만큼 데이터를 쓴다

Tasklet 기반 스탭은 단순한 처리를 할 때 사용되며 데이터를 읽고 쓰는 과정을 하나로 퉁쳤다 이것 또한 하나의 트랜잭션에서 모든것을 처리한다

스터디 모집글 중 스터디 시작일이 오늘인 것과, 그중에서 스터디 마감 여부가 false인 데이터만 가져와서 스터디를 마감시키는거기 때문에 우리는 대용량 데이터도 아니며 간단한 작업이기 때문에 Tasklet을 사용하기로 했다

4. 구현

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StudyPostJobConfig {

    public final JobBuilderFactory jobBuilderFactory;

    public final StepBuilderFactory stepBuilderFactory;

    private final StudyPostRepository studyPostRepository;

    @Bean("studyPostJob")
    public Job studyPostJob() {
        return jobBuilderFactory.get("studyPostJob")
                .incrementer(new RunIdIncrementer())
                .start(changeStudyPostCloseByDeadline())
                .on("FAILED")
                .stopAndRestart(changeStudyPostCloseByDeadline())
                .on("*")
                .end()
                .end()
                .build();
    }

    @JobScope
    @Bean("changeStudyPostCloseByDeadline")
    public Step changeStudyPostCloseByDeadline() {
        return stepBuilderFactory.get("changeStudyPostCloseByDeadline")
                .tasklet(studyPostTasklet())
                .build();
    }

    @StepScope
    @Bean("studyPostTasklet")
    public Tasklet studyPostTasklet() {
        return (contribution, chunkContext) -> {
            List<StudyPostEntity> studyPostList = studyPostRepository.findByStudyStartDate(LocalDate.now());
            	for(StudyPostEntity studyPost : studyPostList) {
  	               studyPost.closeStudyPost();
    	           studyPostRepository.save(studyPost);
            	}
            return RepeatStatus.FINISHED;
        };
    }

}

studyPostJob() 메서드:

changeStudyPostCloseByDeadlineStep() 메서드:

studyPostTasklet() 메서드:

@Component
@Slf4j
@RequiredArgsConstructor
public class StudyPostScheduler {

   private final JobLauncher jobLauncher;
   private final Job job;

    @Scheduled(cron = "0 * * * * *")
    public void runJob() {

        try{
            jobLauncher.run(
                    job, new JobParametersBuilder().addString("dateTime", LocalDateTime.now().toString()).toJobParameters()
            );
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

private final JobLauncher jobLauncher: 스프링 배치 Job을 실행하기 위한 JobLauncher를 주입받음 private final Job job: 실행할 스프링 배치 Job을 주입받음 JobParametersBuilder().addString(“dateTime”, LocalDateTime.now().toString()).toJobParameters(): 실행 시에 Job에 전달할 파라미터를 설정. “dateTime”이라는 키와 현재 시간을 문자열로 변환한 값을 파라미터로 설정 왜? 매 실행마다 JobParmeter가 변경되어야 해서

public interface StudyPostRepository extends JpaRepository<StudyPostEntity, Long>, StudyPostRepositoryCustom{
    @Transactional
    @Modifying(clearAutomatically = true)
    @Query("UPDATE StudyPostEntity sp SET sp.close = true WHERE sp.studyStartDate = :studyStartDate AND sp.close = false")
    void closeStudyPostsByStartDate(@Param("studyStartDate") LocalDate studyStartDate);}

5. 개선

저렇게 변경감지로 하나하나 업데이트 하니까 마감할 공고들이 10개면 update쿼리가 10개 나간다 직접 update 쿼리를 벌크연산으로 써주고 벌크연산은 영속성 컨텍스트에 반영되지 않기 때문에 혹시 모르니까 대응해주자(@Modifying(clearAutomatically = true))

    @Transactional
    @Modifying(clearAutomatically = true)
    @Query("UPDATE StudyPostEntity sp SET sp.close = :close WHERE sp.studyStartDate = :studyStartDate AND sp.close = false")
    void closeStudyPostsByStartDate(@Param("studyStartDate") LocalDate studyStartDate, @Param("close") Boolean close);
}
@Slf4j
@Configuration
@RequiredArgsConstructor
public class StudyPostJobConfig {

    public final JobBuilderFactory jobBuilderFactory;

    public final StepBuilderFactory stepBuilderFactory;

    private final StudyPostRepository studyPostRepository;

    @Bean("studyPostJob")
    public Job studyPostJob() {
        return jobBuilderFactory.get("studyPostJob")
                .incrementer(new RunIdIncrementer())
                .start(changeStudyPostCloseByDeadline())
                .on("FAILED")
                .stopAndRestart(changeStudyPostCloseByDeadline())
                .on("*")
                .end()
                .end()
                .build();
    }

    @JobScope
    @Bean("changeStudyPostCloseByDeadline")
    public Step changeStudyPostCloseByDeadline() {
        return stepBuilderFactory.get("changeBoardStatus")
                .tasklet(studyPostTasklet())
                .build();
    }

    @StepScope
    @Bean("studyPostTasklet")
    public Tasklet studyPostTasklet() {
        return (contribution, chunkContext) -> {
            studyPostRepository.closeStudyPostsByStartDate(LocalDate.now(), true);
            return RepeatStatus.FINISHED;
        };
    }

}

코드가 훨씬 간결해졌다 성능도 훨씬 좋아졌다

@Component
@Slf4j
@RequiredArgsConstructor
public class StudyPostScheduler {

   private final JobLauncher jobLauncher;
   private final Job job;

    @Scheduled(cron = "0 0 4 * * *")
    public void runJob() {

        try{
            jobLauncher.run(
                    job, new JobParametersBuilder().addString("dateTime", LocalDateTime.now().toString()).toJobParameters()
            );
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

그리고 스캐쥴러도 유저가 가장 없어서 서버 부하가 적은 시간(새벽 4시)에 적용 해 주었다