🌱 Spring 프레임워크에서 제공하는
@Scheduled
어노테이션은 주기적으로 실행되는 작업(task)을 스케줄링을 통해 자동화하는 도구입니다.
이번 포스팅에서는@Scheduled
어노테이션에 대한 개념과 이를 활용한 주기적인 작업 실행 예제를 다루겠습니다.
Spring Framework의 @Scheduled
어노테이션은 Spring에서 제공하는 스케줄링 작업을 정의하는 어노테이션입니다. 이를 통해 특정 메서드를 주기적으로 실행하거나, 예약된 시간에 실행하도록 설정할 수 있습니다.
특정 시간 간격, 특정 시간, 또는 CRON 표현식을 사용하여 메서드를 자동 실행할 수 있습니다.
Spring Boot 애플리케이션에서 스케쥴러는 백그라운드 작업(예: 캐시 갱신, 데이터 집계, 상태 업데이트)을 처리하는 데 자주 사용됩니다.
Spring에서 @Scheduled
어노테이션은 기본적으로 싱글 스레드 환경에서 동작합니다. 따라서 두 개 이상의 @Scheduled
작업이 정의되어 있더라도, 별도로 병렬 실행 설정을 하지 않으면 작업들이 순차적으로 실행됩니다. 즉, 여러 작업이 동시에 실행되는 것이 아니라, 하나의 작업이 끝난 후 다음 작업이 실행되는 구조입니다.
@EnableScheduling
@SpringBootApplication
public class PicketApplication {
public static void main(String[] args) {
SpringApplication.run(PicketApplication.class, args);
}
}
PicketApplication
클래스에 @EnableScheduling
을 추가하거나, @Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT5M")
public class SchedulerConfig {
@Bean
public RedisLockProvider lockProvider(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockProvider(redisConnectionFactory);
}
}
SchedulerConfig
로 분리해 @EnableScheduling
를 추가하여 Scheduling을 활성화합니다.🙋♀️ Picket Project에선 후자의 방법으로 Config를 분리하였습니다. 이유는 다음과 같습니다.
1. 관심사 분리
설정들을 각각의 설정 클래스 (@Configuration
)로 분리하면, 설정 목적별로 코드를 명확하게 관리할 수 있습니다.
=> 즉,SchedulerConfig
는 "스케줄링" 관련 설정만 모아서 담고 있다는 게 명확해집니다.
2. 테스트 시 유연성 증가
단위 테스트나 슬라이스 테스트에서 스케줄링을 선택적으로 포함/제외하기 쉬워집니다.
예:@Import(SchedulerConfig.class)
또는 아예 해당 설정을 제외하여 테스트할 수 있습니다.
3. 조건부 등록 (@Conditional 등) 활용 가능
예를 들어 특정 프로파일(dev, prod)이나 설정 여부에 따라 스케줄링을 켜고 끄고 싶을 경우, 다음처럼 응용할 수 있습니다.@Configuration @ConditionalOnProperty(name = "scheduler.enabled", havingValue = "true") @EnableScheduling public class SchedulerConfig { ... }
이렇게 하면
application.yml
에scheduler.enabled: true/false
로 제어 가능해집니다.
4. 다양한 스케줄 관련 설정들과의 집합성
ShedLock
,ThreadPoolTaskScheduler
, 커스텀 Executor 등을 한 클래스에서 구성할 수 있어서 일관성이 생깁니다.
@Slf4j
@Component
public class SchedulerTest {
@Scheduled(cron = "* * * * * ?") // 매초 실행
public void myTask() {
log.info("Scheduled task");
}
}
@Component
를 사용하여 해당 스케줄러를 Spring Bean에 등록합니다.
해당 클래스를 Bean으로 등록 후 애플리케이션을 실행하면 설정한 시간(매초)마다 실행되는 모습을 확인할 수 있습니다.
아래의 내용은 Spring 공식 문서에서 정리한 내용입니다.
1. cron
cron 표현식은 작업이 실행될 시점을 세밀하게 제어하기 위한 문자열 포맷입니다.
Spring에서는 일반 UNIX cron과 달리 초 단위부터 시작해서 총 6자리 또는 7자리 표현식을 사용합니다.
필드 순서는 아래와 같습니다.
예시
@Scheduled(cron = "0 0/5 * * * *") // 매 5분마다 실행
public void run() {
System.out.println("cron 작업 실행");
}
cron 필드
필드 | 범위 | 설명 |
---|---|---|
초 (second) | 0–59 | 작업이 실행될 초 |
분 (minute) | 0–59 | 작업이 실행될 분 |
시 (hour) | 0–23 | 작업이 실행될 시각 |
일 (day) | 1–31 | 작업이 실행될 일 |
월 (month) | 1–12 , JAN–DEC | 실행할 월 (영문도 가능) |
요일 (day) | 0–6 , SUN–SAT | 실행할 요일 (0 : 일요일) |
연도 (year) | (옵션) | 생략 가능 |
특수 기호
기호 | 의미 |
---|---|
* | 모든 값 (전체 범위) |
? | 특정 값 지정 안 함 (주로 일/요일 중 하나에 사용) |
- | 범위 지정 (예: 1-5 ) |
/ | 간격 지정 (예: */5 → 5단위 실행) |
, | 여러 값 지정 (예: 1,3,5 ) |
L | 마지막 (예: L = 마지막 날, 5L = 마지막 금요일) |
W | 가장 가까운 평일 (예: 15W = 15일에 가까운 평일) |
# | 몇 번째 요일인지 (예: MON#2 = 두 번째 월요일) |
cron 예시
표현식 | 설명 |
---|---|
0 0 * * * * | 매 시 정각 |
*/10 * * * * * | 매 10초마다 |
0 0 8-10 * * * | 매일 8~10시 정각 |
0 0 6,19 * * * | 매일 6시, 19시에 실행 |
0 0/30 8-10 * * * | 8시~10시 사이 30분 간격 실행 |
0 0 9-17 * * MON-FRI | 평일 9~17시 매 정각 |
0 0 0 25 12 ? | 매년 12월 25일 0시 (크리스마스 자정) |
0 0 0 L * * | 매달 마지막 날 자정 |
0 0 0 L-3 * * | 매달 마지막에서 3일 전 자정 |
0 0 0 1W * * | 매달 1일 기준 가장 가까운 평일 자정 |
0 0 0 LW * * | 매달 마지막 평일 자정 |
0 0 0 ? * 5#2 | 매달 두 번째 금요일 자정 |
0 0 0 ? * MON#1 | 매달 첫 번째 월요일 자정 |
매크로 표현식
표현식 | 확장된 cron 표현식 | 의미 |
---|---|---|
@yearly | 0 0 0 1 1 * | 매년 1월 1일 자정 |
@monthly | 0 0 0 1 * * | 매달 1일 자정 |
@weekly | 0 0 0 * * 0 | 매주 일요일 자정 |
@daily | 0 0 0 * * * | 매일 자정 |
@hourly | 0 0 * * * * | 매시간 정각 |
주의 사항
일(day)과 요일(weekday) 필드에 동시에 숫자를 넣을 수 없음. 하나는 반드시 ? 사용해야 함.
Spring에서 cron 표현식은 기본적으로 서버 시간대를 기준으로 작동함. 필요하면 zone 속성 지정 가능 (예: zone = "Asia/Seoul").
cron 표현식이 잘못되면 IllegalArgumentException 발생하니 주의
2. fixedDelay
이전 작업이 종료된 시점 기준으로 지정한 시간(ms)만큼 기다린 후 다음 실행이 시작됩니다.
예: 작업이 10:00:00에 시작해서 10:00:03에 끝났고, fixedDelay = 2000
이면 다음 실행은 10:00:05에 시작됩니다.
@Scheduled(fixedRate = 5000) // 5초마다 실행
public void run() {
System.out.println("fixedRate 작업 실행");
}
-1L
(비활성화)3. fixedRate
이전 작업이 시작된 시점 기준으로 지정한 시간(ms)마다 반복 실행됩니다. 이전 작업이 아직 끝나지 않았더라도 다음 실행이 시작될 수 있습니다.
예: 작업이 10:00:00에 시작했고, fixedRate = 5000
이면 다음 실행은 10:00:05에 시작됩니다.
@Scheduled(fixedDelay = 3000) // 이전 작업 종료 후 3초 후 실행
public void run() {
System.out.println("fixedDelay 작업 실행");
}
-1L
(비활성화)4. initialDelay
애플리케이션 시작 후, 최초 실행을 지연시킬 시간(ms)입니다. fixedRate
또는 fixedDelay
와 함께 사용됩니다.
예: initialDelay = 10000
이면 앱 시작 후 10초 뒤에 첫 실행이 시작됩니다.
@Scheduled(initialDelay = 10000, fixedDelay = 5000)
public void run() {
System.out.println("앱 시작 후 10초 뒤, 이후 5초 간격으로 실행");
}
참고사항
단위는 기본적으로 밀리초(ms) 입니다.
Java 21 이상의 가상 스레드(Virtual Thread) 환경에서는 fixedRate
나 cron
방식 사용이 권장됩니다.
fixedDelay
는 단일 스레드에서 실행되며, 병렬 실행이 되지 않습니다. 하나의 작업이 끝난 뒤 다음 작업이 시작됩니다.
Spring의 스케줄링은 기본적으로 싱글 스레드로 작동해서, 여러 @Scheduled
메서드가 있을 경우 하나씩 순차적으로 실행됩니다.
동시에 실행되길 원한다면, 스레드 풀을 구성해서 병렬 처리를 가능하게 만들어야 합니다.
@Slf4j
@Component
public class SchedulerTest {
@Scheduled(zone = "Asia/Seoul", fixedDelay = 1000) // 타임존 서울, 이전 작업이 끝난 후 1초마다 실행
public void run1() {
log.info("Scheduler task 1 : {}", Thread.currentThread().getName());
}
@Scheduled(zone = "Asia/Seoul", fixedDelay = 1000) // 타임존 서울, 이전 작업이 끝난 후 1초마다 실행
public void run2() {
log.info("Scheduler task 2 : {}", Thread.currentThread().getName());
}
}
위에서 설명한 것과 같이 @Scheduled
는 기본적으로 싱글 스레드로 작동합니다. (task1과 task2의 currentThread
는 동일한 scheduling-1
) 이 때, 아래와 같은 이유로 병렬 처리가 필요할 수 있습니다.
1. 작업 시간이 긴 경우 다른 스케줄에 영향
기본 스케줄러는 단일 스레드로 작동하므로, 하나의 작업이 오래 걸리면 다음 작업이 기다려야 합니다.
예를 들어
A 스케줄러: 매 1분마다 실행되지만 30초 소요
B 스케줄러: 매 1분마다 실행되지만 A가 끝나야 실행됨 → 지연 발생
2. 정확한 주기로 실행되어야 하는 작업이 있는 경우
3. 서로 다른 성격의 작업을 동시에 처리해야 하는 경우
예: 하나는 빠른 로그 수집, 다른 하나는 느린 리포트 생성 작업
성격이 다른 작업을 병렬로 분리해서 처리하지 않으면 전체 시스템 응답이 느려질 수 있습니다.
4. 에러 격리 및 리소스 분산
한 작업에서 에러가 발생해도 다른 작업은 독립적으로 실행되도록 할 수 있습니다.
병렬 처리 시, 각 스레드가 격리되기 때문에 서비스 안정성도 높아집니다.
따라서 이와 같은 이유로 Thread Pool
을 구성해서 작업의 병렬 처리를 가능하게 만들 수 있습니다.
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 병렬 실행할 스레드 수
scheduler.setThreadNamePrefix("scheduled-task-"); // 스레드 그룹명
scheduler.initialize(); // 스케줄러 초기화
taskRegistrar.setTaskScheduler(scheduler);
}
}
ScheduledTaskRegistrar
는 Spring에서 스케줄링 작업을 관리하는 객체고,
기본적으로는 내부에서 단일 스레드 기반의 DefaultTaskScheduler
를 사용합니다.
하지만 ThreadPoolTaskScheduler
를 만들어서 병렬 처리 가능한 스케줄러를 구성했다면, Spring에게 "스케줄링 작업은 이제 이 커스텀 스레드풀을 써!"라고 알려줘야 합니다. 그것이 아래의 코드입니다.
taskRegistrar.setTaskScheduler(scheduler);
ThreadPoolTaskScheduler
를 실제로 @Scheduled
작업들이 사용하게 되는 것입니다.taskRegistrar
: 스케줄링 작업들을 등록/관리하는 객체
setTaskScheduler(...)
: 사용할 TaskScheduler
(스레드풀 등)를 설정
scheduler.setPoolSize(5)
을 통해 스레드 수를 5개로 설정한 뒤 실행해보면 실행되는 task가 각각 다른 스레드에서 병렬 처리 되는 것을 확인할 수 있습니다.