Spring의 @Scheduled 어노테이션은 Spring Framework에서 쉽게 cron을 구성할 수 있는 유용한 기능입니다. 그런데 WAS를 늘리게 되면 상당히 골치아픈 상황이 벌어집니다. cron작업이 WAS마다 실행되기 때문에 WAS의 개수만큼 중복된 cron이 실행되기 때문인데요 이러한 상황을 해결할 수 있는 방법을 정리해보려고 합니다.
Scheduler Lock
위와 같이 2개 이상의 서버로 구성된 환경에서 중복된 Schedule이 실행되지 않도록 Lock을 걸 수 있게 만들어놓은 라이브러리입니다. maven repository에 shedlock이라는 이름으로 올라와있습니다. 그 중에 shedlock-spring과 shedlock-provider-jdbc-template을 추가합니다.
- shedlock-spring : https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring
- shedlock-provider-jdbc-template : https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-provider-jdbc-template
shedlock-provider-jdbc-template을 추가하는 것을 보면 데이터베이스(DataBase)를 이용하는 것을 예상할 수 있습니다. 이 포스팅에서는 RDB를 통해 lock을 하는 방벙블 정리하지만 RDB 말고도 maven repository 검색결과 목록에 MongoDB, Redis provider도 보이는 걸 보면 다른 저장소를 통해서도 가능한 것으로 보입니다. (개인적으로는 Redis로 하는 방법을 추가로 정리 해보고 싶네요)
pom.xml에 라이브러리 추가
<!-- Scheduler lock -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>3.0.0</version>
</dependency>
<!-- JDBC Template -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>3.0.0</version>
</dependency>
Bean 등록
public class ScheduleController {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
}
위와같이 java로 만들어도 되고 아래와 같이 xml로 만들어도 됩니다.
<bean id="lockProvider" class="net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider">
<constructor-arg ref="dataSource"/>
</bean>
만약 기존에 연동된 jdbc 또는 DB Connector가 없는 경우에는 DB 연동부터 설정해주셔야 합니다. 여기서는 이미 연동된 상태를 전제로 설명합니다.
여기서 ref="dataSource"는 기존에 연결되어있는 dataSource 명을 넣어주시면 됩니다.
DB 테이블 생성
Schedule Lock을 사용하기 위해 위에서 설정한 dataSource에 해당하는 DB에 테이블을 생성합니다. 테이블을 통해서 lock기능을 사용할 수 있습니다.
DROP TABLE IF EXISTS `shedlock` RESTRICT;
CREATE TABLE `shedlock` (
`name` VARCHAR(64) NOT NULL COMMENT '스케줄잠금이름', -- 스케줄잠금이름
`lock_until` TIMESTAMP(3) NULL COMMENT '잠금기간', -- 잠금기간
`locked_at` TIMESTAMP(3) NULL COMMENT '잠금일시', -- 잠금일시
`locked_by` VARCHAR(255) NULL COMMENT '잠금신청자' -- 잠금신청자
)
COMMENT '스케줄잠금';
ALTER TABLE `shedlock` ADD CONSTRAINT `PK_shedlock` -- 스케줄잠금 기본키
PRIMARY KEY (
`name` -- 스케줄잠금이름
);
여기서 name은 어노테이션에서 전달할 이름입니다. 고유값으로 들어가고 이름으로 스케줄을 구분합니다.
@SchedulerLock
어노테이션을 사용해서 lock을 걸어줍니다.
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S") // Scheduler Lock 사용 가능 설정 (기본 30초동안 Lock)
public class BatchController {
private static final String ONE_MIN = "PT1M"; // 1분동안 LOCK
@Scheduled(cron = "0 10 * * * ?") // 초(0~59) 분(0~59) 시(0~23) 일(1~31) 월(1~12) 요일(1~7, 일요일 : 1) 연도(생략가능)
// Schdule Lock (1분동안)
@SchedulerLock(name = "runScenarioOneTime", lockAtMostForString = ONE_MIN, lockAtLeastForString = ONE_MIN)
public void runScenarioOneTime() throws Exception {
// do something...
}
@Scheduled(cron = "0 20 * * * ?")
// Schdule Lock (1분동안)
@SchedulerLock(name = "runScenarioCycle", lockAtMostForString = ONE_MIN, lockAtLeastForString = ONE_MIN)
public void runScenarioCycle() throws Exception {
// do something...
}
}
이렇게 간단한 어노테이션 설정을 통해서 각 WAS가 Scheduling할 때 생성한 테이블을 참조하여 아래와 같은 작업이 이루어집니다.
- shedlock 테이블에 같은 이름의 데이터가 있는지 여부 파악
- 같은 이름이 있는 경우
- 잠금 기간 및 잠금 일시를 참조하여 Scheduling 중단
- 같은 이름이 없는 경우
- shedlock 테이블에 이름, 잠금일시, 기간 등을 설정하여 insert
- 같은 이름이 있는 경우
이 글은 블로그를 참조하여 작성하였습니다.