Spring · 분산 스케줄링
ShedLock — 스케줄 태스크가 여러 서버에서 중복 실행되는 걸 막아요
기술백엔드spring스케줄러락
목차
ShedLock을 이 순서로 살펴봐요
1무슨 문제를 푸는지, 어디까지가 역할인지 짚어봐요
2어떻게 설정하고 어떻게 동작하는지 뜯어봐요
3한계와 다른 도구와의 차이를 정리해요
01
스케줄 태스크가 인스턴스마다 다 돌아요
서버를 여러 대로 늘리면 @Scheduled가 모든 대에서 동시에 실행돼요.
문제
같은 배치가 대수만큼 중복 실행돼요
스케줄 발동
매 15분
→
서버 A 실행
서버 B 실행
서버 C 실행
→ 정산·메일 발송·집계가 3번 돌아 데이터가 어긋나거나 중복 발송돼요. 락으로 한 대만 실행하게 만들어야 해요.
@Scheduled: Spring이 정해진 주기/시각에 메서드를 자동 실행하는 기능 · 인스턴스: 같은 앱을 띄운 각 서버 프로세스
무엇인가
ShedLock은 "스케줄 태스크 전용 락"이에요
🎯
하는 일
한 태스크가 동시에 최대 1회만 실행되게 락을 걸어요.
⏭️
대기 없이 skip
이미 다른 노드가 잡았으면 기다리지 않고 그냥 건너뛰어요.
🚫
아닌 것
스케줄러도, 범용 분산 락(뮤텍스)도 아니에요.
뮤텍스(mutex): 임계 구역을 한 번에 하나만 들어가게 하는 범용 락 — 비즈니스 로직 보호는 ShedLock이 아닌 이쪽 영역
전제: 태스크는 반복 실행돼도 안전해야 해요
ShedLock은 "동시 실행"을 막을 뿐, 재시도·다음 주기 재실행까지 막지 않아요. 멱등하게 짜두세요.
02
애너테이션 두 개와 저장소 하나면 돼요
기존 @Scheduled는 그대로 두고, 락 계층만 얹어요.
구성요소
이 세 가지로 락을 구성해요
🔧
@EnableSchedulerLock
설정 클래스에 붙여 락 기능을 켜고, 전역 기본값을 정해요.
🏷️
@SchedulerLock
락을 걸 스케줄 메서드에 붙여요. 이게 붙은 메서드만 대상이에요.
🗄️
LockProvider
DB·Redis 같은 공유 저장소를 락 백엔드로 연결해요.
락 이름(name)은 저장소의 레코드 1개에 매핑돼요 — 같은 이름 = 클러스터 전체에서 동시 1개만 실행
설정 코드
LockProvider를 빈으로 등록해요
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
class SchedulerConfig {
@Bean
LockProvider lockProvider(DataSource ds) {
return new JdbcTemplateLockProvider(
Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(ds))
.usingDbTime() // DB 서버 시각 사용
.build());
}
}
시간 값은 ISO-8601 Duration 포맷 — PT30S=30초, PT5M=5분.
usingDbTime(): 각 서버 시각 대신 DB 서버 시각을 기준으로 삼아 노드 간 시계 차이 문제를 피하는 옵션
태스크 코드
스케줄 메서드에 락을 붙여요
@Scheduled(cron = "0 */15 * * * *")
@SchedulerLock(
name = "reportTask",
lockAtLeastFor = "PT5M",
lockAtMostFor = "PT14M")
public void run() {
// 15분마다, 클러스터에서 한 대만 실행
}
@SchedulerLock을 빠뜨리면 락이 안 걸려요 — 붙이는 걸 잊지 마세요.
cron: 실행 시각을 정하는 표현식 · name: 이 태스크의 락 식별자(유니크해야 함)
두 개의 타이밍
두 파라미터가 서로 다른 사고를 막아요
lockAtMostFor
- 노드가 release 없이 죽었을 때 락이 유지되는 최대 시간
- 크래시 시 데드락을 막는 안전망
- 정상 실행 시간보다 훨씬 길게 잡아야 해요
lockAtLeastFor
- 태스크가 빨리 끝나도 최소 이만큼 락 유지
- 초단타 실행 + 시계 차이로 인한 이중 실행 방지
- 보통 실행 주기보다 짧게 잡아요
데드락(deadlock): 락을 쥔 채 풀지 못해 아무도 진행 못 하는 상태 · 시계 차이(clock skew): 서버마다 시각이 조금씩 어긋나는 것
정상 동작
락은 시작에 잡고 끝에 풀어요
1태스크 시작 시 락을 획득해요. 다른 노드는 실패하면 그냥 skip해요.
3단, lockAtLeastFor가 아직 안 지났으면 그 시점까지는 계속 잡고 있어요.
4노드가 죽어 풀지 못하면, lockAtMostFor 시점에 자동으로 만료돼요.
즉 락 해제 시각 = max(작업 종료, 획득 시각 + lockAtLeastFor), 최악의 경우 획득 + lockAtMostFor
DB 모델
락은 shedlock 테이블 한 행으로 표현돼요
| 컬럼 | 의미 |
| name (PK) | 락 이름 = 태스크 식별자. 기본 키라 같은 이름은 한 행만 존재 |
| lock_until | 락 만료 시각. 이 시각이 지나야 다른 노드가 잡을 수 있어요 |
| locked_at | 락을 획득한 시각 |
| locked_by | 락을 쥔 노드/프로세스 식별자 |
PK(Primary Key): 행을 유일하게 식별하는 기본 키 — 같은 name 두 개가 못 들어가게 막아 상호 배제를 보장
획득·해제
한 행을 두고 원자적으로 경쟁해요
1행이 없으면 INSERT, 있으면 lock_until ≤ now일 때만 UPDATE로 가져와요.
2이미 유효한 락(lock_until > now)이면 획득 실패 → 이 노드는 태스크를 skip해요.
3두 노드가 동시에 같은 name을 노려도 PK 제약·원자성 덕에 한쪽만 성공해요.
4해제는 lock_until = now로 갱신 (단, lockAtLeastFor가 더 나중이면 그 시각으로).
원자성(atomicity): 여러 작업이 중간에 끼어듦 없이 전부 되거나 전부 안 되는 성질 · now: 현재 시각(가능하면 DB 시각)
저장소 선택
다양한 저장소를 백엔드로 쓸 수 있어요
🗃️
RDB / JDBC
JdbcTemplate·R2DBC·JPA·jOOQ. 가장 일반적, DB 시각 사용 권장.
⚡
Redis · Hazelcast
Redis(Jedis·Lettuce)·Hazelcast·Memcached 등 인메모리 계열.
🧭
Mongo · ZK · etcd
MongoDB·ZooKeeper·etcd·DynamoDB·Consul 등도 지원해요.
⚠️ 보장 수준은 저장소에 종속돼요 — 예: Redis는 master 장애 시 신뢰성이 떨어질 수 있어요.
프로바이더(LockProvider): 락 저장·검사를 특정 저장소로 구현한 어댑터 · failover: 장애 시 대기 노드로 전환
03
"최대 1회"이지 "정확히 1회"는 아니에요
한계를 알아야 안전하게 써요. 특히 lockAtMostFor 설정이 중요해요.
가장 위험한 실수
실행이 lockAtMostFor보다 길면 두 번 돌아요
1서버 A가 락을 잡고 무거운 배치를 시작해요 (lockAtMostFor = 5분).
35분에 락이 자동 만료돼, A가 아직 도는 중에 풀려버려요.
4다음 주기에 서버 B가 락을 잡고 같은 배치를 또 시작해요 💥
5해결: lockAtMostFor를 최악 실행 시간보다 넉넉히 크게 잡아요.
lockAtMostFor는 "정상 종료를 기다리는 값"이 아니라 "죽었다고 간주하는 한계" — 실행 시간보다 항상 길어야 해요
한계 요약
이 네 가지 한계를 기억하세요
⚠️ 최대 1회 (정확히 1회 아님)
skip된 노드는 그냥 안 해요. 누락 없는 "정확히 1회"가 필요하면 별도 설계가 필요해요.
⚠️ 시계에 의존
노드 시각이 동기화됐다고 가정해요. usingDbTime()으로 완화해요.
⚠️ 저장소별 신뢰성
Redis master 장애 등 백엔드 한계를 그대로 물려받아요.
⚠️ 스케줄 전용
비즈니스 로직 상호 배제엔 쓰지 마세요.
체크리스트
쓰기 전에 이걸 확인하세요
1lockAtMostFor > 최악 실행 시간 — 가장 흔한 사고 지점이에요.
2락 걸 메서드마다 @SchedulerLock을 빠짐없이 붙였나요?
3가능하면 usingDbTime()으로 시계 의존을 없앴나요?
4초단타 태스크엔 lockAtLeastFor로 이중 실행을 막았나요?
5락 name이 유니크한가요? (다른 태스크와 이름이 겹치면 서로 배제돼요)
도구 비교
목적에 따라 도구가 달라요
| 도구 | 목적 | 특징 |
| ShedLock | 스케줄 태스크 중복 방지(락만) | 테이블 1개·check+update만 → 부하 낮고 설정 단순. 스케줄링은 @Scheduled가 담당 |
| Quartz (clustered) | 완전한 잡 스케줄러 | 트리거·클러스터링·failover 내장. 강력하지만 부하·설정 복잡 |
| Redlock·범용 락 | 비즈니스 로직 상호 배제 | 임의 임계 구역 보호. ShedLock이 대체하지 않는 영역 |
Quartz: 자바의 대표적 잡 스케줄러 · TPS(Transactions Per Second): 초당 처리 건수 — 락 관리를 ShedLock에 위임해 개선한 사례도 있어요
선택 가이드
스케줄 중복만 막을 거면 ShedLock이 딱이에요
ShedLock으로 충분
- 다중 인스턴스에서 @Scheduled 중복만 막고 싶을 때
- 이미 DB·Redis 같은 공유 저장소가 있을 때
- 가볍게, 코드 변경 최소로 얹고 싶을 때
다른 선택이 필요
- 잡 저장·재시도·부하분산까지 → Quartz
- 비즈니스 임계 구역 보호 → 범용 분산 락
- 누락 없는 "정확히 1회" → 별도 설계
스케줄 중복만 막을 거면 ShedLock,
그 이상은 다른 도구를 쓰세요
기술 · 백엔드 · spring · 스케줄러락