Spring Scheduler란?Spring Scheduler사용시점WhySpring Scheduler 체험Multi-Server 환경에서의 Spring SchedulerSpring Scheduler의 스레드REFER
Spring Scheduler란?
일정한 시간 간격 또는 일정한 시각에 특정 로직을 돌리기 위해서 사용하는 것을 Scheduler라고 한다.
일반적으로 SpringBatch와는 용도가 다르다.
SpringBatch는 일괄 처리 방식으로 대량의 데이터를 처리하기 위한 방식이다.
즉, 스케줄러보다는 큰 단위를 처리한다.
그래서 그러한 처리 방식이 또 세분화 적으로 나누어졌다.
Spring Scheduler사용시점
보통 고정된 가격, 지정된 시간에 작업을 예약하고 실행하는데 사용된다.
예를 들어 이메일 알림전송, 데이터베이스 레코드 업데이트 또는 정리 작업과 같은 간단한 작업에 적합하다.
Why
수기로 하는 것은 생산적인 측면에서 비효율적이기 때문이다.
이것은 정기적으로 처리를 하기 위한 자동화 기법으로 생산성 측면에서 좋은 장점이다.
Spring Scheduler 체험
별도의 추가적인 의존성이 필요하지 않고 애노테이션 설정으로 처리할 수 있다.
- 애노테이션 설정(나 스케줄러 사용한다!)

- 스케줄러 실제 사용

스케줄 애노테이션 속성

이런 구조로 되어있고 보통 cron을 이용하여 처리한다.
fixedDelay : milliseconds 단위로, 이전 Task의 종료 시점으로부터 정의된 시간만큼 지난 후 Task를 실행한다.
fixedDelayString : fixedDelay와 같은데 문자열로 값을 표현하겠다는 의미이다.
fixedRate : milliseconds 단위로, 이전 Task의 시작 시점으로부터 정의된 시간만큼 지난 후 Task를 실행한다.
fixedRateString : fixedRate와 같은데 문자열로 값을 표현하겠다는 의미이다.
※ fixedDelay vs fixedRate

fixedRate는 작업 수행시간과 상관없이 일정 주기마다 메소드를 호출하는 것이다.
fixedDelay는 (작업 수행 시간을 포함하여) 작업을 마친 후부터 주기 타이머가 돌아 메소드를 호출하는 것이다.
initialDelay : 스케줄러에서 메소드가 등록되자마자 수행하는 것이 아닌 초기 지연시간을 설정하는 것이다.
@Scheduled(fixedRate = 5000, initialDelay = 3000) public void run() { System.out.println("Hello CoCo World!"); }
이렇게 사용하면 3초의 대기시간(initialDelay) 후에 5초(fixedRate)마다 "Hello CoCo World!"를 출력하는 작업을 스케줄러가 수행해준다.
cron : cront의 표현식으로 작업을 예약하하는 것이다.

