Cron + Shell VS Spring Batch

게으른 개발자·2025년 5월 30일

현재 한 트랜잭션당 저장되는 이미지가 여러 종류가 있는데 그 중에 하나를 제외한 나머지는 제거한 뒤, 남은 한장의 이미지를 별도의 공간에 백업하는 스케줄을 작성하려고 하는데 회사에서 주로 Java를 사용하다보니 Cron 과 Spring Batch의 차이를 비교해보고 어떤걸 적용하면 될 지 확인해보자.

Cron

정의

  • cron은 유닉스 계열 운영체제에서 정기적으로 반복되는 작업을 자동 실행하기 위한 시스템 스케줄러이다.
  • 사용자는 crontab 명령어를 통해 특정 시간, 날짜, 주기로 명령어 또는 스크립트를 자동 실행하도록 설정할 수 있다.

특징

항목내용
실행 환경리눅스/유닉스 시스템의 cron daemon
실행 대상쉘 스크립트, 명령어, 프로그램 실행 등
스케줄 방식crontab에 시간 설정(분, 시, 일, 월, 요일)
상태 추적기본적으로 없음 (로그 수동 관리 필요)
실패/재시도기본적으로 없음 (별도 로직 필요)
병렬 처리OS 수준에서 병렬 실행 가능하지만 관리 어려움
종속성 관리없음. 단일 실행 단위에 적합

장점

  • 간단하고 빠름
  • 추가 설치 불필요
  • 저비용/경량
  • 어떤 명령어도 실행 가능

단점

  • 작업 로직이 복잡해지면 유지보수 어려움
  • 작업 성공/실패 여부 추적 어려움
  • 에러/재시도 처리 없음
  • 병렬처리, 트랜잭션 처리 불가
  • 상태 저장 불가

Spring Batch

정의

  • Spring Batch는 대용량 배치 처리에 특화된 Spring 기반 프레임워크
  • 정형화된 처리 흐름(읽기 -> 처리 -> 쓰기)을 제공하고, 재시도, 로깅, 상태 추척, 병렬 처리 등 엔터프라이즈 수준의 배치 기능을 지원
  • 대량의 데이터를 효율적으로 처리하기 위해 설계된 프레임워크

특징

항목내용
실행 환경Java + Spring Framework
구성 요소Job, Step, ItemReader, Processor, Writer
스케줄링Spring Scheduler, Quartz, 또는 외부 cron과 연동 가능
상태 추적실행 로그 및 Job 상태 DB에 저장 가능
실패/재시도내장 기능 제공(재시도, skip, rollback)
병렬 처리멀티스레드, 파티셔닝, 별령 step 등 가능
트랜잭션각 step 단위로 트랜잭션 지원 가능

장점

  • 복잡한 로직 구조화 가능: reader → processor → writer 구조
  • 상태 추적 가능
  • 재시도/skip 전략 내장
  • 병렬 처리, 파티셔닝 가능
  • DB와 연동된 작업에 최적화됨
  • 트랜잭션 처리 가능

단점

  • 설정이 복잡하고 초기 진입 장벽이 있음
  • Spring 환경 필요
  • 간단한 작업에는 오버엔지니어링일 수 있음
  • 추가 설정 필요 (DB, Scheduler 연동 등)

Cron, Spring Batch 비교

항목CronSpring Batch
적합 대상단순, 주기적 명령 실행복잡한 배치 로직, DB 기반 작업
작업 관리스크립트/명령어 수준구조화된 Job/Step 관리
실패 처리없음 (수동 로그 분석)내장된 재시도, skip
실행 이력로그 파일 직접 관리DB에 Job/Step 이력 저장
병렬 처리OS 병렬 실행(비권장)병렬 Step, 멀티스레드 가능
상태 저장불가가능
개발 난이도매우 낮음비교적 높음
유지보수성낮음높음
예외/트랜잭션 처리불가지원

상황별 선택

상황추천
파일 시스템 기반, 간단한 복사/이동Cron
DB 기반 데이터 조회 후 조건 처리 필요Spring Batch
실패 자동 재시도, 상태 저장이 중요Spring Batch
단기, 가벼운 자동화 작업Cron
대규모 정산, 로그 처리, 데이터 마이그레이션Spring Batch

