지난 포스트에서 강결합 서비스를 분리하는 내용을 다뤘습니다.
이번 포스트에서는 ShedLock을 활용하여 다중 서버에서 Spring 스케쥴링 기능을 안전하게 사용하는 방법을 적어보겠습니다.
저의 경우엔 해당 서비스의 분리를 Spring 스케쥴링과 활용해서, 매일 정해진 시간에 데이터의 이상이 있는 펀드를 찾고 메신저로 정보를 받아보는데 사용하고자 했습니다.
그런데 @Scheduled
를 사용해서 스케쥴링을 단순히 하려고 하니 한가지 문제점이 있었습니다.
현재 시스템의 구성에서 WAS 서버는 두대를 사용하고 있었습니다. 그런데 Spring 애플리케이션을 각각의 서버에서 띄우게 되면 스케쥴링이 개별적으로 동작하는 문제가 예상되었습니다.
@Component
class ScheduledFundCheck(private val fundUpdateServiceWithEvent: FundUpdateServiceWithEvent) {
private val log = LoggerFactory.getLogger(this::class.java)
@Value("\${server.port}")
lateinit var serverPort: String
@Scheduled(fixedRate = 5000)
fun scheduledTask() {
fundUpdateServiceWithEvent.asyncUpdateFund()
log.info("실행된 포트는 :$serverPort 입니다.")
}
}
다음과 같이 스케쥴링된 상황을 예를들어서, 로컬에서 8080, 8081 포트로 두개의 톰캣 서버를 띄워보겠습니다.
당연하게도 각각의 서버에서 스케쥴링이 동작하고, 개별적으로 메서드가 실행되게 됩니다.
이렇게 되면 저의 상황에선 두 개의 서버에서 각각 펀드를 확인하고 메신저 API를 호출하게 됩니다.
저희 시스템 현재 구조는 WAS가 두 대에서 더이상 스케일 아웃될 상황이 없고, 해당 스케쥴 작업은 매일 한번씩 짧은 시간안에 끝나기 때문에 간단히 ShedLock을 사용하기로 결정했습니다.
ShedLock 해당 포스트에서 ShedLock의 사용법을 확인 할 수 있습니다.
현재 운영중인 시스템에선 RDBMS를 사용하기 때문에 JdbcTemplate 설정을 적용하겠습니다.
먼저 의존성을 추가합니다.
implementation("net.javacrumbs.shedlock:shedlock-spring:5.8.0")
implementation("net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.8.0")
ShedLock에서 사용할 테이블을 만들어줍니다.
CREATE TABLE shedlock
(
name VARCHAR(64) NOT NULL,
lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);
그리고 설정 클래스를 만들고 LockProvider를 Bean으로 설정합니다.
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class ScheduleConfig {
@Bean
fun lockProvider(dataSource: DataSource): LockProvider {
return JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(JdbcTemplate(dataSource))
.usingDbTime() // Works on Postgres, MySQL, MariaDb, MS SQL, Oracle, DB2, HSQL and H2
.build()
)
}
}
@Scheduled
가 적용된 메서드에 @SchedulerLock
애너테이션을 추가합니다.
package com.tistory.tech.gyutech.service
import net.javacrumbs.shedlock.core.LockAssert
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
@Component
class ScheduledFundCheck(private val fundUpdateServiceWithEvent: FundUpdateServiceWithEvent) {
private val log = LoggerFactory.getLogger(this::class.java)
@Value("\${server.port}")
lateinit var serverPort: String
@Scheduled(fixedRate = 5000)
@SchedulerLock(name = "scheduledTaskName", lockAtLeastFor = "4s")
fun scheduledTask() {
// To assert that the lock is held (prevents misconfiguration errors)
LockAssert.assertLocked()
fundUpdateServiceWithEvent.asyncUpdateFund()
log.info("실행된 포트는 :$serverPort 입니다.")
}
}
다시 두 개의 포트에 서버를 띄워서 확인해보겠습니다.
8080 포트를 사용하는 서버에서 Lock을 소유하고 해제하고를 반복하는 모습을 볼 수 있습니다.
8081 서버에서는 "It's locked."라는 메시지를 출력하며 스케쥴링된 작업이 수행되지 않는 모습을 볼 수 있습니다.
여기서 8080 포트를 재시작해보면 ..
Lock을 얻지 못하던 8081 포트에서 Lock을 획득하여 로직을 수행하는 모습을 확인 할 수 있습니다.
현재 운영 환경에서도 이와 비슷한 설정으로, 두 대의 서버를 가용하면서 한 쪽 서버에서만 Spring의 스케쥴링 기능이 동작하게 만들 수 있었습니다.