첫 번째 * 부터
초(0-59) 분(0-59) 시간(0-23) 일(1-31) 월(1-12) 요일(0-6) (0: 일, 1: 월, 2:화, 3:수, 4:목, 5:금, 6:토)
Spring @Scheduled cron은 6자리 설정만 허용하며 연도 설정을 할 수 없는 단점이 있다.
Cron 표현식 :
- : 모든 조건(매시, 매일, 매주처럼 사용)을 의미
- ? : 설정 값 없음 (날짜와 요일에서만 사용 가능)
- : 범위를 지정할 때
- , : 여러 값을 지정할 때
- / : 증분값, 즉 초기값과 증가치 설정에 사용
- L : 마지막 - 지정할 수 있는 범위의 마지막 값 설정 시 사용 (날짜와 요일에서만 사용 가능)
- W : 가장 가까운 평일(weekday)을 설정할 때
예) 10W
10일이 평일 일 때 : 10일에 실행
10일이 토요일 일 때 : 가장 가까운 평일인 금요일(9일)에 실행
10일이 일요일 일 때 : 가장 가까운 평일인 월요일(11일)에 실행
- # : N번째 주 특정 요일을 설정할 때 (-요일에서만 사용 가능)
예) 4#2 (목요일#2째주에 실행)
Cron 사용예시
// 매일 오후 18시에 실행 @Scheduled(cron = "0 0 18 * * *") public void run() { System.out.println("Hello CoCo World!"); } // 매달 10일,20일 14시에 실행 @Scheduled(cron = "0 0 14 10,20 * ?") public void run() { System.out.println("Hello CoCo World!"); } // 매달 마지막날 22시에 실행 @Scheduled(cron = "0 0 22 L * ?") public void run() { System.out.println("Hello CoCo World!"); } // 1시간 마다 실행 ex) 01:00, 02:00, 03:00 ... @Scheduled(cron = "0 0 0/1 * * *") public void run() { System.out.println("Hello CoCo World!"); } // 매일 9시00분-9시55분, 18시00분-18시55분 사이에 5분 간격으로 실행 @Scheduled(cron = "0 0/5 9,18 * * *") public void run() { System.out.println("Hello CoCo World!"); } // 매일 9시00분-18시55분 사이에 5분 간격으로 실행 @Scheduled(cron = "0 0/5 9-18 * * *") public void run() { System.out.println("Hello CoCo World!"); } // 매달 1일 10시30분에 실행 @Scheduled(cron = "0 30 10 1 * *") public void run() { System.out.println("Hello CoCo World!"); } // 매년 3월내 월-금 10시30분에 실행 @Scheduled(cron = "0 30 10 ? 3 1-5") public void run() { System.out.println("Hello CoCo World!"); } // 매달 마지막 토요일 10시30분에 실행 @Scheduled(cron = "0 30 10 ? * 6L") public void run() { System.out.println("Hello CoCo World!"); }
Multi-Server 환경에서의 Spring Scheduler
보통 실제 운영하면서 하나의 웹애플리케이션 서버로 운영하지는 않기 때문에 이점을 반드시 확인해야 한다.
우려되는 부분은 중복처리 또는 동시성 이슈가 있을 수 있다고 판단했다.
예시를 들면
데이터베이스에 아직 처리되지 않는 알림을 처리하기 위해 레코드 데이터를 동시에 접근하여 알림 서버로 보내게 되면 2번 중복발생이 될것이다.[알림 또는 메일 중복 발송]
혹은 일정 주기로 발급받은 포인트를 2달이내에 사용하지 않으면 소멸 시키는 작업을 스케줄러로 작업한다 했을때, 아직 처리되지 않는 레코드에 동시에 접근하여 포인트를 2배로 감소시키는 이슈가 있을것이다. [포인트 중복 적립 또는 소멸 처리]
이러한 문제를 해결하기 위해서는 ShedLock이라는 것을 사용하여 해결할 수 있다.
- 의존성 추가
// ShedLock Dependency implementation 'net.javacrumbs.shedlock:shedlock-spring:5.2.0' implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.2.0'
- 빈등록

