💡 배경
프로젝트를 진행하면서 스케쥴러를 통해 정해진 시간에 주기적으로
REST API 데이터와 DB 데이터의 정합성을 맞춰야 하는 작업이 필요했다.
이 때 여러 대의 서버 인스턴스에서 동일한 서버 애플리케이션이 구동되고 있다면, (ex.톰캣 이중화)
스케쥴러가 여러 서버에서 동시에 실행되었을 때 중복작업 이슈가 발생할 수 있다는 것을 알았다.
그래서 스케쥴러 작업이 하나의 서버에서만 수행되도록 해주었다.
✅ 방법
1. Spring Batch + Quartz + JDBC
2. Redis 또는 DB 기반 분산락
3. 외부 잡 매니저 (e.g., Airflow, Jenkins, AWS EventBridge)
이 중 내가 선택한 방식은 2번의 DB 기반 분산락 방식이었다.
| DB 분산락이란?
락 전용 테이블을 사용해서 특정 작업의 실행 권한을 DB에 먼저 선점합니다.
선점에 성공한 인스턴스만 스케줄을 실행합니다.
락은 트랜잭션 or 시간 기반으로 해제됩니다.
| 프로젝트 적용
1. 락 테이블 생성 SQL
CREATE TABLE distributed_lock (
lock_key VARCHAR(100) PRIMARY KEY, /*락 식별자*/
locked_until TIMESTAMP, /*락 만료 시간*/
locked_by VARCHAR(100) /*락 소유자 (서버 IP 등)*/
);
2. Spring + Mybatis
<!-- Mapper XML -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cohttp://m.example.mapper.DistributedLockMapper">
<update id="tryLock">
UPDATE distributed_lock
SET locked_until = #{lockedUntil}, locked_by = #{lockedBy}
WHERE lock_key = #{lockKey}
AND (locked_until IS NULL OR locked_until < #{now})
</update>
<update id="releaseLock">
UPDATE distributed_lock
SET locked_until = NULL
WHERE lock_key = #{lockKey}
AND locked_by = #{lockedBy}
</update>
</mapper>
3. Service
@Service
public class LockService {
@Autowired
private DistributedLockMapper lockMapper;
public boolean tryLock(String lockKey, Duration holdDuration) {
String lockedBy = InetAddress.getLocalHost().getHostName();
LocalDateTime now = LocalDateTime.now();
LocalDateTime until = now.plus(holdDuration);
int updated = lockMapper.tryLock(lockKey, until, lockedBy, now);
return updated > 0;
}
public void releaseLock(String lockKey) {
String lockedBy = InetAddress.getLocalHost().getHostName();
lockMapper.releaseLock(lockKey, lockedBy);
}
}
4. 스케쥴러
@Scheduled(cron = "0 0 * * * *")
public void myScheduledTask() {
if (lockService.tryLock("my-task", Duration.ofMinutes(10))) {
try {
// 실제 작업 실행
runMyTask();
} finally {
lockService.releaseLock("my-task");
}
} else {
// 다른 인스턴스가 이미 수행 중
}
}
| 테스트
runMyTask()에서 특정 테이블에 데이터 값을 insert 하는 작업을 구현한 다음 테스트를 진행해보았다.
톰캣 2개를 띄워두고 테스트 해보았는데..각각의 톰캣에서 모두 insert 작업이 된 것이 아닌가..?
그래서 원익을 파악해보니 아래와 같았다.
| 이슈 및 해결
1. 왜 이런 일이 발생하나?
대부분의 DB 기반 분산 락 구현은 다음과 같은 방식으로 동작한다.
1) 스케줄러 실행 시 DB에 락 획득 시도 (SELECT ... FOR UPDATE 또는 INSERT)
2) 성공하면 작업 수행
3) 작업이 끝나면 락 해제 (UPDATE, DELETE 등)
🔴 작업이 너무 빠르게 끝나면
두 인스턴스가 거의 같은 시간에 락을 요청
=> 락 획득과 해제가 너무 빨라 둘 다 통과하게 되는 "레이스 컨디션" 발생
2. 해결
runMyTask() 실행 전에 강제로 sleep 을 넣어 인위적으로 시간을 확보하여 락 유지 시간을 조절했다.
찜찜..

| 개선
ShedLock (Spring Scheduler + JDBC/Redis 기반 락을 쉽게 적용 가능) 방식을 통해
레이스 컨디션을 방지하도록 개선할 예정이다.
▼ ShedLock 해결 보러가기 ▼
[ spring ] ShedLock 스케줄러 중복 실행 이슈 해결
✅ 개발환경Java 8 Spring Boot 2.7 ShedLock 4.44.0 MySQL (JDBC Template 기반) 초반에 java8인데 ShedLock 5.10.1 버전 사용하려해서 계속 에러 이슈 있었다.. 후 | Gradle 의존성 (build.gradle)dependencies { implementation 'net.jav
im-codding.tistory.com
'Web' 카테고리의 다른 글
| [ spring ] ShedLock 스케줄러 중복 실행 이슈 해결 (1) | 2025.06.10 |
|---|---|
| [ spring ] 최초 폐쇄망(내부망) 순수 gradle 환경 세팅 (0) | 2025.04.02 |
| [ spring ] egovframework(전자정부프레임워크) log4j 이슈해결 maven & gradle (0) | 2025.04.01 |
| [ spring ] egovframework(전자정부프레임워크) 3.9.0 적용 maven & gradle (0) | 2025.04.01 |
| [ Gradle ] gradle exclude dependency (gradle 의존성 제외) (0) | 2025.03.31 |