[SpringBoot] @Scheduled를 이용한 스케줄러 구현

2025. 4. 21. 00:22SpringBoot

 

이번 포스팅에선 스프링부트 프레임워크에서 제공하는 @Scehduled 어노테이션을 활용해서 스케줄러를 구현해보겠습니다. 

 

@Scheduled란?

@Scheduled는 Spring에서 제공하는 스케줄링 작업을 정의하는 어노테이션입니다. 이를 통해 특정 메서드를 주기적으로 실행하거나, 예약된 시간에 실행하도록 설정할 수 있습니다.

Spring에서 @Scheduled 어노테이션은 기본적으로 싱글 스레드에서 실행되며, 만약 두 개 이상의 @Scheduled 작업이 정의되어 있어도, 별도의 병렬 스케줄링 설정을 하지 않으면 순차적으로 실행됩니다. 즉, 여러 스케줄링 작업이 동시에 실행되지 않고, 하나씩 차례로 실행됩니다.

구현

@Scheduled 어노테이션을 활성화하려면 애플리케이션에 @EnableScheduling을 추가해야 합니다.

이 어노테이션을 사용하면 @Scheduled 어노테이션이 붙은 메서드들이 자동으로 실행됩니다.

 

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

 

스케줄러로 실행할 작업을 구현할 클래스를 생성합니다.

 

@Component를 사용하여 해당 스케줄러를 Spring Bean에 등록합니다.

스케줄링으로 작업할 메서드를 구현하고 @Scheduled 어노테이션을 달아줍니다.

@Scheduled 어노테이션의 다양한 속성을 이용해서 얼마만큼의 일정 주기로 실행할지 지정할 수 있습니다.

 

스케줄링으로 작업할 메서드에는 일정 시간 단위로 실행하고 싶은 작업을 작성합니다.

저는 좌석의 상태가 OCCUPIED인 좌석들 중 Redis에 해당 좌석 ID로 된 키값의 데이터가 없다면 AVAILABLE 상태로 바꿔주는 작업을 작성했습니다.

@Component
@RequiredArgsConstructor
@Slf4j
public class Scheduler {

	private final SeatRepository seatRepository;
	private final StringRedisTemplate redisTemplate;

    @Scheduled(fixedDelay = 300000) // 5분마다 실행
    public void scheduler() {

        log.info("[SEAT CLEANUP] 예약된 좌석 상태 정리 작업 시작");

        List<Seat> occupiedSeats = seatRepository.findAllBySeatStatus(SeatStatus.OCCUPIED);

        log.info("[SEAT CLEANUP] 점유된 좌석 수: {}", occupiedSeats.size());

        List<Seat> updatedSeats = new ArrayList<>();

        for (Seat seat : occupiedSeats) {
            String redisSeatKey = "SEAT-HOLDING-LOCK:SEAT:" + seat.getId();

            Boolean exists = redisTemplate.hasKey(redisSeatKey);
            if (exists == null || !exists) {
                seat.updateSeatStatus(SeatStatus.AVAILABLE);
                updatedSeats.add(seat);
                log.info("[SEAT CLEANUP] Redis 키 없음 → 좌석 상태 AVAILABLE로 변경 (seatId: {})", seat.getId());
            }
        }

        seatRepository.saveAll(updatedSeats);

        log.info("[SEAT CLEANUP] 상태 변경된 좌석 수: {}", updatedSeats.size());
        log.info("[SEAT CLEANUP] 예약된 좌석 상태 정리 작업 종료");
    }
}

 

 

결과

다음과 같이 5분마다 지정한 작업이 실행되는 것을 볼 수 있습니다.

 

@Scheduled 속성

@Scheduled의 속성을 이용하여 스케줄 옵션을 다양하게 설정할 수 있습니다.

cron 필드

cron 정규 표현식을 통해 원하는 시간, 요일에 작업을 수행하도록 지정할 수 있습니다.

 

cron 필드는 다음과 같이 표현할 수 있습니다.

순서 필드 설명 예시
1 초 (Seconds) 0–59 0: 정각에 실행
2 분 (Minutes) 0–59 30: 매시 30분에 실행
3 시 (Hours) 0–23 14: 오후 2시에 실행
4 일 (Day of month) 1–31 1: 매달 1일에 실행
5 월 (Month) 1–12 또는 JAN–DEC 3: 3월에 실행
6 요일 (Day of week) 0–7 또는 SUN–SAT (0과 7은 일요일) MON: 월요일에 실행
7 연도 (Year, 옵션) 생략 가능 2025: 2025년에만 실행

 

다음과 같은 특수 문자를 cron 정규표현식을 쓸 때 사용할 수 있습니다.

문자 의미
* 매 필드마다 해당 (전체)
? "특정하지 않음" (일/요일 중 하나만 지정할 때 사용)
- 범위 (예: 1-5는 1~5)
, 여러 값 지정 (예: 1,3,5)
/ 주기 (예: 0/15는 0부터 시작해서 15초 간격)
L 마지막 (예: 월의 마지막 날)
W 가장 가까운 평일 (예: 15W는 15일 근처 평일)
# 몇 번째 요일 (예: 6#3은 세 번째 금요일)

 

