본문 바로가기
IT/java by 하루 또다시 하루 2026. 5. 26.
반응형

Spring Scheduler 중복 실행 문제는 실무에서 생각보다 자주 겪는 이슈입니다. @Scheduled로 배치 작업을 설정했는데 같은 로직이 두 번 실행되거나, DB에 데이터가 중복으로 쌓여 당황하는 경우가 많습니다.

이 글에서는 단일 서버와 멀티 서버 환경에서 발생하는 Spring Scheduler 중복 실행의 핵심 원인을 분석하고, 실무에서 바로 적용할 수 있는 단계별 해결 방법을 정리합니다.

자주 보이는 증상

실제 서버 운영 중 스케줄러 문제가 발생했을 때 겪게 되는 대표적인 증상은 다음과 같습니다.

  • 같은 시간대에 동일한 스케줄러 시작/종료 로그가 두 번씩 연속으로 찍힘
  • DB에 INSERT 또는 UPDATE 되어야 할 데이터가 중복으로 처리됨
  • 외부 연동 API(알림톡, 결제 등)에 동일한 요청이 두 번 전송됨
  • 이전 스케줄러 작업이 채 끝나기도 전에 다음 스케줄러가 시작되는 로그가 확인됨
  • 로컬 환경에서는 정상이었으나, 운영(또는 개발) 서버에 배포한 직후부터 중복 실행됨

왜 이런 문제가 생길까

1) 작업 시간이 스케줄 주기보다 긴 경우

예를 들어 5분마다 실행되도록 설정한 스케줄러의 실제 비즈니스 로직 처리 시간이 7분이 걸린다면, 이전 작업이 끝나기 전에 다음 실행 시간이 도래하게 됩니다. 특히 fixedRate 옵션을 사용했을 때 이전 작업 완료 여부와 무관하게 정해진 간격으로 스레드를 실행시키려 하면서 중복 실행 현상이 두드러집니다.

2) 서버 인스턴스가 여러 개 떠 있는 경우

Spring Scheduler 중복 실행 원인 중 실무에서 가장 흔하게 놓치는 부분입니다. 로컬에서는 한 번만 돌던 스케줄러가 운영 서버에서 두 번 도는 이유는 인스턴스(Pod, 컨테이너 등)가 여러 개 떠 있기 때문입니다. 서버가 2대라면 동일한 Spring Boot 애플리케이션 안의 @Scheduled 역시 각각 독립적으로 실행되므로 당연히 중복 실행이 발생합니다.

Spring Scheduler 중복 실행 해결 방법

1단계. 가장 먼저 확인할 부분: 실제 중복 실행 로그 분석

  • 무엇을 확인하는지: 스케줄러 실행 시점의 시작/종료 로그, 스레드 이름, 그리고 서버(인스턴스)의 Hostname이나 IP.
  • 왜 먼저 확인해야 하는지: 단순히 로그가 여러 번 찍힌 것인지, 로직 처리가 지연되어 밀린 것인지, 아니면 실제 물리적으로 동시 실행된 것인지를 구별해야 정확한 대처가 가능합니다.
  • 어떻게 판단하는지: 동일한 초(Second)에 로그가 찍혔으나 Hostname이 다르다면 멀티 서버 구조가 원인입니다. Hostname은 같은데 스레드 번호만 다르다면 스케줄러 주기에 따른 지연 중첩일 가능성이 높습니다.

2단계. 다음 점검: fixedRate vs fixedDelay 설정 확인

  • 무엇을 확인하는지: @Scheduled 어노테이션에 설정된 주기 옵션.
  • 왜 먼저 확인해야 하는지: fixedRate는 작업 종료 여부와 상관없이 '정해진 시간 간격'마다 실행을 시도합니다. 반면 fixedDelay는 이전 작업이 '완전히 끝난 시점'을 기준으로 일정 시간이 지난 후 다음 작업을 실행합니다.
  • 어떻게 판단하는지: 작업 소요 시간이 유동적이거나 데이터 중복이 치명적이라면, 무조건 fixedDelay로 변경하여 작업이 물리적으로 겹치지 않게 방어해야 합니다.

