현재 한 트랜잭션당 저장되는 이미지가 여러 종류가 있는데 그 중에 하나를 제외한 나머지는 제거한 뒤, 남은 한장의 이미지를 별도의 공간에 백업하는 스케줄을 작성하려고 하는데 회사에서 주로 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 비교
| 항목 | Cron | Spring 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 "$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())
.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;
}
}
정리
지금 현재 내가 만들어야될 기능에 대한 요구사항은 아래와 같다.
- /data/source/YYYYMMDD/ 디렉토리들 중 오늘 기준 60일 이전의 디렉토리만 탐색
- 각 디렉토리 내 result.png 파일만
- /data/archive/YYYYMMDD/로 이동
현재까지 정리된 내용을 바탕으로 Spring Batch 보단 cron + shell 을 이용한 방법으로 위 기능을 구현하는 것이 바람직해 보인다.