Notice
Recent Posts
Recent Comments
Link
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

DevYGwan

분산 환경에서 안전한 스케줄링: Spring ShedLock 적용 본문

카테고리 없음

분산 환경에서 안전한 스케줄링: Spring ShedLock 적용

YGwan 2025. 9. 2. 00:20

 저희는 공장의 여러 기기에서 실시간으로 사용자 데이터를 가져와 관리하는 B2B 서비스를 개발하고 있습니다. 그래서 특별히 장애가 발생하지 않는 한 특정 사용자(공장)의 기기 데이터가 한번에 다 올라오지 않는 경우는 거의 없습니다. 물론, 그 공장이 전체 소등을 한다던가 하지 않는 이상은 말이죠... 그러다보니 특별한 일 없이, 특정 공장의 모든 기기에서 데이터가 만약 올라오지 않는다면 이는 큰 장애로 이어졌을 가능성이 높아 빠르게 처리해야되죠. (일단, 고객보다 먼저 알아서 빠르게 처리할 수 있어야 합니다.)

 이를 처리하기 위해 저희는 15분 주기마다 사용자의 기기 데이터들을 조회해서 올라온 데이터가 하나도 없는지 조회하여 없다면 알림을 보내는 로직을 구현해 사용하고 있습니다. 이를 구현하기 위해서 Spring Scheduler를 통해 15분을 간격으로 특정 로직을 돌리게끔 로직을 구현했습니다.

 처음에는 이게 문제가 되지 않았습니다. 하지만, 서버를 1개가 아닌 여러개로 Scale Out 해야 할 상황이 오니... 문제가 되었습니다. 저는 15분마다 한번씩 실행하게 하고 싶은데, 서버의 갯수만큼 실행하는 문제가 발생했습니다. 따라서 이를 처리하기 위해 ShedLock을 도입하게 되었습니다.

 


ShedLock이란?

  • shedLock은 분산 환경에서 스케줄러(Job/Scheduler) 작업이 여러 서버에서 동시에 실행되는 것을 막기 위한 라이브러리입니다.
    • 여기서 주요한 키워드는 "분산 환경" 입니다.
  • ShedLock의 기본 원리는 DB에 락 정보를 남기고 처리 시 이 락 정보를 확인해 선점한 인스턴스만 실행하도록 보장하는 것입니다.

 

특징

  • 분산 락 기반 스케줄러 제어 : 여러 서버 인스턴스가 동시에 예약되어 있는 Job을 실행하지 않도록 보장 (1번만 실행되도록 보장)
  • 스토리지 기반 처리 : Lock 정보를 공용 스토리지(DB)에 저장 및 조회
  • Spring 환경에 친화적 : @SchedulerLock 어노테이션을 통해 기존 @Scheduled 메서드에 간단히 적용 가능
  • 시간 기반 락 해제 지원 : lockAtMostFor, lockAtLeastFor 같은 파라미터로 Lock 유지 시간 제어 가능

 

동작 순서

  1. 스케줄러가 실행될 때, DB의 shedlock 테이블에서 해당 Job 이름(name)으로 락을 획득하려고 시도합니다.
  2. 락을 획득하면 획득과 동시에 row을 Insert & update 해 현재 실행 중임을 기록합니다.
  3. 다른 인스턴스가 접근 시 Lock과 관련된 row을 확인하고 현재 실행중임을 확인하면, Job을 실행하지 않습니다.
  4. 락을 선점한 인스턴스가 작업을 진행하고 작업이 끝나면 Lock을 해제합니다.

 

 

옵션 의미 사용 목적 방식
lockAtMostFor 락의 최대 유지 시간
(자동 해제 시점)
서버 장애로 락이 풀리지 않는 상황 방지 Job 최대 실행 시간 + 여유로 설정
lockAtLeastFor 락의 최소 유지 시간 Job이 너무 빨리 끝나 중복 실행되는 것 방지 크론 주기 또는 실행 최소 주기로 설정

 

그렇다면, 지금부터 원래 코드를 간단하게 소개하고 이를 scale out 했을 때 어떤 문제가 발생하는지 설명드리고 shedLock을 통해 어떻게 해결했는지 설명드리도록 하겠습니다.

 


원래 코드

@Scheduled(
	initialDelay = 15 * 60 * 1000,
	fixedRate = 15 * 60 * 1000,
)
fun scheduledJobEvery15Min() {
	// 로직...			
}
  • 애플리케이션 시작 후 15분 후부터 시작해 매번 15분마다 아래의 로직을 실행하는 JOB 생성 및 처리

 

< 이 코드의 문제점 >
서버를 scale out 할 경우 여러 서버가 각각 15분마다 아래 로직을 실행한다.

 