- 테이블 생성
CREATE TABLE `shedlock` ( name varchar(64) NOT NULL COMMENT '스케줄잠금이름', lock_until timestamp(3) NULL DEFAULT NULL COMMENT '잠금기간', locked_at timestamp(3) NULL DEFAULT NULL COMMENT '잠금일시', locked_by varchar(255) DEFAULT NULL COMMENT '잠금신청자', PRIMARY KEY (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
주의사항 : 위 테이블을 절대 수동으로 삭제하면 안된다.
shedLock은 인메모리 캐시를 지니고 있기 때문에 애플리케이션이 다시 시작될 때까지 row를 재생성하지 않는다.
즉, 수동으로 삭제될 경우 lock에 대한 update 정보가 누락된다.
- 애노테이션 적용


@SchedulerLock 어노테이션 속성값들에 대해서 알아보자.
속성 | 내용 |
name | 스케줄 작업의 고유 이름. ShedLock 테이블의 name 컬럼으로 기본키 역할을 하게 되므로 스케줄 작업의 고유한 이름을 입력해야 한다. |
lockAtLeastFor | 작업이 Lock 되어야 할 최소한의 시간을 입력한다. 짧은 작업일 경우에는 노드간의 클럭 차이로 중복 실행되는 것을 막기위해 사용한다 |
lockAtMostFor | 작업을 진행 중인 노드(웹 애플리케이션 서버)가 소멸될 경우에도 Lock이 유지될 시간을 입력한다. 해당 시간은 실제 작업에 소요되는 시간보다 훨씬 길게 해야 한다. (입력하지 않으면 @EnableSchedulerLock의 디폴트 값으로 세팅 5s) |
ShedLock 테이블 확인(웹 애플리케이션 실행 후)
보면 어떤 작업의 고유 이름과 lock_until(언제까지 유지될지), lock_at(언제 시작됬는지), locked_by(어디서 lock을 실행했는지) 에 대한 레코드가 입력된 것을 알 수있다.

해당 라이브러리를 기반으로 중복 실행되어 발생하는 문제를 해결할 수 있다.
Spring Scheduler의 스레드
스프링 스케줄러도 곧 자원(쓰레드)를 이용해서 처리할 것이다.
그러면 API를 처리하는 쓰레드와는 별개로 작동할 것인지 궁금해서 실험을 해봤다.

결과는 API 처리 쓰레드와는 다르지만 해당 서비스 안에서 처리되는 다른 스케줄러 처리와 동일 쓰레드로 돌려쓰고 있다.
시간이 달라서 효율적으로 하기 위해 하나로 돌려쓰는 건가? 라는 의문이 들었고 이점을 시험해보기로 했다.

그럼 시간을 한번 다시 동일하게 바꿔보겠다.


같은 스레드로 처리하고 있는것을 확인할 수 있었다.
또 궁금한 점이 생겼다.
그래서 애초에 다른 서비스에 두면 다른 스레드가 처리하는지도 궁금했다.
각 메소드를 3개의 서비스 각각에 넣어보고 실행해보았다.

그래도 결국 같은 쓰레드로 돌려쓰고 있는것을 확인할 수 있다.

결국 스케줄 처리가 많으면 많을 수록 어떤 한 작업이 오래걸리게 되면 뒤에것도 자동으로 뒤로 밀어지게 되면서 이슈가 발생할 수 있는 것을 알 수 있었다.
이 부분은 쓰레드 풀을 조정해야 겠다는 생각이 들었다.
적정 쓰레드는 해당 테스크가 겹치는 부분의 최댓값 정도만 하면 좋을 것 같다. (여분으로 +2정도 무리 없을 것 같다)
쓰레드는 다다익선이 절대 아니기 때문이다.
쓰레드 풀 조정
@Configuration class SchedulerConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(5); threadPoolTaskScheduler.setThreadGroupName("scheduler thread pool"); threadPoolTaskScheduler.setThreadNamePrefix("scheduler-thread-"); threadPoolTaskScheduler.initialize(); taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); } }

결과적으로 다른 쓰레드를 사용하는 것을 볼 수 있다.
즉 각 테스트들이 종속에 의한 문제가 생기지 않는다.
먼저 처리되는 작업이 소요가 오래되면서 다음것들도 밀리게되는 이슈를 예방할 수 있을 것이다.
이제, 스레드 풀을 생성하여 각 task들을 따로 처리할 수 있어 각 작업끼리 구애받지 않게 되었다.
그러면 스레드 풀의 여유분이 있으니 매 2초마다 다른 스레드가 이것을 주기적으로 실행하겠단 기대 또한 가지고 있을 것이다.

하지만 스레드를 해당 task만큼 생성하여 따로 실행되기를 기대했지만 그렇지 않았다.

결과는 동기적인 처리 과정을 거쳤다.

그저 다른 스레드로 처리할 뿐 작업은 동기적으로 처리하고 있다. 1번그림은 스레드 풀 설정 이전, 2번그림은 스레드 풀 설정 후이다.

스레드 풀 설정 후내가 기대하는 바는 이런 작업을 기대했다.

이 부분은 비동기적인 처리가 필요하다.
- config 설정에 @EnableAsync 추가

- 작업할 테스크 메소드 위 @Async 추가

결과적으로 우리가 기대하는 바와 같이 2초간 정기적으로 실행됨을 확인할 수 있게된다.

하지만, 쓰레드라는 비싼 자원을 사용했으므로 다른 부분에서 이슈가 생길 수 있어 지속적으로 모니터링하여 확인해야 겠다.
REFER
ShedLock
lukas-krecan • Updated May 1, 2025