환율 크롤링, 버스 도착 조회, LOL 랭크 폴링, 카카오 토큰 갱신, 자동매매 루프까지. 여러 프로젝트를 만들다 보니 어느 것 하나 스케줄러 없이 돌아가는 게 없습니다. Spring Boot의 @Scheduled는 설정이 간단하고 편하지만 제대로 쓰려면 알아야 할 것들이 있습니다.
기본 설정
메인 클래스나 설정 클래스에 @EnableScheduling을 추가해야 스케줄러가 동작합니다.
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
fixedDelay vs fixedRate
둘 다 밀리초 단위로 주기를 지정하지만 동작 방식이 다릅니다.
// fixedDelay: 이전 실행 종료 후 N ms 대기
@Scheduled(fixedDelay = 60000)
public void fixedDelayTask() {
// 실행에 10초 걸리면 다음 실행은 70초 후
}
// fixedRate: 이전 실행 시작 기준으로 N ms마다
@Scheduled(fixedRate = 60000)
public void fixedRateTask() {
// 실행에 10초 걸려도 다음 실행은 60초 후
// 만약 실행이 60초를 초과하면 종료 즉시 재실행
}
API를 폴링하거나 크롤링하는 작업은 fixedDelay가 안전합니다. 이전 요청이 완전히 끝난 뒤 다음 요청을 보내기 때문에 요청이 겹치지 않습니다. fixedRate는 작업이 밀려서 동시에 여러 개가 실행될 수 있습니다.
cron 표현식
더 세밀한 시간 제어가 필요하면 cron을 사용합니다. 주식 자동매매처럼 장 시작 직전에 딱 한 번 실행해야 하는 작업에 적합합니다.
// cron = "초 분 시 일 월 요일"
@Scheduled(cron = "0 0 9 * * MON-FRI") // 평일 오전 9시
@Scheduled(cron = "0 30 15 * * MON-FRI") // 평일 오후 3시 30분
@Scheduled(cron = "0 0 1 1 * *") // 매월 1일 새벽 1시
@Scheduled(cron = "0 0 0 1 1 *") // 매년 1월 1일 자정
timezone 설정
서버 시간대가 UTC인 경우 cron이 예상과 다른 시각에 실행될 수 있습니다. timezone을 명시하면 안전합니다.
@Scheduled(cron = "0 10 9 * * MON-FRI", zone = "Asia/Seoul")
public void morningSession() {
// 한국 시간 기준 평일 오전 9시 10분
}
실행 중 예외 처리
스케줄러 메서드에서 예외가 발생하면 해당 실행이 중단됩니다. 그런데 다음 주기에는 정상적으로 다시 실행됩니다. 문제는 예외가 조용히 사라질 수 있다는 점입니다. 반드시 try-catch로 감싸고 로그를 남겨두세요.
@Scheduled(fixedDelay = 60000)
public void pollingTask() {
try {
doWork();
} catch (Exception e) {
log.error("스케줄러 실행 오류", e);
// 예외를 잡지 않으면 다음 실행 자체는 되지만 원인을 알 수 없음
}
}
스케줄러 스레드 수 조정
기본적으로 Spring의 스케줄러는 단일 스레드로 동작합니다. 스케줄러가 여러 개 있을 때 하나가 오래 걸리면 다른 스케줄러가 밀립니다.
// application.properties
spring.task.scheduling.pool.size=5
스케줄링 작업이 많아지면 풀 사이즈를 늘려야 합니다. LOL 프로젝트에서 소환사가 여러 명이고 각각 폴링 사이클이 있을 때 단일 스레드로는 타이밍이 어긋나는 문제가 있었습니다.
initialDelay — 서버 시작 직후 실행 방지
스케줄러는 기본적으로 앱 시작 즉시 첫 실행을 합니다. 서버가 막 뜨는 시점에 외부 API를 호출하거나 DB를 조회하면 초기화가 덜 된 상태라 오류가 날 수 있습니다. initialDelay로 첫 실행을 늦출 수 있습니다.
@Scheduled(fixedDelay = 60000, initialDelay = 30000)
public void task() {
// 앱 시작 30초 후 첫 실행, 이후 60초마다
}
조건부 실행
자동매매처럼 거래 가능 시간에만 동작해야 하는 경우, 스케줄러 자체는 항상 실행되더라도 메서드 안에서 조건을 체크하는 방식이 관리가 편합니다.
@Scheduled(fixedDelay = 5000)
public void tradingLoop() {
if (!isTradingHour()) return;
if (isHoliday()) return;
// 실제 매매 로직
}
cron으로 시간 조건을 정교하게 맞추는 것보다, 단순하게 자주 실행하면서 내부에서 조건을 확인하는 방식이 디버깅도 쉽고 조건 변경도 편합니다.