본문으로 건너뛰기
개발 머꼬
개발 노트Spring
hohyeon.dev25

@Scheduled 작업이 겹쳐 실행돼서 데이터가 중복 처리된 이유

  • #Engineering Note
  • #Scheduling
  • #Spring

문제 발생

5분마다 미처리 주문을 배치로 처리하는 @Scheduled 작업을 등록했는데, 어느 날 배치 처리 로직이 평소보다 오래 걸리면서 같은 주문이 중복으로 처리되는 문제가 발생했습니다.

@Scheduled(fixedRate = 300000) // 5분마다
public void processOrders() {
    // 처리에 6분이 걸린다면?
}

원인 분석

@ScheduledfixedRate이전 실행이 끝났는지와 무관하게, 이전 실행이 시작된 시점 기준으로 지정한 간격마다 다음 실행을 시작합니다. 만약 한 번의 처리가 지정한 간격(5분)보다 오래 걸리면, 이전 실행이 끝나기도 전에 다음 실행이 시작되어 두 실행이 동시에 같은 데이터를 처리하게 됩니다 — 이번 사례처럼 중복 처리로 이어질 수 있습니다.

참고로 fixedDelay는 다른 동작입니다 — 이전 실행이 끝난 시점부터 지정한 간격을 기다렸다가 다음 실행을 시작하므로, 이 문제 자체가 발생하지 않습니다. 하지만 fixedRate가 필요한 상황(예: 처리 시간과 무관하게 일정한 주기로 시작하고 싶은 경우)도 분명히 있어서, fixedDelay로 무조건 바꾸는 것이 항상 정답은 아닙니다.

또한 애플리케이션이 여러 인스턴스로 배포된 환경(수평 확장)이라면, 인스턴스마다 독립적으로 스케줄러가 돌기 때문에 같은 배치 작업이 여러 인스턴스에서 동시에 실행되는 문제도 별도로 존재합니다 — 이건 fixedRate/fixedDelay 선택과 무관하게 항상 고려해야 하는 문제입니다.

해결 방안

  1. 작업 시간이 간격보다 길어질 수 있다면 fixedDelay를 우선 검토합니다 — 같은 인스턴스 안에서의 중복 실행은 이것으로 해결됩니다.
  2. 여러 인스턴스 환경에서는 분산 락(Redis, DB 락 등)으로 "지금 이 배치 작업을 실행 중인 인스턴스가 있는지"를 확인하고, 있다면 건너뛰도록 만듭니다. ShedLock 같은 전용 라이브러리가 이 문제를 위해 존재합니다.
@Scheduled(fixedDelay = 300000)
@SchedulerLock(name = "processOrders", lockAtMostFor = "10m")
public void processOrders() { ... }
  1. 배치 로직 자체도 **멱등성(idempotency)**을 갖도록 설계하면, 설령 중복 실행이 되더라도 실제 부작용(중복 처리)은 발생하지 않습니다 — 예를 들어 "처리 완료" 상태를 먼저 원자적으로 표시하고 그 상태인 것만 골라 처리하는 방식입니다. 스케줄링 자체의 겹침을 막는 것과 별개로, 이 방어선을 함께 두는 것이 더 안전합니다.
  2. 배치 작업의 실제 소요 시간을 모니터링해서, 지정한 주기에 비해 얼마나 여유가 있는지 계속 확인하는 것도 이런 문제를 미리 발견하는 데 도움이 됩니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.