@Scheduled — 주기적 작업을 스프링에서 실행하는 방법
매일 새벽에 데이터를 정리하거나, 5분마다 외부 API를 폴링해야 한다면 스프링에서 어떻게 구현할까요?
@Scheduled란
@Scheduled는 스프링에서 메서드를 주기적으로 실행하도록 예약하는 어노테이션입니다. 별도의 스케줄러 프레임워크 없이도 간단한 배치 작업이나 폴링 로직을 구현할 수 있습니다.
기본 설정
@Configuration
@EnableScheduling // 필수: 스케줄링 활성화
public class SchedulingConfig {
}
실행 방식 3가지
1. fixedRate — 일정 간격으로 실행
이전 작업의 시작 시점부터 간격을 계산합니다.
@Component
@Slf4j
public class MetricsCollector {
// 10초마다 실행 (이전 작업이 끝나지 않아도 시간이 되면 실행 예약)
@Scheduled(fixedRate = 10_000)
public void collectMetrics() {
log.info("메트릭 수집 시작");
// 서버 상태, 메모리 사용량 등 수집
}
// 문자열로 외부 설정 가능
@Scheduled(fixedRateString = "${metrics.collect.interval:10000}")
public void collectMetricsConfigurable() {
// application.yml에서 간격 설정
}
}
2. fixedDelay — 이전 작업 완료 후 대기
이전 작업의 완료 시점부터 간격을 계산합니다. 작업이 겹치지 않습니다.
@Component
public class ExternalApiPoller {
// 이전 작업이 끝나고 5초 후 다시 실행
@Scheduled(fixedDelay = 5_000)
public void pollExternalApi() {
// 외부 API 호출 — 응답 시간이 가변적일 때 적합
List<Event> events = externalApi.fetchNewEvents();
eventProcessor.process(events);
}
// 애플리케이션 시작 후 30초 뒤에 첫 실행
@Scheduled(fixedDelay = 5_000, initialDelay = 30_000)
public void pollWithInitialDelay() {
// 초기화가 완료된 후에 실행
}
}
3. cron — 특정 시간에 실행
유닉스 cron과 유사한 표현식으로 실행 시간을 지정합니다.
초 분 시 일 월 요일
0 0 2 * * * → 매일 새벽 2시
0 */5 * * * * → 5분마다
0 0 9 * * MON-FRI → 평일 오전 9시
0 0 0 1 * * → 매월 1일 자정
@Component
public class DailyBatchJob {
// 매일 새벽 2시에 실행
@Scheduled(cron = "0 0 2 * * *")
public void cleanupExpiredData() {
log.info("만료 데이터 정리 시작");
dataCleanupService.removeExpiredRecords();
}
// 평일 오전 9시에 리포트 생성
@Scheduled(cron = "0 0 9 * * MON-FRI")
public void generateDailyReport() {
reportService.createDailyReport();
}
// 타임존 지정
@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")
public void seoulTimeJob() {
// 한국 시간 기준 새벽 2시
}
// 외부 설정으로 cron 표현식 관리
@Scheduled(cron = "${batch.cleanup.cron:0 0 2 * * *}")
public void configurableCronJob() {
// application.yml에서 cron 변경 가능
}
}
fixedRate vs fixedDelay 비교
fixedRate = 5초, 작업 시간 = 3초인 경우:
|--작업(3초)--|--대기(2초)--|--작업(3초)--|--대기(2초)--|
0 3 5 8 10
fixedDelay = 5초, 작업 시간 = 3초인 경우:
|--작업(3초)--|----대기(5초)----|--작업(3초)--|----대기(5초)----|
0 3 8 11 16
- fixedRate: 정확한 주기가 중요할 때 (메트릭 수집, 상태 체크)
- fixedDelay: 작업 겹침을 방지해야 할 때 (외부 API 폴링, 데이터 동기화)
TaskScheduler 설정 — 스레드 풀 튜닝
기본 스케줄러는 단일 스레드입니다. 여러 스케줄 작업이 있으면 하나가 오래 걸릴 때 다른 작업도 밀립니다.
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 스레드 5개
scheduler.setThreadNamePrefix("scheduler-");
scheduler.setErrorHandler(t ->
log.error("스케줄 작업 에러: ", t)); // 에러 핸들링
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
scheduler.initialize();
registrar.setTaskScheduler(scheduler);
}
}
또는 application.yml로 간단하게 설정할 수도 있습니다.
spring:
task:
scheduling:
pool:
size: 5
thread-name-prefix: scheduler-
shutdown:
await-termination: true
await-termination-period: 30s
예외 처리
@Scheduled 메서드에서 예외가 발생하면 기본적으로 로그만 남기고 다음 실행을 계속합니다. 하지만 명시적으로 처리하는 것이 좋습니다.
@Scheduled(fixedRate = 60_000)
public void robustScheduledTask() {
try {
riskyOperation();
} catch (TransientException e) {
// 일시적 오류 — 다음 실행에서 재시도
log.warn("일시적 오류, 다음 실행에서 재시도: {}", e.getMessage());
} catch (Exception e) {
// 심각한 오류 — 알림 발송
log.error("스케줄 작업 실패: ", e);
alertService.sendAlert("스케줄 작업 실패: " + e.getMessage());
}
}
ShedLock — 분산 환경의 중복 실행 방지
서버 인스턴스가 여러 개일 때, 동일한 스케줄 작업이 모든 인스턴스에서 동시에 실행됩니다. ShedLock은 DB나 Redis를 이용한 분산 락으로 이를 방지합니다.
// build.gradle
implementation 'net.javacrumbs.shedlock:shedlock-spring:5.16.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.16.0'
DB 테이블 생성
CREATE TABLE shedlock (
name VARCHAR(64) NOT NULL PRIMARY KEY,
lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
locked_by VARCHAR(255) NOT NULL
);
설정
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime() // DB 서버 시간 사용
.build()
);
}
}
사용
@Component
public class DistributedBatchJob {
@Scheduled(cron = "0 0 2 * * *")
@SchedulerLock(
name = "cleanupExpiredData", // 락 이름 (고유해야 함)
lockAtLeastFor = "5m", // 최소 잠금 유지 시간
lockAtMostFor = "30m" // 최대 잠금 유지 시간
)
public void cleanupExpiredData() {
// 여러 인스턴스 중 하나만 실행
dataCleanupService.removeExpiredRecords();
}
}
lockAtLeastFor: 작업이 빨리 끝나더라도 이 시간 동안은 다른 인스턴스가 실행하지 못하게 합니다. 매우 짧은 작업이 여러 인스턴스에서 동시에 실행되는 것을 방지합니다.lockAtMostFor: 작업이 비정상적으로 오래 걸릴 때 락이 자동 해제되는 안전장치입니다.
동적 스케줄링
@Scheduled의 cron 표현식은 정적이지만, TaskScheduler를 직접 사용하면 런타임에 스케줄을 변경할 수 있습니다.
@Service
@RequiredArgsConstructor
public class DynamicSchedulerService {
private final TaskScheduler taskScheduler;
private ScheduledFuture<?> scheduledFuture;
public void scheduleTask(String cronExpression) {
// 기존 스케줄 취소
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
}
// 새로운 스케줄 등록
scheduledFuture = taskScheduler.schedule(
() -> {
log.info("동적 스케줄 작업 실행");
executeTask();
},
new CronTrigger(cronExpression)
);
}
public void cancelTask() {
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
scheduledFuture = null;
}
}
}
@Scheduled vs Spring Batch vs Quartz
| 기준 | @Scheduled | Spring Batch | Quartz |
|---|---|---|---|
| 복잡도 | 단순 | 중간 | 높음 |
| 재시도/재개 | 없음 | 지원 | 지원 |
| 분산 실행 | ShedLock 필요 | 자체 지원 | 클러스터 모드 |
| 대용량 처리 | 부적합 | 최적화됨 | 스케줄링 특화 |
| 적합한 경우 | 단순 반복 작업 | 대용량 배치 | 복잡한 스케줄 관리 |
정리
- fixedRate는 작업 시작 기준, fixedDelay는 작업 완료 기준으로 주기를 결정합니다.
- cron 표현식으로 특정 시간에 작업을 실행할 수 있고, zone 속성으로 타임존을 지정합니다.
- 기본 스케줄러는 단일 스레드입니다. 여러 작업이 있다면 반드시 pool-size를 늘리세요.
- 다중 인스턴스 환경에서는 ShedLock으로 중복 실행을 방지합니다.
- 대용량 데이터 처리가 필요하면 Spring Batch를, 복잡한 스케줄 관리가 필요하면 Quartz를 고려하세요.
댓글 로딩 중...