위의 그림과 같이 예를 들어 서버가 3개라고 가정했을 때 위의 코드는 서버마다 각각 15분마다 한번씩 특정 Job을 실행합니다. 따라서 서버의 개수만큼 동일한 작업을 실행합니다.

 

따라서 이 문제를 해결하기 위해, shedLock을 적용해 Lock을 관리했습니다.

 

 

ShedLock 적용 코드

  • shedLock 설정을 위한 부분도 존재하지만 여기 설명에서는 사용 관점의 설명만 드리도록 하겠습니다.
    • 하나 이슈 사항이 있었다면, 기본적으로 DB 스키마가 고정되어 있었습니다. (DB 명, 컬럼 명 등 )-> 물론 설정을 통해 변경 가능
  • Lock 관리는 기존 PostgresDB의 shedlock table을 사용했습니다.

 

@SchedulerLock(name = "Job-Name", lockAtMostFor = "PT1M", lockAtLeastFor = "PT10S")
@Scheduled(
	initialDelay = 15 * 60 * 1000,
	fixedRate = 15 * 60 * 1000,
)
fun scheduledJobEvery15Min() {
	// 로직...			
}
  • 애플리케이션 시작 후 15분 후부터 시작해 매번 15분마다 아래의 로직을 실행하는 JOB 생성 및 처리
  • 이 때, Shedlock table에 Job-Name 이라는 이름으로 락이 생성돼 이를 통해 하나의 서버만 실행되도록 보장

 

하지만, 이 경우 이슈가 하나 있었습니다.
지금 저의 @scheduled 관련된 코드를 보시면, 서버 실행 후 15분마다 저 Job을 실행하게 됩니다.

그런데, 서버 시작은 서버마다 다를 수도 있기 때문에 만약 서버 간 실행 시간 차이가 10분 이상이 난다면, 둘다 실행되는 문제가 있었습니다. 예시를 통해 쉽게 설명드리도록 하겠습니다.

 

문제 상황

  • 서버 A가 11:00분에 시작, 서버 B가 11:08분에 시작, 서버 C가 11:12분에 시작했다고 가정

  1. 서버 A 기준 11:15분에 락 획득 시도 -> 아무도 락을 안걸고 있기 때문에 락 생성 후 JOB 처리 (위의 사진처럼 Lock 생성)
  2. 서버 B가 11:23분에 락 획득 시도 -> 기존 락의 locked_until 보다 더 빠르기 때문에 Lock 획득 실패
  3. 서버 C가 11:27분에 락 획득 시도 -> 기존 락의 Locked_until 보다 나중이기 때문에 Lock 획득 가능 -> JOB 처리

 

 이런 문제가 발생할 수 있습니다. 따라서 제가 도입한건, cron을 통해 절대 시간 기준으로 실행 주기를 맞추는 것이였습니다.

 

Cron 적용 코드

@SchedulerLock(name = "Job-Name", lockAtMostFor = "PT1M", lockAtLeastFor = "PT10S")
@Scheduled(cron = "0 0/15 * * * *")
fun scheduledJobEvery15Min() {
	// 로직...			
}
  • cron은 스케줄을 표현하는 규칙(표현식)입니다.
  • Spring 기준으로는 6자리(초 ~ 요일) 혹은 7자리(초 ~ 연도)까지 쓸 수 있습니다.
  • 형식
    •       일(날짜)     요일   [연도(옵션)]
  • 기호
    • * → 모든 값
    • / → 주기 (예: */5 = 5 단위로)
    • , → 여러 값 지정 (예: 1,15 = 1일과 15일)
    • - → 범위 (예: 9-17 = 9시~17시)

 

저같은 경우에는0 0/15 * * * * 이렇게 설정했고, 매 15분 마다 실행한다는 의미입니다.

 

따라서 이 설정을 사용하면 서버의 시작 시점과 관계없이 서버 시간 기준으로 매 15분마다 Job이 실행되어 실행 시점이 일정하게 맞춰집니다. 즉, 서버 시작 시간에 따라 실행 주기가 어긋나는 문제가 발생하지 않습니다.
이처럼 cron job과 ShedLock을 함께 사용하면 여러 서버 인스턴스가 동시에 같은 Job을 실행하지 않도록 안전하게 제어할 수 있습니다.

 


정리

 이렇게해서 기존의 단일 인스턴스에서 발생하지 않은 문제를 찾고, 여러 테스트를 통해 문제를 해결했습니다. 확실히 단일 인스턴스 말고 scale-out을 통한 여러 인스턴스로 확장할 때는 다양한 고려사항이 필요한 것 같습니다. 단순히 성능을 위해 인스턴스를 확장하고 끝나면... 거기서 오는 추가적인 trade off 들로 인해 여러 문제가 발생할 것 같다는 생각이 들었습니다...