[Spring] Spring Batch를 사용한 게시물 상태 변경 자동화

·2023년 2월 21일
1

킁킁메이트

목록 보기
5/10
post-thumbnail
post-custom-banner

💡 메인 프로젝트를 진행하면서 사용자가 입력한 시간대가 지나면 게시 글의 상태를 모집중에서 모집 완료로 변경해야 했다. 사용자나 제 3자가 수공업으로 게시 글의 상태를 변경할 수 없었기 때문에 Batch 기능을 사용해 구현했다.

시나리오

사용자 요구 사항

  1. 사용자는 약속 시간30분 단위로 입력한다.
  2. 사용자가 입력한 약속 시간이 지나면 게시 글은 자동으로 모집 완료상태로 변경한다.

Scheduler

배치에는 원하는 로직을 반복적으로 실행할 수 있는 기능이 없기 때문에 Spring에서 지원하는 Scheduler와 같이 사용하는 편이다.

Spring에서 사용할 수 있는 Scheduler는 2가지 방식으로 나눌 수 있다.

  • Spring Scheduler : 별도의 의존성 추가 없이 사용이 가능하며, 간단하게 사용 가능하다.
  • Spring Quartz : 스케줄링의 디테일한 제어가 필요한 경우 사용한다.

Spring Quartz는 다양한 기능을 제공하지만, 필수적으로 구현해야 하는 부분들이 있었다. 간단하게 스케줄링을 돌리는 정도라면 Spring Scheduler을 사용해도 무리가 없겠다 판단하여 Spring Scheduler을 사용하였다.

Tasklet

Spring Batch에는Tasklet와 Chunk 두 가지 동작 방식을 지원한다.

대용량 데이터를 사용한다면 Chunk 지향 처리 방식을 사용해야 옳지만, 프로젝트는 대용량 데이터가 발생하지 않고 짧은 개발 기간 내에 Chunk 지향 처리 방식을 학습해 프로젝트에 적용하는 것은 무리가 있다고 판단하여 Tasklet 방식을 사용하였다.

다이어그램

기본 설정

💡개발 환경
Java 11, Srping 2.7.X, MySQL

의존성 라이브러리 추가

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

Batch와 Scheduler 활성화

@EnableBatchProcessing  // 배치 기능 활성화
@EnableScheduling      // 스케줄러 기능 활성화
@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}
  • 애플리케이션단에 @EnableBatchProcessing@EnableScheduling를 추가하지 않으면 정상 작동되지 않는다.

데이터베이스 설정

spring:
  datasource: 
    url: ${DB_URL}
    driver-class-name: com.mysql.cj.jdbc.Driver
    schema: classpath:/org/springframework/batch/core/schema-mysql.sql
  • Spring Batch가 제공하는 기본적인 기능들을 사용하기 위해서는 메타 데이터 테이블이 필요하다.
  • 메타 데이터 테이블은 배치 작업을 하는 동안 사용되는 모든 메타 정보들을 기록하여 작업 중에 사용하거나 모니터링 용도로 사용할 수 있게 해준다.
  • 각 테이블의 용도는 Spring Batch 용어 정리에서 확인할 수 있다.
  • Batch 라이브러리를 추가하면 자동적으로 스키마 파일도 생성된다. 그 중 해당하는 데이터베이스에 맞춰 설정해주면 간단하게 테이블들을 생성할 수 있다.

비즈니스 로직 작성

BoardTasklet

BoardTaskletTasklet 인터페이스를 구현하며, 실질적으로 실행할 비즈니스 로직을 담고 있다.

@Slf4j
@RequiredArgsConstructor
public class BoardTasklet implements Tasklet {

    private final BoardRepository boardRepository;
    
    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        log.info("=====Start Change Board Status======");
        List<Board> findBoards
                = boardRepository.findByAppointTimeLessThanEqual(LocalDateTime.now());

        if(findBoards == null || findBoards.isEmpty()) {
            log.info("=====변경할 게시글이 없습니다.=====");
        } else {
            for(Board board : findBoards) {
                board.setBoardStatus(Board.BoardStatus.BOARD_CLOSE);
                boardRepository.save(board);
            }
        }
        log.info("=====End Change Board Status======");
        return RepeatStatus.FINISHED;
    }
}
  • Tasklet 인터페이스의 execute()를 오버라이딩하여 실질적으로 처리할 비즈니스 로직을 구현한다.

BoardRepository를 DI하여 현재 시간 전에 작성된 게시 글들을 불러오도록 했다.

불러온 게시 글 리스트가 null이거나 비어있을 경우 로그 정보만 넘기고 해당 실행을 종료하고, 값이 있을 경우 BoardStatus 값을 변경한 후 다시 저장할 수 있도록 하였다.

BatchConfig

