개요
회사에서 프로젝트를 진행하던 중, ECS 기반 다중 서버 환경에서 동일한 스케줄러 작업이 동시에 실행되는 문제를 마주치게 된 적이 있었다.
각 스케줄러는 사용자 인스턴스2개 , 관리자 인스턴스 2개가 올라가 있었고 서버 인스턴스가 늘어나면서 같은 작업이 여러 번 실행되는 상황이 발생했다. 그 결과 불필요한 DB 접근, 자원 낭비, 성능 저하 같은 부작용이 우려되었다.
그때는 이 문제를 막기 위해 ShedLock + RDB 조합을 사용했다.
DB 테이블에 락 정보를 저장해 두고, 한 번에 한 서버만 해당 스케줄러를 실행하도록 막는 방식이었다. 문제 자체는 해결됐지만, 지금 생각해 보면 아쉬움이 크다.
락 제어만을 위해 DB에 주기적으로 쓰기/삭제를 반복시키는 건 DB의 본래 역할(트랜잭션 데이터 관리)과는 거리가 있었다.
더 아쉬운 건, 당시 이미 Redis를 서비스 내에서 캐싱 용도로 쓰고 있었다는 점이다. 상품 전체 조회나 자주 조회되는 시험지 데이터는 Redis 캐싱으로 처리했는데, 정작 락 관리는 DB에 맡겨버렸다. 지금 돌아보면 락 제어만큼은 Redis로 풀었으면 불필요한 DB 접근을 줄이고 훨씬 효율적으로 처리할 수 있었을 것이다.
그래서 이번엔 같은 문제 상황을 Redis 기반으로 개선해 봤다.
Redis는 인메모리 기반이기 때문에
- 읽기/쓰기 속도가 빠르고,
- TTL(만료 시간) 관리가 유연하며,
- 분산 환경에서의 락 처리에도 최적화된 도구
라는 장점이 있다.
즉, “스케줄러 로직 자체는 여전히 DB 트랜잭션으로 처리”되어야 하지만, 락 제어만큼은 DB 대신 Redis가 더 적합하다 할 수 있겠다.라는 생각이 들었다.
ShedLock이 적용되기 전 Redis를 이용한 코드
@Scheduled(cron = "0 * * * * *") // 매 분 0초
public void runTask() throws UnknownHostException {
String serverName = InetAddress.getLocalHost().getHostName();
String message = "[" + serverName + "] 실행됨 at " + LocalDateTime.now();
// Redis에 기록 (List 구조로 append)
redisTemplate.opsForList().rightPush(COUNTER_KEY, message);
System.out.println(message);
}
다중 서버를 가정한 두 개의 서버를 실행 후 로그

