스케줄러(Scheduler)를 사용해 정해진 시간에 반복 작업을 실행하고자 할 때, 다중 인스턴스 환경에서는 같은 작업이 여러 인스턴스에서 동시에 실행되는 상황이 발생하게 됩니다.

이번 포스팅에서는 ShedLock의 개념과 라이브러리를 활용하여 여러 인스턴스에서 Scheduler 사용 시 발생하는 문제를 해결하는 방법에 대해 알아보도록 하겠습니다.

 

문제 상황

Scheduler 문제 이미지

Scheduler를 적용하면 지정한 일정 시간마다 실행하게 됩니다.

그런데 만약 여러 인스턴스 서버(Scale-out)를 띄울 경우 각 서버마다 존재하는 Scheduler가 동작하게 될 것입니다.

 

@Scheduled(zone = "Asia/Seoul", cron = "*/1 * * * * *") // 1초 간격으로 실행
public void run() {
log.info("Scheduler 실행");
}

1초 간격으로 실행되는 Scheduler를 작성하였습니다. 두 대의 서버를 띄워 실행하면

좌: 서버1, 우: 서버2

두 서버 모두 Scheduler가 실행되는 모습을 볼 수 있습니다. 이메일 발송 스케줄러로 본다면 똑같은 메일이 2번씩 발송되는 상황이 발생하게 됩니다. 이는 Scale-out이 커질수록 점점 더 큰 문제로 다가올 수 있습니다.

 

이 때 Shedlock을 사용하여 문제를 해결할 수 있습니다.

 

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을 해제합니다.

 

좌: 서버1, 우: 서버2

초기 문제 상황과 달리 이제는 하나의 서버에서만 스케줄러가 작동되는 모습을 볼 수 있습니다.

반드시 하나의 서버에서만 실행되는 것은 아니며 lock 유무 체크가 먼저 일어난 서버에서 실행됩니다.

실행 전 lock 유무 체크 → lock이 없다면 생성

Shedlock 이미지

설정한 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 실행");
}
}

이전 환경과 동일하게 설정하였습니다.

 

좌: 서버1, 우: 서버2

마찬가지로 하나의 서버에서만 스케줄러가 동작하는 모습을 볼 수 있습니다.

 

Shedlock 동작과정

  • 스케줄러가 실행될 때, 먼저 Redis에 lock이 존재하는지 확인합니다.
  • lock이 없으면 현재 실행중인 서버가 lock을 획득하고 스케줄러를 실행합니다.
  • lock이 이미 존재하면 다른 서버에서는 스케줄러 실행이 되지 않습니다.
  • 작업이 완료되면 lock이 해제되며, lockAtMostFor(최대 유지 시간)이 지나면 강제로 해제됩니다.

 

redis cli 이미지

이미지와 같이 스케줄러에서 설정한 주기마다 lock 획득을 시도하는 모습을 볼 수 있습니다.

 

RDBMS vs Redis

 

REBMS 동작 방식

  1. Scheduler 실행 시 DB에 SELECT 쿼리로 lock 존재 여부 확인
  2. Lock이 없으면 INSERT 또는 UPDATE 쿼리 실행 (잠금 등록)
  3. 작업 수행 후 lock 해제 (또는 lockAtMostFor 시간이 지나면 자동 해제)

 

Redis 동작 방식

  1. Scheduler 실행 시 SETNX (Set if Not Exists) 명령어로 lock 설정 시도
  2. Lock이 존재하면 다른 서버는 실행 불가
  3. 작업이 끝나면 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 환경에서 안정적으로 동작할 수 있도록 고려하는 것이 중요합니다.