[SpringBoot] Scale-out 환경에서 발생하는 Scheduler 중복 실행 문제 Shedlock으로 해결하기
스케줄러(Scheduler)를 사용해 정해진 시간에 반복 작업을 실행하고자 할 때, 다중 인스턴스 환경에서는 같은 작업이 여러 인스턴스에서 동시에 실행되는 상황이 발생하게 됩니다.
이번 포스팅에서는 `ShedLock`의 개념과 라이브러리를 활용하여 여러 인스턴스에서 Scheduler 사용 시 발생하는 문제를 해결하는 방법에 대해 알아보도록 하겠습니다.
문제 상황
Scheduler를 적용하면 지정한 일정 시간마다 실행하게 됩니다.
그런데 만약 여러 인스턴스 서버(Scale-out)를 띄울 경우 각 서버마다 존재하는 Scheduler가 동작하게 될 것입니다.
@Scheduled(zone = "Asia/Seoul", cron = "*/1 * * * * *") // 1초 간격으로 실행
public void run() {
log.info("Scheduler 실행");
}
1초 간격으로 실행되는 Scheduler를 작성하였습니다. 두 대의 서버를 띄워 실행하면
두 서버 모두 Scheduler가 실행되는 모습을 볼 수 있습니다. 이메일 발송 스케줄러로 본다면 똑같은 메일이 2번씩 발송되는 상황이 발생하게 됩니다. 이는 Scale-out이 커질수록 점점 더 큰 문제로 다가올 수 있습니다.
이 때 Shedlock을 사용하여 문제를 해결할 수 있습니다.
Shedlock란?
Shedlock은 Spring 기반의 애플리케이션에서 중복 실행 방지를 위한 라이브러리 입니다. 여러 서버나 여러 인스턴스에서 동일한 스케줄러가 동시에 실행되는 상황을 방지하기 위해 사용됩니다.
Shedlock은 분산 환경에서 특정 작업이 한 번만 실행되도록 보장하는 기능을 제공합니다. 기본적으로 데이터베이스를 활용하여 작업의 실행 여부를 관리합니다.
사용되는 라이브러리는 다음과 같습니다. Maven Repository 에서 버전별 확인이 가능합니다.
implementation 'net.javacrumbs.shedlock:shedlock-spring:6.2.0'
Spring 환경에서 Shedlock을 사용하기 위해 추가하는 라이브러리 입니다. 이 라이브러리는 기본적인 스케줄링 잠금 기능을 제공하며, 데이터베이스 또는 Redis와 연동할 수 있습니다.
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:6.2.0'
MySQL과 같은 관계형 데이터베이스 환경에서 Shedlock 설정 시 사용되는 라이브러리 입니다. 데이터베이스를 통해 잠금 상태를 관리하며, 스케줄러가 중복 실행되지 않도록 보장합니다.
implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:6.2.0'
Redis 환경에서 Shedlock 설정 시 사용되는 라이브러리 입니다. Redis를 통한 분산 잠금 기능을 사용할 수 있습니다.
Shedlock을 활용한 분산 스케줄러 적용
Shedlcok을 사용하면 여러 서버 인스턴스에서 동일한 스케줄러가 동시에 실행되는 문제를 해결할 수 있습니다. 이번 포스팅에서는 `RDBMS와 Redis 환경에서 Shedlock을 적용하는 방법`을 알아보도록 하겠습니다.
1. RDBMS 환경에서 적용
1-1. 의존성 추가
implementation 'net.javacrumbs.shedlock:shedlock-spring:6.2.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:6.2.0'
Shedlock 적용 전 의존성을 추가합니다.
1-2. Shedlock Table 생성
CREATE TABLE shedlock
(
name VARCHAR(64) NOT NULL COMMENT '이름',
locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '잠금 일시',
lock_until TIMESTAMP(3) NOT NULL COMMENT '잠금 기간',
locked_by VARCHAR(255) NOT NULL COMMENT '잠금 실행자',
PRIMARY KEY (name)
) COMMENT '스케줄 락킹';
적용하고자하는 스키마에 Shedlock 테이블을 생성합니다.
RDBMS 환경에서 Shedlock 적용시 꼭 해당 테이블을 생성해야 합니다. (테이블명 동일하게)
1-3. Scheduler Config 설정
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10s")
public class SchedulerConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime()
.build()
);
}
}
- `@EnableScheduling` Spring의 스케줄링 기능을 활성화하여 @Scheduled 어노테이션이 붙은 메서드가 실행됩니다.
- `@EnableSchedulerLock(defaultLockAtMostFor = "10s")` 기본 최대 lock 유지시간을 10초로 설정합니다.
- `DataSource` Spring에서 관리하는 DB연결 객체입니다. JDBC를 통해 Shedlock이 DB를 활용할 수 있도록 전달합니다.
- `JdbcTemplateLockProvider` Shedlock이 DB의 특정 테이블을 이용해 lock을 관리하도록 설정합니다.
- `.withJdbcTemplate(new JdbcTemplate(dataSource))` Shedlock이 DB 테이블을 조회하고 lock을 설정할 수 있도록 합니다.
- `.usingDbTime()`DB의 현재 시간을 기준으로 lock을 관리하도록 설정합니다.
1-4. Shedlock 적용
@Slf4j
@Component
@RequiredArgsConstructor
public class SchedulerTask {
@Scheduled(zone = "Asia/Seoul", cron = "*/1 * * * * *") // 1초 간격으로 실행
@SchedulerLock(name = "test", lockAtMostFor = "10s", lockAtLeastFor = "5s")
public void run() {
log.info("Scheduler 실행");
}
}
- `@SchedulerLock`어노테이션을 사용하여 해당 스케줄러 메서드에 lock을 활성화 합니다.
- `name` unique 한 값으로, 다른 스케줄러와 겹치지 않게 설정해야 합니다.
- `lockAtLeastFor` 작업을 마치고 해당 설정한 시간만큼 lock을 획득한 상태로 유지합니다.
- `lockAtMostFor` 해당 설정한 시간안에 작업을 끝내지 못하면 lock을 해제합니다.
초기 문제 상황과 달리 이제는 하나의 서버에서만 스케줄러가 작동되는 모습을 볼 수 있습니다.
반드시 하나의 서버에서만 실행되는 것은 아니며 lock 유무 체크가 먼저 일어난 서버에서 실행됩니다.
`실행 전 lock 유무 체크 → lock이 없다면 생성`
`설정한 name, lock이 생성된 시간, lock의 유효 시간, lock이 잡힌 서버`로 DB에 저장된 것을 확인할 수 있습니다.
2. Redis 환경에서 적용
2-1. 의존성 추가
implementation 'net.javacrumbs.shedlock:shedlock-spring:6.2.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:6.2.0'
마찬가지로 Shedlock 적용 전 의존성을 추가합니다.
2-2. Scheduler Config 설정
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class SchedulerConfig {
@Bean
public RedisLockProvider lockProvider(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockProvider(redisConnectionFactory);
}
}
ShedLock이 Redis를 통해 lock을 관리할 수 있도록 설정하는 Bean을 등록합니다.
- `RedisLockProvider` Redis를 사용해 분산 락을 제공하는 클래스입니다.
- `redisConnectionFactory` SpringBoot에서 Redis와의 연결을 관리하는 객체입니다. application.yml에서 설정된 Redis 정보를 사용하여 자동으로 연결 객체가 주입됩니다.
2-3. Shedlock 적용
@Slf4j
@Component
@RequiredArgsConstructor
public class SchedulerTask {
@Scheduled(zone = "Asia/Seoul", cron = "*/1 * * * * *") // 1초 간격으로 실행
@SchedulerLock(name = "test", lockAtMostFor = "10s", lockAtLeastFor = "5s")
public void run() {
log.info("Scheduler 실행");
}
}
이전 환경과 동일하게 설정하였습니다.
마찬가지로 하나의 서버에서만 스케줄러가 동작하는 모습을 볼 수 있습니다.
Shedlock 동작과정
- 스케줄러가 실행될 때, 먼저 Redis에 lock이 존재하는지 확인합니다.
- lock이 없으면 현재 실행중인 서버가 lock을 획득하고 스케줄러를 실행합니다.
- lock이 이미 존재하면 다른 서버에서는 스케줄러 실행이 되지 않습니다.
- 작업이 완료되면 lock이 해제되며, lockAtMostFor(최대 유지 시간)이 지나면 강제로 해제됩니다.
이미지와 같이 스케줄러에서 설정한 주기마다 lock 획득을 시도하는 모습을 볼 수 있습니다.
RDBMS vs Redis
`REBMS 동작 방식`
- Scheduler 실행 시 DB에 SELECT 쿼리로 lock 존재 여부 확인
- Lock이 없으면 INSERT 또는 UPDATE 쿼리 실행 (잠금 등록)
- 작업 수행 후 lock 해제 (또는 lockAtMostFor 시간이 지나면 자동 해제)
`Redis 동작 방식`
- Scheduler 실행 시 SETNX (Set if Not Exists) 명령어로 lock 설정 시도
- Lock이 존재하면 다른 서버는 실행 불가
- 작업이 끝나면 DEL 명령어로 lock 해제 (또는 TTL 종료)
구분 | RDBMS (JDBC 방식) | Redis (In-memory 방식) |
Lock 저장 위치 | DB 테이블 (shedlock 테이블) | Redis Key-Value Store |
동작 방식 | UPDATE, SELECT 쿼리 실행으로 lock 관리 | SETNX (Set if Not Exists) 방식으로 lock 관리 |
성능 (속도) | 상대적으로 느림 (쿼리 실행 필요) | 매우 빠름 (메모리 기반) |
설정 복잡도 | shedlock 테이블 생성 필요 | 별도 테이블 생성 필요 없음 |
장점 | 기존 DB를 활용 데이터 일관성이 높음 |
빠른 응답 속도(메모리 기반) 높은 확장성 (Scale-out) |
단점 | SQL 실행에 따른 오버헤드 존재 높은 트랜잭션 부하 시 서능 저하 가능성 존재 |
Redis 장애 시 lock 정보 유실 운영 중 Redis 설정 변경이 어려움 |
적용 방법 | `JdbcTemplateLockProvider` 사용 | `RedisLockProvider` 사용 |
추천 상황 | 기존 DB를 사용하는 경우 | 대규모 트래픽 환경 (빠른 분산 락 필요) |
마무리
이번 포스팅에서는 Shedlock을 활용하여 다중 인스턴스 환경에서 스케줄러 중복 실행을 방지하는 방법에 대해 알아보았습니다.
- RDBMS 기반 Shedlock은 데이터 일관성이 보장되지만, 성능 오버헤드가 발생할 수 있습니다.
- Redis 기반 Shedlock은 빠른 성능과 확장성을 제공하지만, Redis 장애 시 lock 정보가 유실될 가능성이 있습니다.
어떤 방식을 선택할지는 시스템의 특성에 따라 달라질 수 있습니다.
- 소규모 프로젝트에서는 기존 RDBMS를 활용하는 것이 적절할 수 있습니다.
- 대규모 트래픽을 처리하는 시스템이라면 Redis를 활용한 방식이 더욱 효율적일 수 있습니다.
스케줄러를 사용할 때는 단순히 실행 주기를 설정하는 것뿐만 아니라, `Scale-out 환경에서 안정적으로 동작`할 수 있도록 고려하는 것이 중요합니다.
'Spring' 카테고리의 다른 글
[SpringBoot] AWS S3 상품 이미지 등록과 고아객체 처리 구현 예제 (0) | 2025.05.06 |
---|---|
[SpringBoot] Spring Docs + Swagger 적용하여 API 문서 자동화하기 (0) | 2025.02.20 |
[SpringBoot] 트랜잭션 전파 속성 (Transaction propagation) (0) | 2025.02.02 |
[SpringBoot] AWS S3 다중 이미지 파일 업로드 및 삭제 구현하기 (feat. MultipartFile) (2) | 2025.01.17 |
[SpringBoot] @Scheduled를 이용한 스케줄러 구현 (0) | 2025.01.13 |