본문 바로가기
dev/spring

Spring Boot ShedLock in Action (with MongoDB)

by igooo 2025. 3. 18.
728x90

https://github.com/igooo/tutorials/tree/main/spring-boot-3.4/shedlock-mongo

개요

Spring Boot를 사용하여 프로젝트를 진행할 때 간단한 예약 작업은 @Scheduled 어노테이션을 사용하여 간단하게 처리할 수 있다. @Scheduled 로직을 실행하는 서버가 1개인 경우에는 아무런 고민 없이 사용가능하지만, 고가용성을 위하여 2개 이상 서버를 실행하는 경우에는 @Scheduled의 로직이 중복으로 실행되어 개발자가 의도하지 않거나 예측하지 못한 결과가 발생할 수 있다.

ShedLock을(https://github.com/lukas-krecan/ShedLock) 사용하면 @Schedule을 사용하여 작성된 로직이 여러 서버에서 실행되더라도 동시에 최대 한 번만 실행되도록 제어할 수 있다. ShedLock의 동작 방식은 @Scheduled 작업이 하나의 서버에서 실행 중이면 다른 서버(또는 다른 스레드)에서 동일한 작업이 실행되는 것을 방지하도록 Lock(잠금)을 획득하여 실행되고, 다른 서버는 Lock을 획득하지 못했으므로 실행을 대기하지 않고 작업을 건너뛰게 된다.

 

ShedLock

Components

  • core - Lock을 위한 메커니즘
  • integration - Spring AOP, Micronaut AOP 또는 수동 코드를 사용한 애플리케이션 통합
  • Lock provider - SQL, Mongo, Redis와 같은 외부 저장소를 사용한 Lock을 제공
ShedLock은 분산 스케줄러가 아니다, 분산 스케줄 기능이 필요하다면 다른 라이브러리를 검토해야 한다.

 

 

Getting started 

이번 포스팅에서는 MongoDB를 사용하여 ShedLock을 사용하는 법을 알아본다.

 

프로젝트 설정

spring-web과  shedlock관련 의존성을 추가한다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'net.javacrumbs.shedlock:shedlock-spring:6.3.0'
    implementation 'net.javacrumbs.shedlock:shedlock-provider-mongo:6.3.0'
    ......

 

application.properties에 MongoDB URL을 설정한다.

spring.application.name=shedlock-mongo
spring.data.mongodb.uri={MONGODB_URL}

 

@Scheduled 로직 추가

@Service
class TaskSchedulerService {

    @Scheduled(fixedDelay = 2000)
    fun executeTask() {
        println("execute task at: ${LocalDateTime.now()}")
    }

}

@Configuration
@EnableScheduling
class SchedulerConfig {
}

 

애플리케이션 실행

execute task at: 2025-03-17T23-33-50.998010400
execute task at: 2025-03-17T23-33-53.005043400
execute task at: 2025-03-17T23-33-55.018567600

 

ShedLock 통합

위 예제를 통하여 @Scheduled 어노테이션을 사용하여 task를 실행하는 방법에 대하여 알아보았다. 

이제 ShedLock을 사용하는 방법에 대하여 알아본다.

 

ShedLock를 사용하기 위해서는 @EnableSchedulerLock 어노테이션을 추가해 준다.

MongoDB를 외부 저장소로 사용하기 때문에 MongoLockProvider 객체를 Bean으로 등록해 준다.

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "5m", defaultLockAtLeastFor = "30s")
class SchedulerConfig {
    @Bean
    fun lockProvider(mongo: MongoClient): LockProvider {
        return MongoLockProvider(mongo.getDatabase("test_db"))
    }
}

 

@SchedulerLock 어노테이션을 사용하여 ShedLock을 사용한다.

  • name 속성을 지정하여 동일한 이름을 지정한 작업은 동시에 하나만 실행한다.
  • lockAtMostFor, lockAtLeastFor의 속성을 사용하여 Lcok이 유지되는 최소, 최대 시간을 설정할 수 있다.
@Service
class TaskSchedulerService {

    @Scheduled(fixedDelay = 2000)
    @SchedulerLock(name="executeTask", lockAtMostFor = "1m", lockAtLeastFor = "1s")
    fun executeTask() {
        println("execute task at: ${LocalDateTime.now()}")
    }

}

 

@SchedulerLock의 동작방식은 아래와 같다.

  1. @SchedulerLock 어노테이션이 추가된 메서드만 잠금이 적용되며, 라이브러리는 다른 예약된 작업을 무시한다. 기본적으로 잠금은 메서드가 스케줄러를 통해서 호출될 때뿐만 아니라 직접 호출되는 경우에도 적용된다.
  2. 동일한 이름을 가진 작업은 동시에 하나만 실행된다. 
  3. 동일한 이름의 작업이 실행되는 동안, 다른 작업은 차단되지 않고 단순히 건너뛰어진다.
  4. 작업이 완료되면 잠금은 자동으로 해제된다. (lockAtLeastFor가 지정되지 않는 경우)
  5. 작업이 완료되기 전에 JVM이 강제로 종료되는 경우 lockAtMostFor 속성이 작동된다. 잠금은 항상 lockAtMostFor 이후에 해제된다. lockAtMostFor를 일반적으로 실행 시간보다 훨씬 길게 설정해야 한다. 작업이 lockAtMostFor보다 오래 걸린다면 개발자가 생각한 대로 작업이 동작하지 않을 수 있다.(둘 이상의 프로세스가 잠금을 유지할 수 있다.)
  6. lockAtMostFor를 지정하지 않으면 @EnableSchedulerLock의 기본값이 사용된다.
  7. lockAtLeastFor 속성을 설정하여 잠금을 유지하는 최소 시간을 지정할 수 있다. 이 속성의 주요 목적은 매우 짧은 작업과 서버 간의 시간 차이로 인해 여러 서버에서 실행되는 것을 방지하는 것이다.
  8. 모든 주석은 Spring Exepression Language(SpEL)를 지원한다.

 

Demo

위 예제에서는 @SchedulerLock 어노테이션에 name 속성에 executeTask를 지정하였다 두 개의 서버를 실행하여 Task가 어떻게 실행되는지 알아보자.

 

첫 번째 서버 실행

$ gradlew bootRun
execute task at: 2025-03-17T23:44:12.932642900
execute task at: 2025-03-17T23:44:14.944701100
execute task at: 2025-03-17T23:44:16.955702500
execute task at: 2025-03-17T23:44:18.994708300

 

MongoDB에 컬렉션을 보면 executeTask에 실행 정보가 저장되어 있다.

{
    "_id" : "executeTask",
    "lockUntil" : ISODate("2025-03-17T23:44:52.198+09:00"),
    "lockedAt" : ISODate("2025-03-17T23:44:51.198+09:00"),
    "lockedBy" : "igooo-D01"
}

 

두 번째 서버 실행

8080 port로 서버가 실행 중이라 8081 port로 지정하여 서버를 실행한다.

$ gradlew bootRun --args="--server.port=8081"
execute task at: 2025-03-17T23:45:27.583415800
execute task at: 2025-03-17T23:45:29.629350500
execute task at: 2025-03-17T23:45:31.649354900
execute task at: 2025-03-17T23:45:33.667726700

 

8080으로 실행한 서버에서는 task가 실행되지 않는다.

 

요약

간단한 설정만으로 @Scheduled의 로직을 동기화 할 수 있어서 많은 프로젝트에서 쉽게 적용가능하다

 

전체 소스코드는 GitHub에서 확인할 수 있습니다.

728x90