두 개의 스케줄러 작업이 다른 서버에서 같이 이루어지는 걸 볼 수 있었다. " 중복되는 작업을 두 번이나 실행" 하게 된다. Scale-Out이 커질수록 스케줄링이 중복으로 호출되는 횟수도 증가할 것이다. 해당 문제 해결을 위해
ShedLock을 사용하여 스케줄링 Lock 걸기
ShedLock이란?
ShedLock이란 여러 대의 인스턴스가 존재할 때, 동일한 스케줄링이 중복으로 수행되지 않도록 Lock을 걸어주는 라이브러리이다. 예를 들어, 인스턴스 A, B가 존재할 때 A에서 스케줄링이 실행되면 Lock이 걸리기 때문에 B에서 동일한 스케줄링을 실행할 수 없다.
테이블 생성
RDB를 사용하는 경우 ShedLock을 위한 테이블 생성이 필요하다.
CREATE TABLE shedlock (
name VARCHAR(64),
lock_until TIMESTAMP(3) NULL,
locked_at TIMESTAMP(3) NULL,
locked_by VARCHAR(255),
PRIMARY KEY (name)
)
RDB의 경우
- DB는 기본적으로 단순 저장소라서 “락”이라는 개념이 없음.
- ShedLock이 락을 흉내 내려면 shedlock 테이블에 행을 만들어서
- lock_until: 언제까지 유효한지
- locked_at: 언제 락을 잡았는지
- locked_by: 어느 서버에서 잡았는지
- 이런 메타데이터를 저장해야 함
- 이 테이블이 있어야 여러 서버가 같은 DB를 보면서 “누가 락을 잡았는지” 확인 가능.
레디스의 경우
- Redis는 SETNX (SET if Not Exists) 같은 원자적 연산을 제공 → 분산락을 구현하기 쉬움.
- ShedLock은 이 기능을 그대로 활용해서 key=value로 락을 관리함.
예를 들어
SET myTask-lock "server1" NX PX 5000
- NX: 없을 때만 생성 (이미 있으면 실패)
- PX 5000: 5초 후 자동 만료 (TTL)
즉, 락을 잡으면 Redis key에 TTL이 걸려서 자동으로 풀림 → 테이블처럼 수동 관리 필요 없음.
- RDB → “락 상태”를 테이블로 관리해야 하므로 shedlock 테이블 필요.
- Redis → 원래부터 락 관련 원자 연산과 TTL 기능을 제공하므로 테이블 불필요, 그냥 key-value로 관리.
내가 Redis 쓰니까 테이블 만들 필요가 없는 거고, 대신 lockAtMostFor, lockAtLeastFor 같은 옵션이 Redis key의 TTL로 적용되고 있다.
의존성 추가
// shedlock 관련 라이브러리
implementation 'net.javacrumbs.shedlock:shedlock-spring:5.13.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:5.13.0'
설정 파일 생성
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class SchedulerConfig {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, "shedlock");
}
}
- P = Period (기간 시작)
- T = Time (시간 단위 시작)
- 30S = 30초 (Seconds)
비즈니스 로직 작성 (수정 후)
DB 방식은 shedlock 테이블의 row를 이용해 잠금을 관리하는 반면,
Redis 방식은 key-value 기반으로 @SchedulerLock(name)이 key가 되어,
해당 key를 SETNX + expire(만료시간)으로 저장해 분산락을 보장한다.
@Scheduled(cron = "0 * * * * *") // 매 분 0초마다 실행
@SchedulerLock(name = "myTask", lockAtMostFor = "PT5M", lockAtLeastFor = "PT5S")
public void runTask() throws UnknownHostException {
String serverName = InetAddress.getLocalHost().getHostName();
String timestamp = LocalDateTime.now().toString();
// 실행 로그 메시지
String message = "[" + serverName + "] 실행됨 at " + timestamp;
// Redis에 실행 로그 저장
redisTemplate.opsForList().rightPush(EXECUTION_KEY, message);
// 콘솔 + 로그에도 출력
System.out.println(message);
log.info(message);
}
해당 코드를 보면 최소 5초 이상 최대 5분까지 락을 들고 있는 설정
다중 인스턴스에서 ShedLock적용 후 로그


각서버에서 ShedLock이 적용되어 존재할 땐 스케줄러가 돌지 않는 것이 확인된다.
늘 개발을 하고 지나면 더 나은 방법이 생각이 나지만 아쉽게도 그날의 나는 그게 최선이었을 수도 있다.
참고 자료
IntelliJ 스프링부트 서버 2대 이상 실행하는 법
인텔리제이에서 스프링부트를 실행할 때, n(>=2) 대 이상의 서버를 실행할 경우가 있다. 1. Edit Configurarions 클릭 필자는 이미 2대의 서버를 생성해둔 상태이다. 2. '+' 버튼 클릭 후 Springboot 선택 3. 기
junior-datalist.tistory.com
GitHub - lukas-krecan/ShedLock: Distributed lock for your scheduled tasks
Distributed lock for your scheduled tasks. Contribute to lukas-krecan/ShedLock development by creating an account on GitHub.
github.com
'Java > Spring' 카테고리의 다른 글
| Redis 캐싱 적용과 TTL, 캐시 전략 패턴 활용기 (0) | 2025.08.22 |
|---|---|
| open-in-view=false 환경에서 LazyInitializationException → 403 에러 전파 사례 (0) | 2025.08.20 |
| Kafka Listener 예외 반복 실행되는 이유 그리고 해결 (1) | 2025.08.15 |
| 전략 패턴에서 Object vs 제네릭, 왜 나는 제네릭을 선택했을까? (2) | 2025.07.30 |
| Spring에서 확장성을 고려한 채팅방 생성 로직 전략 패턴 적용 (1) | 2025.07.24 |