2025. 4. 21. 00:22ㆍSpringBoot

이번 포스팅에선 스프링부트 프레임워크에서 제공하는 @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을 설정하면 다음과 같이 여러개의 스레드에서 스케줄러가 실행되고 있음을 볼 수 있습니다.
'SpringBoot' 카테고리의 다른 글
| [SpringBoot] Bulk insert를 활용한 속도 개선 (0) | 2025.05.09 |
|---|---|
| [SpringBoot] Facade 패턴을 활용한 동시성 트랜잭션 범위 문제 해결하기 (1) | 2025.05.08 |
| [SpringBoot] 토스페이먼츠 결제 연동하기 (1) | 2025.04.29 |
| [SpringBoot] Redis의 Atomic Opertaion을 이용한 좌석 선점 시스템 만들기 (1) | 2025.04.24 |
| [SpringBoot] Scale-out 환경에서 발생하는 Scheduler 중복 실행 문제 Shedlock으로 해결하기 (0) | 2025.04.21 |