Batch의 실행 할 Job과 Step을 관리하는 Configuration으로, 해당 작업이 정상적으로 수행되지 못하거나 실패할 경우, 어떻게 대처할 것인지에 대한 설정도 할 수 있다.

@Slf4j
@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
public class BatchConfig {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    private final BoardRepository boardRepository;
    
    @Bean
    public Job boardJob() {             // 빌더로 초기화하는 과정
        Job job = jobBuilderFactory.get("boardJob")
                .start(changeBoardStatus())
                .on("FAILED")
                .stopAndRestart(changeBoardStatus())
                .on("*")
                .end()
                .end()
                .build();

        return job;
    }
    
    @Bean
    public Step changeBoardStatus() {
        return stepBuilderFactory.get("changeBoardStatus")
                .tasklet(new BoardTasklet(boardRepository))
                .build();
    }
}

boardJob()

Job에 대해 JobBuilderFactory를 통해 초기화 하는 과정.

Job job = jobBuilderFactory.get("boardJob") // 메서드의 이름과 같아야 한다.
                .start(changeBoardStatus()) // 실행할 Step
                .on("FAILED")               // 실행한 Step이 실패할 경우
                .stopAndRestart(changeBoardStatus()) // 멈추거나 해당 Step을 재실행
                .on("*")                    // 실패 외의 경우
                .end()						// 해당 Step 종료
                .end()						// 모든 작업 종료
                .build();

changeBoardStatus()

Step에 대해 StepBuilderFactory를 통해 초기화 하는 과정

return stepBuilderFactory.get("changeBoardStatus") // 메서드 이름과 같아야 한다.
                .tasklet(new BoardTasklet(boardRepository)) // 처리할 Tasklet을 생성한다.
                .build();

BoardScheduler

Batch에 설정과 실행할 로직을 다 작성했으니 이제 일정 주기에 맞춰 Batch를 실행 시켜주는 스케줄링을 설정해야 한다.

@Component
@Slf4j
@RequiredArgsConstructor
public class BoardScheduler {
    private final JobLauncher jobLauncher;
    private final Job job;

    @Scheduled(cron = "0 0/30 * * * *")  // 30분마다 도는 스케줄러
    public void runJob() {

        try{
            jobLauncher.run(
                    job, new JobParametersBuilder().addString("dateTime", LocalDateTime.now().toString()).toJobParameters()
            );
        } catch (JobExecutionAlreadyRunningException | JobInstanceAlreadyCompleteException | JobParametersInvalidException | JobRestartException e) {
            log.error(e.getMessage());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}
  • 스케줄링은 간단하게 반복 수행할 메서드에 @Scheduled을 추가해주면 된다. 주기를 설정하는 여러가지 방법이 있지만 여기서는 cron 애트리뷰트를 사용했다.

순서대로 초(0~59), 분(0~59), 시(0~23), 일(1~31), 월(1~12), 요일(0~7)을 나타낸다.

🚨 요일의 경우 0, 7은 일요일을 나타내고 1부터 월요일~토요일로 설정되어 있다.
🚨만약 30분 단위로 설정할 경우, 그 전 단위인 초는 0의 값으로 주고, 분 단위에 30이 아닌 0/30으로 설정해주어야 한다.

JobLauncher.run()

BatchConfig에 정의된 Job을 실행시키는 메소드.
run() 메소드는 run(Job, JobParmeter) 두개의 파라미터 값을 받아 Batch를 실행한다. 매 실행마다 JobParmeter가 변경되어야 하므로 현재 시간을 값을 들어갈 수 있도록 하였다.

💡 JobParmeter
JobInstance에 전달되는 매개변수로, 식별자 역할을 한다.

🚨 주의해야 할 것
Tasklet 처리 방식은 예외가 발생할 경우, 적절한 예외 처리를 하지 않으면 무한 반복 호출된다.

try-catch문을 통해 발생할 예외에 대한 처리를 해주었다.

애플리케이션을 실행하고 게시 글에 작성된 약속 시간이 지나면 자동으로 게시 글의 상태가 변경된다.

회고

구현 당시에는 얼레벌레 학습하자마자 구현하느라 바빠서 뭐가 어떻게 굴러가는지 감을 잡기 힘들었다. 한번 정리하고 나니 왜 이렇게 했어야 했는지 조금은 감이 잡히는 느낌이다.

메타 데이터 테이블의 row가 30분 마다 한줄씩 쌓이고 있다. 프로젝트 배포 링크를 1년 동안 열어둘 생각인데 계속 이렇게 쌓이면 문제가 발생할 것 같다는 생각이 드는데 찾은 SQL문으로는 row 삭제가 안된다...마땅한 레퍼런스가 없어서 고민을 좀 많이 해봐야 할 것 같다.

참고
Batch 소개와 간단한 예제
스케줄 설정 법 & Cron 주기설정

profile
🧑‍💻백엔드 개발자, 조금씩 꾸준하게
post-custom-banner

0개의 댓글