3 min read

Part 2. @Scheduled 실전 운영: 단순함의 대가와 멀티 인스턴스 함정

@Scheduled를 운영 환경에 적용할 때 발생하는 중복 실행, 분산락, 장애 복구 트레이드오프를 아키텍처 기준으로 정리한다.

Series: Spring Boot 배치 전략 완전 정복

12편 구성. 현재 2편을 보고 있습니다.

썸네일 - 스케줄과 시간
썸네일 - 스케줄과 시간

출처: Pexels - Modern Workspace with Digital Clock

버전 기준

  • Java 21
  • Spring Boot 3.3.x
  • Spring Batch 5.2.x
  • Quartz 2.3.x
  • PostgreSQL 15
  • OpenSearch 2.x

1) 문제 제기

@Scheduled는 구현 비용이 가장 낮다. Spring Boot 애플리케이션 내부에서 즉시 동작하고, 러닝커브도 거의 없다. 그래서 초기에는 대부분 @Scheduled로 시작한다. 하지만 서비스 인스턴스가 1대에서 2대로 늘어나는 순간, 같은 cron이 동시에 두 번 실행되는 문제가 거의 반드시 발생한다.

운영에서 중요한 질문은 단순하다.

  • 이 작업은 중복 실행돼도 안전한가?
  • 실행 시간이 스케줄 간격보다 길어도 되는가?
  • 인스턴스 장애 후 재기동 시 이전 작업과 겹쳐도 되는가?

@Scheduled 자체는 이 질문에 답해주지 않는다. 답은 락, 멱등성, 실행 이력 아키텍처에서 나온다.

2) 핵심 개념 정리

@Scheduled의 장점

  • 코드 가독성이 높고 도입 속도가 빠르다.
  • 애플리케이션 컨텍스트와 동일한 DI/트랜잭션 모델 사용 가능.
  • 작은 팀에서 "작업 1~5개" 수준 운영에 효율적이다.

운영 한계

  • 클러스터 환경에서 리더 개념이 없다.
  • Misfire 정책(예: 정시 미실행 후 보정)을 기본 제공하지 않는다.
  • 실행 이력, 재시도, 병렬 제어를 직접 구현해야 한다.

락 전략 비교(요약)

전략장점단점권장 상황
DB 락 (FOR UPDATE/advisory lock)추가 인프라 불필요, 강한 일관성DB 부하 증가이미 RDBMS 중심 운영
Redis 락 (SET NX PX)빠른 락 획득, 분산 환경 적합TTL/네트워크 분할 고려 필요고빈도 스케줄, 다중 앱
무락 + 멱등성구현 단순, 장애 내성 높음중복 실행 자체는 발생부작용 없는 집계/동기화

트랜잭션 격리 수준은 보통 READ COMMITTED로 시작하되, "동일 조건 재조회"가 있는 경우 REPEATABLE READ 또는 락 힌트를 함께 사용한다.

실행 흐름 다이어그램

Mermaid diagram rendering...

본문 이미지 - 운영 콘솔
본문 이미지 - 운영 콘솔

출처: Pexels - A Man Looking at Multiple Monitors

3) 코드 예시

예시 A: @Scheduled + 분산락 래퍼

@Component
@RequiredArgsConstructor
public class SettlementScheduler {

    private final DistributedLockService lockService;
    private final SettlementService settlementService;

    @Scheduled(cron = "0 */5 * * * *")
    public void run() {
        String lockKey = "batch:settlement:5m";
        String lockToken = UUID.randomUUID().toString();

        if (!lockService.tryLock(lockKey, lockToken, Duration.ofMinutes(4))) {
            return;
        }

        try {
            settlementService.processPending(LocalDate.now());
        } finally {
            lockService.unlock(lockKey, lockToken);
        }
    }
}

예시 B: Redis 락 구현 핵심

@Service
@RequiredArgsConstructor
public class RedisDistributedLockService implements DistributedLockService {

    private final StringRedisTemplate redisTemplate;

    @Override
    public boolean tryLock(String key, String token, Duration ttl) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, token, ttl);
        return Boolean.TRUE.equals(result);
    }

    @Override
    public void unlock(String key, String token) {
        String current = redisTemplate.opsForValue().get(key);
        if (token.equals(current)) {
            redisTemplate.delete(key);
        }
    }
}

예시 C: DB advisory lock 사용 SQL(PostgreSQL)

-- job 이름을 hash로 변환해 advisory lock 사용
SELECT pg_try_advisory_lock(hashtext('batch:settlement:5m')) AS acquired;

-- 처리 완료 후 해제
SELECT pg_advisory_unlock(hashtext('batch:settlement:5m'));

예시 D: 키셋 기반 실패 실행 이력 조회

SELECT id, job_name, started_at, ended_at, error_code
FROM batch_job_execution
WHERE job_name = 'settlement'
  AND status = 'FAILED'
  AND id > :last_id
ORDER BY id ASC
LIMIT 100;

4) 실제 장애/운영 시나리오

상황: 5분 주기 정산 배치가 4분 50초에 끝나는 환경에서, GC pause(약 20초)로 락 갱신 heartbeat가 끊겼다. Redis TTL이 만료되면서 다른 인스턴스가 같은 작업을 재실행해 중복 정산이 발생했다.

원인:

  • 락 TTL을 "평균 실행 시간" 기준으로만 설정했다.
  • 장시간 Stop-The-World(G1 mixed GC) 구간을 설계에서 고려하지 않았다.
  • 비즈니스 레벨 멱등성 제약(UNIQUE(order_id, settlement_date))이 없었다.

대응:

  1. TTL을 P99 실행시간 + GC 여유시간으로 재산정(예: 4m50s -> 8m).
  2. 하트비트 실패 시 즉시 작업 중단하고 재진입 방지 플래그 저장.
  3. 도메인 테이블에 멱등성 유니크 인덱스 추가.

5) 설계 체크리스트

  • 멀티 인스턴스에서 중복 실행 방지 전략(락 또는 멱등성)을 명시했는가?
  • 락 TTL을 평균이 아닌 P99 + GC 여유 기준으로 계산했는가?
  • 장애 재기동 시 같은 윈도우를 재실행해도 안전한가?
  • 스케줄 실행 이력(성공/실패/소요시간)을 저장하는가?
  • DB 격리 수준과 락 힌트(SKIP LOCKED 등)를 의도적으로 선택했는가?
  • JVM 힙 사용량과 GC pause가 스케줄 간격에 미치는 영향을 측정했는가?

6) 요약

@Scheduled는 "작고 단순한 배치"에 매우 강력하지만, 클러스터 안정성은 별도 설계 없이는 보장되지 않는다. 락만으로는 충분하지 않고, 멱등성/실행 이력/장애 복구가 함께 있어야 운영 가능한 배치가 된다.

7) 다음 편 예고

다음 편에서는 Quartz를 다룬다. 클러스터 모드, JobStore(RAM vs JDBC), Misfire 정책, 수백~수천 스케줄 운영 시 어떤 기준으로 Quartz를 선택해야 하는지 실무 관점에서 정리한다.

참고 링크

시리즈 네비게이션

댓글