- ShedLock: 분산 환경에서
@Scheduled작업이 동시에 두 번 이상 실행되지 않도록 보장하는 라이브러리.net.javacrumbs.shedlock패키지에서 제공한다.- 핵심 애노테이션이
@SchedulerLock이라, 흔히 SchedulerLock으로도 불린다. - 스케줄러 자체를 대체하지 않는다. 스프링의
@Scheduled는 그대로 두고, 그 위에 “한 번만 실행” 제약만 얹는 동시성 제어 도구다.
- 핵심 애노테이션이
왜 필요한가
- 무상태 애플리케이션을 여러 인스턴스로 띄우면, 각 인스턴스가 자기 스케줄러를 갖는다.
@Scheduled(cron = "0 0 * * * *")하나만 붙여두면 인스턴스가 3대일 때 매시 정각에 같은 작업이 3번 실행된다.- 결제 정산, 배치 집계, 알림 발송 같은 작업이 중복 실행되면 데이터가 어긋난다.
- 스케줄러를 한 인스턴스에만 켜는 방법도 있지만, 그 인스턴스가 죽으면 작업이 아예 멈춘다. ShedLock은 모든 인스턴스에 스케줄러를 켜두되 실행 권한만 한 번에 하나로 제한한다.
동작 원리
- 작업이 트리거되면 외부 저장소(DB·Redis 등)에
name을 키로 락 레코드를 쓰려고 시도한다.- 락 획득 성공 → 작업을 실행한다.
- 락 획득 실패 → 기다리지 않고 그냥 건너뛴다(skip). 이미 다른 인스턴스가 실행 중이라는 뜻이므로 대기할 이유가 없다.
- 작업이 끝나면 락을 해제한다.
- 비차단(non-blocking) 방식이다. 같은 락으로 보호되는 다른 작업은 블로킹되지 않고 단순히 건너뛰어진다.
- 모든 노드의 시계가 동기화되어 있어야 한다. 락 만료 판단이 시각 비교에 기반하기 때문이다.
사용 방법
의존성
implementation("net.javacrumbs.shedlock:shedlock-spring:7.7.0")
// LockProvider 별로 추가 의존성이 필요하다 (예: shedlock-provider-redis-spring)
활성화
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class SchedulerConfig {
@Bean
fun lockProvider(connectionFactory: RedisConnectionFactory): LockProvider =
RedisLockProvider(connectionFactory, "nomat")
}
@EnableScheduling(스프링 기본)과@EnableSchedulerLock(ShedLock)을 함께 붙인다.LockProvider빈을 하나 등록해 어떤 저장소로 락을 잡을지 지정한다.
작업에 락 걸기
@Scheduled(cron = "0 */15 * * * *")
@SchedulerLock(name = "settleDaily", lockAtMostFor = "14m", lockAtLeastFor = "1m")
fun settleDaily() {
LockAssert.assertLocked() // 락을 잡은 상태로 실행 중인지 방어적으로 확인
// ...
}
name은 락을 식별하는 고유 키다. 서로 다른 작업이면 반드시 다른 이름을 써야 한다.LockAssert.assertLocked()는 이 메서드가 정말 락을 획득한 채 실행되는지 검증한다. 설정 누락으로 락 없이 실행되는 실수를 조기에 잡아준다.
lockAtMostFor 와 lockAtLeastFor
lockAtMostFor: 락을 잡은 노드가 죽거나 응답이 없어도 이 시간이 지나면 락이 강제로 풀린다.- 안전장치다. 이 값이 없으면 노드가 죽었을 때 락이 영원히 안 풀려 작업이 멈춘다.
- 실제 실행 시간보다 훨씬 길게 잡아야 한다. 작업이 아직 도는 중인데 락이 풀리면 다른 노드가 끼어들어 중복 실행된다.
lockAtLeastFor: 작업이 아무리 빨리 끝나도 최소 이 시간 동안은 락을 유지한다.- 작업이 몇 밀리초 만에 끝나는 아주 짧은 경우를 위한 장치다. 노드 간 시계 오차 때문에 여러 노드가 거의 동시에 락을 잡아 중복 실행되는 상황을 막는다.
- 표기는
"10m","5s"같은 단순 문자열이나 ISO-8601("PT10M","PT5S") 둘 다 된다.
LockProvider 종류
락 상태를 저장할 백엔드를 고르는 부분이다. 20종 이상 지원한다.
SQL(JdbcTemplateLockProvider):
shedlock테이블에 락을 기록한다. 아래 스키마가 필요하다.CREATE TABLE shedlock( name VARCHAR(64) NOT NULL PRIMARY KEY, lock_until TIMESTAMP NOT NULL, locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL );.usingDbTime()을 켜면 애플리케이션 서버 시계 대신 DB 서버 시계를 기준으로 만료를 판단한다. 노드 간 시계 오차 문제를 피할 수 있어 권장된다.
Redis(RedisLockProvider):
RedisConnectionFactory만 있으면 된다. 이미 Redis를 쓰는 프로젝트라면 별도 테이블 없이 가장 가볍게 붙는다.그 외 MongoDB, Hazelcast, ZooKeeper, DynamoDB, Elasticsearch, Cassandra 등.
주의사항
@SchedulerLock이 붙은 메서드는public이어야 하고final이면 안 된다. AOP 프록시로 동작하기 때문이다.lockAtMostFor를 실제 실행 시간보다 넉넉히 크게 잡는다. 짧게 잡으면 락이 조기 해제돼 중복 실행이 발생한다.- 애노테이션이 붙은 메서드는 스케줄러가 부르든 코드에서 직접 부르든 락이 걸린다.
- 자바의
Lock이나 분산 락 자체 개념은Redis Lock 사용하기참고. ShedLock은 그 분산 락을 “스케줄러 중복 실행 방지"라는 목적에 특화해 애노테이션으로 감싼 것으로 이해하면 된다.