[Spring] Scheduling

Junseo Kim·2022년 9월 19일
3

Spring

목록 보기
7/7
post-thumbnail

통계성 작업을 일정 시간마다 수행하여 알려준다던지 하는 식으로 따로 요청을 보내지 않아도 일정 시간 마다 반복되는 작업이 필요한 경우가 있습니다.

기본 적용 방법

spring을 사용하여 아주 간단한 스케줄러를 만들어보도록 하겠습니다.

  1. @EnableScheduling 어노테이션을 붙여준다.
    • 스케줄러들이 동작할 수 있게 해줍니다.
    • config class로 따로 분리해줘도 됩니다.
@SpringBootApplication
@EnableScheduling // background task executer가 생성된다.
class SchedulingApplication

fun main(args: Array<String>) {
    runApplication<SchedulingApplication>(*args)
}
  1. 스케줄러 메서드를 만들어줍니다.
    • 아래 예시는 5초마다 현재 시간 로그를 찍어주는 스케줄러입니다.
@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

@EnableScheduling@Scheduled 어노테이션을 감지하고 만들어 준 스케줄러를 동작할 수 있게 해주는 어노테이션입니다.

spring은 고유한TaskScheduler 인터페이스 타입의 빈이나 taskScheduler 라는 이름을 가진 빈을 찾습니다. 만약 존재하지 않는다면 자동 설정에 의해 local single thread 스케줄러가 생성되어 사용됩니다.

@Scheduled

스케줄러 메서드를 표시하기 위한 어노테이션입니다. 이 메서드는 인수를 가질 수 없습니다. 일반적으로 void return 형태로 사용하여 그렇지 않다면 return 값이 무시될 수 있습니다.

  • cron: cron 표현식을 지원. ‘초 분 시 일 월 주 (년)’ 으로 표현
  • fixedDelay: ms 단위. 이전 작업이 끝난 시점부터 고정된 호출 간격을 설정.
  • fixedRate: ms 단위. 이전 작업이 수행되기 시작한 시점부터 고정된 호출 간격 설정.
  • initialDelay: 초기 지연 시간.(스케줄러에서 메서드가 등록되자마자 수행하지 않고 설정해준 시간 후부터 동작)
  • zone: cron 표현식을 사용할 때 적용할 time zone. 따로 지정하지 않으면 서버의 time zone

Custom TaskScheduler 설정

위에서 말한 것 처럼 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

스프링은 기본적으로 여러 인스턴스의 경우 스케줄러 동기화 처리를 할 수 없습니다. 따라서 서버가 여러 인스턴스로 구성되어 있다면 모든 인스턴스에서 스케줄링 작업이 발생할 수 있습니다. 이런 경우 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을 붙여줍니다. @EnableSchedulerLockShedLock이 동작하도록 해줍니다.

@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이 잘 저장된 걸 확인할 수 있습니다.

0개의 댓글