예제

  • /data/images/ 디렉토리에서 2025-05-01 ~ 2025-05-15 사이에 생성된 이미지를 /data/archive/ 디렉토리로 복사

Cron

shell

#!/bin/bash

SRC_DIR="/data/images"
DEST_DIR="/data/archive"

START_DATE="2025-05-01"
END_DATE="2025-05-15"

# 복사할 디렉토리 생성
mkdir -p "$DEST_DIR"

# find 명령으로 날짜 필터링 후 복사
find "$SRC_DIR" -type f -newermt "$START_DATE" ! -newermt "$END_DATE +1 day" -exec cp {} "$DEST_DIR" \;

echo "$(date): Copied files from $START_DATE to $END_DATE" >> /var/log/image_copy.log

crontab 등록 (crontab -e)

0 2 * * * /path/to/copy_images.sh

Spring Batch

프로젝트 구조

spring-batch-demo/
 ├── src/
 │   ├── main/
 │   │   ├── java/com/example/batch/
 │   │   │   ├── BatchApplication.java
 │   │   │   ├── config/BatchConfig.java
 │   │   │   ├── tasklet/ImageCopyTasklet.java

build.gradle

plugins {
    id 'org.springframework.boot' version '2.7.12'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'java'
}

group = 'com.example'
version = '1.0.0'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

BatchApplication.java

package com.example.batch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

BatchConfig.java

package com.example.batch.config;

import com.example.batch.tasklet.ImageCopyTasklet;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.*;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class BatchConfig {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    public BatchConfig(JobBuilderFactory jobBuilderFactory,
                       StepBuilderFactory stepBuilderFactory) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
    }

    @Bean
    public Step copyImageStep() {
        return stepBuilderFactory.get("copyImageStep")
                .tasklet(new ImageCopyTasklet())
                .build();
    }

    @Bean
    public Job imageCopyJob() {
        return jobBuilderFactory.get("imageCopyJob")
                .incrementer(new RunIdIncrementer()) // 매번 다른 Job 실행 ID 생성
                .start(copyImageStep())
                .build();
    }
}

ImageCopyTasklet.java

package com.example.batch.tasklet;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.stream.Stream;

public class ImageCopyTasklet implements Tasklet {

    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Path SRC_DIR = Paths.get("/data/images");
    private static final Path DEST_DIR = Paths.get("/data/archive");

    private static final LocalDate START_DATE = LocalDate.parse("2025-05-01", FORMAT);
    private static final LocalDate END_DATE = LocalDate.parse("2025-05-15", FORMAT);

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        Files.createDirectories(DEST_DIR);

        try (Stream<Path> files = Files.list(SRC_DIR)) {
            files.filter(Files::isRegularFile)
                .filter(path -> {
                    try {
                        FileTime fileTime = Files.getLastModifiedTime(path);
                        LocalDate fileDate = Instant.ofEpochMilli(fileTime.toMillis())
                                .atZone(ZoneId.systemDefault())
                                .toLocalDate();
                        return !fileDate.isBefore(START_DATE) && !fileDate.isAfter(END_DATE);
                    } catch (IOException e) {
                        e.printStackTrace();
                        return false;
                    }
                })
                .forEach(path -> {
                    try {
                        Path target = DEST_DIR.resolve(path.getFileName());
                        Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
        }

        return RepeatStatus.FINISHED;
    }
}

정리

지금 현재 내가 만들어야될 기능에 대한 요구사항은 아래와 같다.

  1. /data/source/YYYYMMDD/ 디렉토리들 중 오늘 기준 60일 이전의 디렉토리만 탐색
  2. 각 디렉토리 내 result.png 파일만
  3. /data/archive/YYYYMMDD/로 이동

현재까지 정리된 내용을 바탕으로 Spring Batch 보단 cron + shell 을 이용한 방법으로 위 기능을 구현하는 것이 바람직해 보인다.

profile
6년차 백엔드 엔지니어로 일하고 있습니다~!

0개의 댓글