예제

cron 표현식  의미
"0 0 * * * *" 매 시간 정각 (1시간마다)
"0 0 9 * * *" 매일 오전 9시에 실행
"0 0 9 * * MON-FRI" 주중 오전 9시에 실행
"0 */10 * * * *" 10분마다 실행
"0 0 0 1 * *" 매달 1일 자정에 실행
"0 0 12 15W * ?" 매월 15일에 가장 가까운 평일 정오에 실행

 

fixedDelay(ms 단위)

이전 Task의 "종료 시점"으로부터 정의된 시간만큼 지난 후 Task를 실행합니다.

fixedDelayString는 fixedDelay와 동일하지만, property의 값을 String으로 입력합니다.

 

@Scheduled(fixedDelay = 10000) // 이전 작업 종료 후 10초 뒤 실행
public void run1() {
	log.info("Scheduler 실행1");
}
 
@Scheduled(fixedDelayString = "10000") // 문자열
public void run2() {
    log.info("Scheduler 실행2");
}

 

 

fixedRate(ms 단위)

이전 Task의 "시작 지점"으로부터 정의된 시간만큼 지난 후 Task를 실행합니다. 이

fixedRateString는 fixedRate와 동일하지만, property의 값을 String으로 입력합니다.

 

@Scheduled(fixedRate = 10000) // 10초 간격으로 실행
public void run1() {
    log.info("Scheduler 실행1");
}
 
@Scheduled(fixedRateString = "10000") // 문자열
public void run2() {
    log.info("Scheduler 실행2");
}

 

initialDelay

스케줄러에서 메서드가 등록되자마자 수행하는 것이 아닌, 초기 지연시간을 설정합니다.

initialDelayString는 initialDelay와 동일하지만, property의 값을 String으로 입력합니다.

@Scheduled(fixedRate = 10000, initialDelay = 1000) // 1초 대기 후 시작, 10초 간격 실행
public void run() {
    log.info("Scheduler 실행");
}

 

zone

@Scheduled(cron = "...")은 서버 로컬 시간대 기준입니다.
장소별 시간대를 설정하고 싶다면 zone = "Asia/Seoul" 옵션을 같이 사용할 수 있습니다.

@Scheduled(zone = "Asia/Seoul", cron = "0 0 7 * * ?") // 타임존 서울, 매일 07시에 실행
public void run() {
    log.info("Scheduler 실행");
}

 

Thread Pool 설정

@Component
@Slf4j
public class Scheduler {
    @Scheduled(zone = "Asia/Seoul", fixedDelay = 1000) // 타임존 서울, 이전 작업이 끝난 후 1초마다 실행
    public void run1() {
        log.info("Scheduler 실행1 : {}", Thread.currentThread().getName());
    }

    @Scheduled(zone = "Asia/Seoul", fixedDelay = 1000) // 타임존 서울, 이전 작업이 끝난 후 1초마다 실행
    public void run2() {
        log.info("Scheduler 실행2 : {}", Thread.currentThread().getName());
    }
}

@Scheduled 어노테이션은 기본적으로 싱글 스레드에서 실행됩니다.

싱글 스레드란 하나의 스레드에서 여러개의 Task가 실행될 경우 run1이 종료되어야만 run2가 실행되는 구조입니다.

따라서 사진을 보면 scheduling-1 이라는 스레드 이름을 가진 하나의 스레드에서 run2 작업이 끝난 후에 run1 작업 실행이 반복되고 있음을 볼 수 있습니다.

 

만약 작업 중 Slow Query 또는 긴 시간을 소모하는 연산과 같은 성능 저하 요인이 존재한다면

첫 번째 Task의 종료 시간 지연 → 두 번째 Task의 시작 지연 → 결과적으로 모든 스케줄링 작업에 지연이 발생합니다.

 

이러한 문제를 해결하기 위해 Thread Pool을 설정하여 여러 스레드에서 작업을 병렬로 실행할 수 있도록 구성할 수 있습니다.

 

Config 파일을 작성해서 ThreadPoolTaskScheduler를 설정해서 리턴하는 메서드를 빈으로 등록합니다.

@Configuration
public class SchedulerConfig {
    @Bean
    public ThreadPoolTaskScheduler configureScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(5); // 스레드 풀 크기
        threadPoolTaskScheduler.setThreadGroupName("yubin scheduler thread pool"); // 스레드 그룹명
        threadPoolTaskScheduler.setThreadNamePrefix("yubin-scheduler-"); // 스레드명 접두사
        threadPoolTaskScheduler.initialize(); // 스케줄러 초기화
        return threadPoolTaskScheduler;
    }
}

Thread Pool을 설정하면 다음과 같이 여러개의 스레드에서 스케줄러가 실행되고 있음을 볼 수 있습니다.