728x90
Spring에서 스케줄러를 구현하는 방법을 알아보겠습니다. @Scheduled 어노테이션을 사용한 간단한 방법부터, Cron 표현식을 활용한 복잡한 스케줄링, 그리고 동적 스케줄러 구현까지 다양한 예제와 함께 설명합니다.
목차
1. 기본 설정
1.1 @EnableScheduling 활성화
Spring에서 스케줄러를 사용하려면 메인 애플리케이션 클래스나 설정 클래스에 @EnableScheduling 어노테이션을 추가해야 합니다.
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1.2 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 스케줄러는 Spring Boot에 기본 포함되어 있음
// 별도 의존성 추가 불필요
// Database
runtimeOnly 'com.h2database:h2'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
💡 팁: Spring Boot는 스케줄러 기능을 기본 제공하므로 별도의 의존성을 추가할 필요가 없습니다.
@EnableScheduling만 추가하면 됩니다.2. @Scheduled 어노테이션 사용
@Scheduled 어노테이션을 메서드에 추가하면 해당 메서드가 정해진 시간에 자동으로 실행됩니다.
2.1 기본 스케줄러 예제
package com.example.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@Component
public class BasicScheduler {
/**
* 고정 지연 (Fixed Delay)
* 이전 작업이 완료된 후 지정된 시간만큼 대기 후 실행
* 단위: 밀리초
*/
@Scheduled(fixedDelay = 5000) // 5초마다 실행
public void fixedDelayTask() {
log.info("Fixed Delay Task 실행: {}",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
/**
* 고정 주기 (Fixed Rate)
* 이전 작업 완료 여부와 관계없이 지정된 주기로 실행
* 단위: 밀리초
*/
@Scheduled(fixedRate = 3000) // 3초마다 실행
public void fixedRateTask() {
log.info("Fixed Rate Task 실행: {}",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
/**
* 초기 지연 (Initial Delay)
* 애플리케이션 시작 후 지정된 시간만큼 대기 후 첫 실행
*/
@Scheduled(fixedDelay = 5000, initialDelay = 10000)
public void initialDelayTask() {
log.info("Initial Delay Task 실행: {}",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
/**
* Cron 표현식 사용
* 매일 오전 9시에 실행
*/
@Scheduled(cron = "0 0 9 * * ?")
public void cronTask() {
log.info("Cron Task 실행: {}",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
2.2 @Scheduled 속성 설명
| 속성 | 설명 | 단위 |
|---|---|---|
fixedDelay |
이전 작업 완료 후 대기 시간 | 밀리초 |
fixedRate |
고정된 주기로 실행 | 밀리초 |
initialDelay |
첫 실행 전 대기 시간 | 밀리초 |
cron |
Cron 표현식으로 실행 시간 지정 | Cron 표현식 |
3. Cron 표현식
Cron 표현식은 복잡한 스케줄링을 위해 사용됩니다. 6개 또는 7개의 필드로 구성됩니다.
3.1 Cron 표현식 구조
초 분 시 일 월 요일 [년도]
┌───────────── 초 (0-59)
│ ┌─────────── 분 (0-59)
│ │ ┌───────── 시 (0-23)
│ │ │ ┌─────── 일 (1-31)
│ │ │ │ ┌───── 월 (1-12 또는 JAN-DEC)
│ │ │ │ │ ┌─── 요일 (0-7 또는 SUN-SAT, 0과 7은 일요일)
│ │ │ │ │ │
* * * * * ?
3.2 Cron 표현식 예제
| Cron 표현식 | 설명 |
|---|---|
0 0 * * * ? |
매시간 정각 (0분 0초) |
0 0 9 * * ? |
매일 오전 9시 |
0 0 9 * * MON-FRI |
평일 오전 9시 |
0 0 0 1 * ? |
매월 1일 자정 |
0 */5 * * * ? |
5분마다 |
0 0 12 ? * WED |
매주 수요일 정오 |
0 0 0 ? * MON |
매주 월요일 자정 |
3.3 Cron 표현식 특수 문자
| 문자 | 설명 | 예제 |
|---|---|---|
* |
모든 값 | 0 * * * * ? (매분 0초) |
? |
특정 값 없음 (일/요일에서만 사용) | 0 0 9 ? * MON |
- |
범위 | 0 0 9-17 * * ? (9시~17시) |
, |
값 목록 | 0 0 9,12,15 * * ? (9시, 12시, 15시) |
/ |
증가값 | 0 */5 * * * ? (5분마다) |
3.4 Cron 표현식 사용 예제
package com.example.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CronScheduler {
/**
* 매일 오전 9시에 실행
*/
@Scheduled(cron = "0 0 9 * * ?")
public void dailyMorningTask() {
log.info("매일 오전 9시 작업 실행");
}
/**
* 평일 오전 9시에 실행
*/
@Scheduled(cron = "0 0 9 * * MON-FRI")
public void weekdayTask() {
log.info("평일 오전 9시 작업 실행");
}
/**
* 매시간 정각에 실행
*/
@Scheduled(cron = "0 0 * * * ?")
public void hourlyTask() {
log.info("매시간 정각 작업 실행");
}
/**
* 5분마다 실행
*/
@Scheduled(cron = "0 */5 * * * ?")
public void everyFiveMinutesTask() {
log.info("5분마다 작업 실행");
}
/**
* 매월 1일 자정에 실행
*/
@Scheduled(cron = "0 0 0 1 * ?")
public void monthlyTask() {
log.info("매월 1일 자정 작업 실행");
}
}
4. 고정 지연/고정 주기
4.1 fixedDelay vs fixedRate 차이
⚠️ 중요:
fixedDelay는 이전 작업이 완료된 후 지정된 시간만큼 대기하고, fixedRate는 이전 작업 완료 여부와 관계없이 지정된 주기로 실행됩니다.package com.example.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class DelayAndRateScheduler {
/**
* fixedDelay: 이전 작업 완료 후 5초 대기
* 작업 시간이 3초 걸리면: 실행 → 3초 작업 → 5초 대기 → 다음 실행
* 총 간격: 8초
*/
@Scheduled(fixedDelay = 5000)
public void fixedDelayExample() {
log.info("Fixed Delay 작업 시작");
try {
Thread.sleep(3000); // 3초 작업 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Fixed Delay 작업 완료");
}
/**
* fixedRate: 5초마다 실행 (이전 작업 완료 여부 무관)
* 작업 시간이 3초 걸리면: 실행 → 3초 작업 → 2초 후 다음 실행 (총 5초)
* 작업 시간이 7초 걸리면: 실행 → 7초 작업 → 즉시 다음 실행 (중복 실행 가능)
*/
@Scheduled(fixedRate = 5000)
public void fixedRateExample() {
log.info("Fixed Rate 작업 시작");
try {
Thread.sleep(3000); // 3초 작업 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Fixed Rate 작업 완료");
}
/**
* 초기 지연과 함께 사용
* 애플리케이션 시작 후 10초 대기, 이후 5초마다 실행
*/
@Scheduled(fixedDelay = 5000, initialDelay = 10000)
public void withInitialDelay() {
log.info("Initial Delay 작업 실행");
}
}
5. 비동기 스케줄러
스케줄러 작업이 오래 걸리는 경우, 비동기로 실행하여 다른 작업에 영향을 주지 않도록 할 수 있습니다.
5.1 비동기 설정
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* 비동기 작업을 위한 ThreadPool 설정
*/
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 기본 스레드 수
executor.setMaxPoolSize(10); // 최대 스레드 수
executor.setQueueCapacity(100); // 대기 큐 크기
executor.setThreadNamePrefix("async-"); // 스레드 이름 접두사
executor.initialize();
return executor;
}
}
5.2 비동기 스케줄러 예제
package com.example.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AsyncScheduler {
/**
* 비동기 스케줄러
* @Async를 사용하여 별도 스레드에서 실행
*/
@Async("taskExecutor")
@Scheduled(fixedDelay = 5000)
public void asyncScheduledTask() {
log.info("비동기 스케줄러 실행 - 스레드: {}",
Thread.currentThread().getName());
try {
Thread.sleep(3000); // 긴 작업 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("비동기 스케줄러 완료 - 스레드: {}",
Thread.currentThread().getName());
}
/**
* 여러 비동기 작업 동시 실행
*/
@Async("taskExecutor")
@Scheduled(fixedRate = 3000)
public void concurrentAsyncTask() {
log.info("동시 실행 가능한 비동기 작업 - 스레드: {}",
Thread.currentThread().getName());
}
}
6. 동적 스케줄러
동적 스케줄러는 런타임에 스케줄을 추가, 수정, 삭제할 수 있습니다. TaskScheduler를 사용하여 구현합니다.
6.1 동적 스케줄러 설정
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class DynamicSchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
scheduler.initialize();
return scheduler;
}
}
6.2 동적 스케줄러 Service
package com.example.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
@Slf4j
@Service
@RequiredArgsConstructor
public class DynamicSchedulerService {
private final TaskScheduler taskScheduler;
// 실행 중인 스케줄을 저장
private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
/**
* 고정 지연으로 동적 스케줄 추가
*/
public void scheduleFixedDelay(String taskId, Runnable task, long delayMs) {
ScheduledFuture<?> scheduledTask = taskScheduler.scheduleWithFixedDelay(
task,
Instant.now().plusMillis(1000), // 1초 후 시작
delayMs
);
scheduledTasks.put(taskId, scheduledTask);
log.info("고정 지연 스케줄 추가: taskId={}, delay={}ms", taskId, delayMs);
}
/**
* 고정 주기로 동적 스케줄 추가
*/
public void scheduleFixedRate(String taskId, Runnable task, long periodMs) {
ScheduledFuture<?> scheduledTask = taskScheduler.scheduleAtFixedRate(
task,
Instant.now().plusMillis(1000), // 1초 후 시작
periodMs
);
scheduledTasks.put(taskId, scheduledTask);
log.info("고정 주기 스케줄 추가: taskId={}, period={}ms", taskId, periodMs);
}
/**
* Cron 표현식으로 동적 스케줄 추가
*/
public void scheduleCron(String taskId, Runnable task, String cronExpression) {
CronTrigger cronTrigger = new CronTrigger(cronExpression);
ScheduledFuture<?> scheduledTask = taskScheduler.schedule(
task,
cronTrigger
);
scheduledTasks.put(taskId, scheduledTask);
log.info("Cron 스케줄 추가: taskId={}, cron={}", taskId, cronExpression);
}
/**
* 스케줄 취소
*/
public void cancelSchedule(String taskId) {
ScheduledFuture<?> scheduledTask = scheduledTasks.remove(taskId);
if (scheduledTask != null) {
scheduledTask.cancel(false);
log.info("스케줄 취소: taskId={}", taskId);
} else {
log.warn("스케줄을 찾을 수 없음: taskId={}", taskId);
}
}
/**
* 모든 스케줄 취소
*/
public void cancelAllSchedules() {
scheduledTasks.forEach((taskId, task) -> {
task.cancel(false);
log.info("스케줄 취소: taskId={}", taskId);
});
scheduledTasks.clear();
}
/**
* 실행 중인 스케줄 목록 조회
*/
public Map<String, Boolean> getActiveSchedules() {
Map<String, Boolean> activeSchedules = new ConcurrentHashMap<>();
scheduledTasks.forEach((taskId, task) -> {
activeSchedules.put(taskId, !task.isCancelled() && !task.isDone());
});
return activeSchedules;
}
}
6.3 동적 스케줄러 Controller
package com.example.controller;
import com.example.service.DynamicSchedulerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/scheduler")
@RequiredArgsConstructor
public class SchedulerController {
private final DynamicSchedulerService schedulerService;
/**
* 고정 지연 스케줄 추가
*/
@PostMapping("/fixed-delay")
public ResponseEntity<String> addFixedDelaySchedule(
@RequestParam String taskId,
@RequestParam long delayMs) {
schedulerService.scheduleFixedDelay(taskId, () -> {
log.info("동적 스케줄 실행: taskId={}", taskId);
}, delayMs);
return ResponseEntity.ok("스케줄 추가됨: " + taskId);
}
/**
* Cron 스케줄 추가
*/
@PostMapping("/cron")
public ResponseEntity<String> addCronSchedule(
@RequestParam String taskId,
@RequestParam String cronExpression) {
schedulerService.scheduleCron(taskId, () -> {
log.info("Cron 스케줄 실행: taskId={}", taskId);
}, cronExpression);
return ResponseEntity.ok("Cron 스케줄 추가됨: " + taskId);
}
/**
* 스케줄 취소
*/
@DeleteMapping("/{taskId}")
public ResponseEntity<String> cancelSchedule(@PathVariable String taskId) {
schedulerService.cancelSchedule(taskId);
return ResponseEntity.ok("스케줄 취소됨: " + taskId);
}
/**
* 실행 중인 스케줄 목록
*/
@GetMapping("/active")
public ResponseEntity<Map<String, Boolean>> getActiveSchedules() {
return ResponseEntity.ok(schedulerService.getActiveSchedules());
}
}
7. 실제 사용 예제
7.1 데이터베이스 정리 스케줄러
package com.example.scheduler;
import com.example.repository.LogRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Slf4j
@Component
@RequiredArgsConstructor
public class DatabaseCleanupScheduler {
private final LogRepository logRepository;
/**
* 매일 자정에 30일 이상 된 로그 삭제
*/
@Scheduled(cron = "0 0 0 * * ?")
@Transactional
public void cleanupOldLogs() {
log.info("오래된 로그 정리 시작");
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
int deletedCount = logRepository.deleteByCreatedAtBefore(cutoffDate);
log.info("오래된 로그 {}개 삭제 완료", deletedCount);
}
}
7.2 캐시 갱신 스케줄러
package com.example.scheduler;
import com.example.service.CacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class CacheRefreshScheduler {
private final CacheService cacheService;
/**
* 매시간 정각에 캐시 갱신
*/
@Scheduled(cron = "0 0 * * * ?")
public void refreshCache() {
log.info("캐시 갱신 시작");
cacheService.refreshAllCaches();
log.info("캐시 갱신 완료");
}
/**
* 매일 오전 3시에 모든 캐시 초기화
*/
@Scheduled(cron = "0 0 3 * * ?")
@CacheEvict(allEntries = true, value = {"users", "products", "categories"})
public void clearAllCaches() {
log.info("모든 캐시 초기화 완료");
}
}
7.3 통계 수집 스케줄러
package com.example.scheduler;
import com.example.service.StatisticsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class StatisticsScheduler {
private final StatisticsService statisticsService;
/**
* 매일 오전 1시에 전날 통계 수집 (비동기)
*/
@Async("taskExecutor")
@Scheduled(cron = "0 0 1 * * ?")
public void collectDailyStatistics() {
log.info("일일 통계 수집 시작");
try {
statisticsService.collectDailyStatistics();
log.info("일일 통계 수집 완료");
} catch (Exception e) {
log.error("일일 통계 수집 실패", e);
}
}
/**
* 매주 월요일 오전 2시에 주간 통계 수집
*/
@Async("taskExecutor")
@Scheduled(cron = "0 0 2 ? * MON")
public void collectWeeklyStatistics() {
log.info("주간 통계 수집 시작");
try {
statisticsService.collectWeeklyStatistics();
log.info("주간 통계 수집 완료");
} catch (Exception e) {
log.error("주간 통계 수집 실패", e);
}
}
}
8. 주의사항 및 Best Practice
8.1 주의사항
- 중복 실행 방지:
fixedRate사용 시 작업 시간이 주기보다 길면 중복 실행될 수 있음 - 예외 처리: 스케줄러 내부에서 예외가 발생해도 다음 실행은 계속됨. 반드시 try-catch로 처리
- 트랜잭션: DB 작업이 있는 경우
@Transactional사용 - 클러스터 환경: 여러 서버에서 동일한 스케줄러가 중복 실행되지 않도록 주의 (Redis Lock 등 사용)
8.2 Best Practice
- 로깅: 스케줄러 시작/완료/실패를 모두 로깅
- 모니터링: 스케줄러 실행 시간과 빈도를 모니터링
- 리소스 관리: 오래 걸리는 작업은 비동기로 처리
- 설정 외부화: Cron 표현식이나 주기를 application.yml에 설정
- 테스트: 스케줄러 로직은 별도 메서드로 분리하여 단위 테스트 가능하게
8.3 설정 외부화 예제
scheduler:
cleanup:
cron: "0 0 0 * * ?" # 매일 자정
cache-refresh:
cron: "0 0 * * * ?" # 매시간 정각
statistics:
cron: "0 0 1 * * ?" # 매일 오전 1시
package com.example.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ConfigurableScheduler {
@Value("${scheduler.cleanup.cron}")
private String cleanupCron;
@Scheduled(cron = "${scheduler.cleanup.cron}")
public void cleanupTask() {
log.info("정리 작업 실행");
}
}
✅ 핵심 요약:
@EnableScheduling으로 스케줄러 활성화@Scheduled로 간단한 스케줄링 구현- Cron 표현식으로 복잡한 스케줄링 가능
fixedDelay와fixedRate의 차이 이해- 비동기 스케줄러로 긴 작업 처리
- 동적 스케줄러로 런타임에 스케줄 관리
- 예외 처리와 로깅 필수

카카오톡 오픈채팅 링크
https://open.kakao.com/o/seCteX7h
추가로 궁금한 점이 있으시면 댓글로 남겨주세요! 🚀
참고 자료
728x90
'개발 > BACK' 카테고리의 다른 글
| SpringBoot + QueryDSL 환경에서 동적 쿼리와 페이지네이션 구현하기 (Java 17) (0) | 2025.12.26 |
|---|---|
| Spring 환경에서 RabbitMQ 설정하기 예제 (0) | 2025.12.25 |
| Spring 환경에서 DB 접근 최소화 방법 정리 (0) | 2025.12.24 |
| Spring 환경에서 RSA / AES로 데이터 암호화 하기 - 예제 (0) | 2025.12.24 |
| SpringBoot QueryDSL 환경에서 Servlet 구현 예제 (0) | 2025.12.24 |