본문 바로가기

프로젝트/Techfork

[26/01/03] 오늘의 개발 일지 - Spring Batch JobExecutionListener 도입

오늘은 Spring Batch 공식 문서를 열심히 뜯어본 날입니다.

 

Spring Batch JobExeuctuionListener라는 객체도 존재한다는 것을 알 수 있었는데

이 객체는 쉽게 말하면 Job이 실행되는 생명 주기를 관리하는 객체입니다.

 

기존에는 스케쥴러와 서비스 등에 로그 찍는게 분산이 되어있었는데요.

차라리 JobExecutionListener를 사용해서 로깅에 대한 책임을 얘한테만 주는 게 낫겠다는 생각이 들었습니다.

이게 더 표준적인 방법이라고 나와있기도 했고요.

 

적용을 한 결과

crawlingService는 단순히 Job의 실행만 책임지게 되었고, 스케쥴러도 스케쥴링만 책임지게 되었습니다.

Job의 로깅 및 성공과 실패에 대한 책임을 RssCrawlingJobListener한테 몰 수 있어서 매우 좋은 적용이었던거 같습니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class RssCrawlingJobListener implements JobExecutionListener {

    private final CrawlingHistoryRepository crawlingHistoryRepository;
    private final WebhookNotificationService webhookNotificationService;

    @Override
    @Transactional
    public void beforeJob(JobExecution jobExecution) {
        Long jobExecutionId = jobExecution.getId();
        log.info("RSS crawling job started: jobExecutionId={}", jobExecutionId);

        CrawlingHistory history = CrawlingHistory.createStarted(jobExecutionId);
        crawlingHistoryRepository.save(history);
    }

    @Override
    @Transactional
    public void afterJob(JobExecution jobExecution) {
        Long jobExecutionId = jobExecution.getId();
        BatchStatus batchStatus = jobExecution.getStatus();

        CrawlingHistory history = crawlingHistoryRepository
                .findByJobExecutionId(jobExecutionId)
                .orElseThrow(() -> new IllegalStateException(
                        "CrawlingHistory not found for jobExecutionId: " + jobExecutionId));

        if (batchStatus == BatchStatus.COMPLETED) {
            handleJobSuccess(history, jobExecution);
        } else {
            handleJobFailure(history, jobExecution, "Job failed with status: " + batchStatus);
        }
    }

    /**
     * Job 성공 처리
     */
    private void handleJobSuccess(CrawlingHistory history, JobExecution jobExecution) {
        StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();

        int readCount = (int) stepExecution.getReadCount();
        int writeCount = (int) stepExecution.getWriteCount();
        int skipCount = (int) stepExecution.getSkipCount();

        history.complete(readCount, writeCount, skipCount);
        crawlingHistoryRepository.save(history);

        log.info("RSS crawling completed successfully: " +
                        "total={}, success={}, failed={}",
                readCount, writeCount, skipCount);
    }

    /**
     * Job 실패 처리
     */
    private void handleJobFailure(CrawlingHistory history, JobExecution jobExecution, String errorMessage) {
        history.fail(errorMessage);
        crawlingHistoryRepository.save(history);

        log.error("RSS crawling failed: {}", errorMessage);

        // 실패 알림 전송
        Map<String, Object> context = new HashMap<>();
        context.put("errorMessage", errorMessage);
        context.put("timestamp", LocalDateTime.now());
        context.put("jobExecutionId", jobExecution.getId());

        webhookNotificationService.sendCrawlingFailureNotification(context);
    }
}

 

감사합니다.