통계성 작업을 일정 시간마다 수행하여 알려준다던지 하는 식으로 따로 요청을 보내지 않아도 일정 시간 마다 반복되는 작업이 필요한 경우가 있습니다.
spring을 사용하여 아주 간단한 스케줄러를 만들어보도록 하겠습니다.
@EnableScheduling
어노테이션을 붙여준다.@SpringBootApplication
@EnableScheduling // background task executer가 생성된다.
class SchedulingApplication
fun main(args: Array<String>) {
runApplication<SchedulingApplication>(*args)
}
@Component
class ScheduledTasks {
companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
private val dataFormat: SimpleDateFormat = SimpleDateFormat("HH:mm:ss")
}
@Scheduled(fixedRate = 5000)
fun reportCurrentTime() {
log.info("The tims is now ${dataFormat.format(Date())}")
}
}
이후 실행시켜보면 5초마다 로그가 잘 찍히는 걸 확인할 수 있습니다.
@EnableScheduling
은 @Scheduled
어노테이션을 감지하고 만들어 준 스케줄러를 동작할 수 있게 해주는 어노테이션입니다.
spring은 고유한TaskScheduler
인터페이스 타입의 빈이나 taskScheduler
라는 이름을 가진 빈을 찾습니다. 만약 존재하지 않는다면 자동 설정에 의해 local single thread 스케줄러가 생성되어 사용됩니다.
스케줄러 메서드를 표시하기 위한 어노테이션입니다. 이 메서드는 인수를 가질 수 없습니다. 일반적으로 void return 형태로 사용하여 그렇지 않다면 return 값이 무시될 수 있습니다.
cron
: cron 표현식을 지원. ‘초 분 시 일 월 주 (년)’ 으로 표현fixedDelay
: ms 단위. 이전 작업이 끝난 시점부터 고정된 호출 간격을 설정.fixedRate
: ms 단위. 이전 작업이 수행되기 시작한 시점부터 고정된 호출 간격 설정.initialDelay
: 초기 지연 시간.(스케줄러에서 메서드가 등록되자마자 수행하지 않고 설정해준 시간 후부터 동작)zone
: cron 표현식을 사용할 때 적용할 time zone. 따로 지정하지 않으면 서버의 time zone위에서 말한 것 처럼 spring은 기본적으로 taskScheduler를 생성해주긴 하지만, 하나의 스레드만 사용하기 때문에 비효율적입니다. custom하게 스레드 수와 스레드이름을 설정해보겠습니다.
yml 파일로도 해줄 수 있지만 저는 @Configuration
을 통해 만들어주겠습니다.
@EnableScheduling // main에서 config로 옮겼습니다.
@Configuration
class SchedulerConfiguration {
@Bean
fun taskScheduler(): TaskScheduler {
val scheduler = ThreadPoolTaskScheduler()
scheduler.poolSize = 5
scheduler.setThreadNamePrefix("sc-thread-")
// 추가적으로 graceful shutdown(실행 중인 작업 완료 후 종료) 관련 옵션도 줄 수 있다.
// scheduler.setWaitForTasksToCompleteOnShutdown(true) // 진행중이던 작업 완료 후 thread 종료
// scheduler.setAwaitTerminationSeconds(10) // 10초 동안 작업 완료할 시간 줌. 10초 넘으면 강제 종료
return scheduler
}
}
위와 같이 Bean을 등록해주면 기존에 등록되던 TaskScheduler
대신 등록되고 스레드도 여러개를 사용하는 걸 볼 수 있습니다.
스프링은 기본적으로 여러 인스턴스의 경우 스케줄러 동기화 처리를 할 수 없습니다. 따라서 서버가 여러 인스턴스로 구성되어 있다면 모든 인스턴스에서 스케줄링 작업이 발생할 수 있습니다. 이런 경우 ShedLock
을 통해 동시에 한 번만 실행되도록 할 수 있습니다.
먼저 의존성을 추가해줍니다.
implementation("net.javacrumbs.shedlock:shedlock-spring:4.25.0")
ShedLock은 공유 DB가 있는 환경에서 LockProvider
에 의해 동작합니다. JdbcTemplate, redis 등 다양하게 지원합니다. 저는 redis를 사용해보겠습니다.(docker로 redis를 띄워서 사용했습니다.)
ShedLock redis 의존성을 추가해줍니다.
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("net.javacrumbs.shedlock:shedlock-provider-redis-spring:4.25.0")
LockProvider
를 빈으로 등록해줍니다. RedisLockProvider
로 등록해보겠습니다.
@EnableScheduling
@Configuration
class SchedulerConfiguration {
@Bean
fun taskScheduler(): TaskScheduler {
// ...
}
@Bean
fun lockProvider(connectionFactory: RedisConnectionFactory): LockProvider {
return RedisLockProvider(connectionFactory, "shed_lock")
}
}
설정 파일에 @EnableSchedulerLock
을 붙여줍니다. @EnableSchedulerLock
은 ShedLock
이 동작하도록 해줍니다.
@EnableSchedulerLock(defaultLockAtLeastFor = "1s", defaultLockAtMostFor = "1s")
@EnableScheduling
@Configuration
class SchedulerConfiguration {
// ...
그 후 스케줄러에 @SchedulerLock
을 붙여줍니다.
@SchedulerLock(
name = "TaskScheduler",
lockAtLeastFor = "PT10S",
lockAtMostFor = "PT10S"
)
@Scheduled(fixedRate = 5000)
fun reportCurrentTime1() {
log.info("[TaskScheduler1] The tims is now ${dataFormat.format(Date())}")
}
@SchedulerLock
은 3가지 값을 넣어줄 수 있습니다.
name
: Lock의 이름을 뜻합니다. name을 기준으로 동시에 한 번만 동작하도록 하기 때문에 unique해야합니다. lockAtLeastFor
: 최소한 잠금을 유지해야 하는 기간. 적어준 기간동안 한 번만 실행할 수 있다는 의미입니다. 짧은 시간안에 작업이 완료되는 경우 중복 실행을 막기위해 존재하는 옵션입니다.(동일한 name을 가진 다른 스케줄러는 이 기간 동안 실행 불가)lockAtMostFor
: 실행 노드가 죽을 경우 추가적으로 잠금을 유지해야하는 기간을 뜻합니다. 일반적으로는 작업 완료 후 lockAtLeastFor
가 지나면 잠금을 해제합니다. 스케줄러에서 문제가 있을 때 다른 스케줄러가 기다려주는 시간을 말합니다.이렇게 ShedLock
설정이 끝났습니다.
직접 여러 인스턴스를 띄워서 실행해보는 것이 가장 좋겠지만 동일한 스케줄러를 하나 더 만들고 name
값을 동일하게 줬을 때와 name
값을 다르게 줬을때 로그가 어떻게 찍히는지만 살펴보려고 합니다.
case1) 동일한 lock 이름을 준 경우
@Component
class ScheduledTasks {
// ...
@SchedulerLock(
name = "TaskScheduler",
lockAtLeastFor = "PT10S",
lockAtMostFor = "PT10S"
)
@Scheduled(fixedRate = 5000)
fun reportCurrentTime1() {
log.info("[TaskScheduler1] The tims is now ${dataFormat.format(Date())}")
}
@SchedulerLock(
name = "TaskScheduler",
lockAtLeastFor = "PT10S",
lockAtMostFor = "PT10S"
)
@Scheduled(fixedRate = 5000)
fun reportCurrentTime2() {
log.info("[TaskScheduler2] The tims is now ${dataFormat.format(Date())}")
}
ShedLock이 제대로 적용되었다면 두 스케줄러의 lock의 name이 TaskScheduler
로 동일하기 때문에 10초마다 하나의 스케줄러만 동작해야합니다.
ShedLock에 의해 10초마다 하나의 로그만 잘 찍히는 걸 볼 수 있습니다.
case2) 다른 lock 이름을 준 경우
@Component
class ScheduledTasks {
// ...
@SchedulerLock(
name = "TaskScheduler",
lockAtLeastFor = "PT10S",
lockAtMostFor = "PT10S"
)
@Scheduled(fixedRate = 5000)
fun reportCurrentTime1() {
log.info("[TaskScheduler1] The tims is now ${dataFormat.format(Date())}")
}
@SchedulerLock(
name = "TaskScheduler2",
lockAtLeastFor = "PT10S",
lockAtMostFor = "PT10S"
)
@Scheduled(fixedRate = 5000)
fun reportCurrentTime2() {
log.info("[TaskScheduler2] The tims is now ${dataFormat.format(Date())}")
}
두번째 스케줄러의 name을 TaskScheduler2
로 바꿔줬습니다. 이 경우 name이 다르기 때문에 10초마다 두개의 로그가 찍혀야합니다.
잘 동작하는 걸 확인할 수 있습니다.
redis에도 name 값을 key 값으로 ShedLock
이 잘 저장된 걸 확인할 수 있습니다.