3단계. 추가 조치: 단일 서버 환경 플래그 처리

  • 무엇을 확인하는지: 단일 서버(또는 단일 JVM) 내에서의 동시 진입 차단.
  • 왜 먼저 확인해야 하는지: 스케줄러 스레드가 여러 개 열려있을 때, 우발적으로 동일한 작업에 동시 진입하는 것을 애플리케이션 레벨에서 가볍게 막기 위함입니다.
  • 어떻게 판단하는지: 운영 환경이 단일 서버라면 아래와 같이 AtomicBoolean을 활용한 실행 플래그만으로도 훌륭한 방어막이 됩니다.
Java
 
private final AtomicBoolean running = new AtomicBoolean(false);

@Scheduled(fixedDelay = 300000)
public void runScheduler() {
    if (!running.compareAndSet(false, true)) {
        log.info("scheduler already running. skip");
        return;
    }

    try {
        log.info("scheduler start");
        // 핵심 비즈니스 로직
    } finally {
        running.set(false);
        log.info("scheduler end");
    }
}

4단계. 그래도 안 되면 볼 부분: 멀티 서버 환경의 분산 Lock

  • 무엇을 확인하는지: 다중 인스턴스 환경에서 스케줄러 실행 권한 제어 (ShedLock, Redis Lock, DB Lock).
  • 왜 먼저 확인해야 하는지: 서버가 2대 이상이라면 JVM 메모리에 의존하는 AtomicBoolean이나 synchronized로는 다른 서버의 스케줄러 실행을 막을 수 없습니다.
  • 어떻게 판단하는지: 멀티 서버 환경이 확실하다면 코드가 아닌 인프라 레벨의 제어가 필요합니다. 공통으로 바라보는 DB를 활용해 ShedLock 라이브러리를 적용하거나, Redis 기반의 분산 락(Redisson)을 도입하여 특정 서버 하나만 작업을 선점하도록 구조를 변경해야 합니다.

실전에서 자주 놓치는 부분

  • 의미 없는 synchronized 남용: Spring Scheduler 중복 실행을 막겠다고 로직에 synchronized 키워드부터 붙이는 경우가 많습니다. 단일 서버에서는 일부 효과가 있을지 모르나, 요즘 같은 클라우드/컨테이너(Scale-out) 환경에서는 아무런 의미가 없는 조치입니다.
  • 로그 정보 부족: 장애나 버그를 추적할 때 시간만 찍어두면 판단이 매우 어렵습니다. 스케줄러 로그를 남길 때는 반드시 MDC 등을 활용해 Server-IP나 Instance-ID를 함께 출력되도록 구성하세요.
  • 성급한 Lock 도입: 작업 시간이 길어서 겹치는 문제인데, 무작정 Redis 분산 락부터 도입하면 타임아웃 설정 오류로 데드락에 빠지는 등 더 큰 장애를 부를 수 있습니다. 원인이 '시간 지연'인지 '다중 인스턴스'인지 구별하는 것이 항상 먼저입니다.

마무리

Spring Scheduler 중복 실행 문제는 원인을 '작업 지연'과 '서버 다중화' 두 가지로 나누어 접근해야 빠르게 해결할 수 있습니다. 단일 서버라면 fixedDelay 전환이나 AtomicBoolean과 같은 코드 레벨 조치로 충분하지만, 다중 서버 환경이라면 반드시 DB Lock이나 ShedLock 같은 분산 제어 환경을 구성해야 합니다. 문제 발생 시 무작정 코드부터 수정하지 말고, 남겨진 로그의 서버 정보와 타임스탬프를 확인해 정확한 타깃을 잡고 접근하시길 바랍니다.

반응형