3 min read

Part 9. 분산 환경 배치: Leader Election, Kubernetes CronJob, 락 전략 비교

멀티 인스턴스에서 배치 중복 실행을 방지하고 운영 복잡도를 통제하기 위한 실행 주체/락 전략 선택 기준을 제시한다.

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

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

썸네일 - 분산된 컨테이너 환경
썸네일 - 분산된 컨테이너 환경

출처: Pexels - Row of blue shipping containers

버전 기준

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

1) 문제 제기

분산 환경에서 가장 흔한 배치 장애는 "중복 실행"이다. 애플리케이션이 10개 파드로 확장된 순간, 배치 트리거가 10배가 될 수 있다. 이 문제는 기술 선택의 문제가 아니라 "실행 주체"를 명확히 정의하지 않았기 때문에 발생한다.

핵심 질문은 두 가지다.

  • 배치를 누가 트리거할 것인가? (Kubernetes vs 애플리케이션 내부)
  • 동시에 하나만 실행된다는 보장을 어디서 만들 것인가? (락/리더 선출)

2) 핵심 개념 정리

Kubernetes CronJob vs 앱 내부 스케줄러

기준Kubernetes CronJob앱 내부 스케줄러 (@Scheduled/Quartz)
실행 주체플랫폼(K8s)애플리케이션
배포/재시작 독립성높음앱 라이프사이클 영향 큼
코드 근접성낮음(외부 Job Pod)높음(같은 코드베이스)
운영 난이도K8s 의존앱 코드/락 설계 의존
권장 상황단순 독립 배치, 인프라 표준화도메인 로직과 밀접한 배치

락/리더 선출 전략 비교

전략장점단점권장 상황
DB 락강한 일관성, 추가 인프라 없음DB 경합 증가트리거 빈도 낮고 DB 중심
Redis 락빠르고 확장성 좋음TTL/분할 브레인 고려고빈도 작업
Zookeeper/etcd 선출리더 선출에 강함운영 복잡도 높음대규모 플랫폼 팀
K8s Lease쿠버네티스 친화적K8s 종속K8s 표준 환경

분산 실행 제어 다이어그램

Mermaid diagram rendering...

본문 이미지 - 포트 운영
본문 이미지 - 포트 운영

출처: Pexels - Container ships at cargo port

3) 코드 예시

예시 A: Kubernetes CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: settlement-job
spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: settlement
              image: my-registry/batch:1.0.0
              args: ["--job=settlement"]

예시 B: DB 기반 리더 락

CREATE TABLE batch_leader_lock (
    lock_name VARCHAR(100) PRIMARY KEY,
    holder_id VARCHAR(100) NOT NULL,
    expires_at TIMESTAMP NOT NULL
);
-- 리더 선점 시도
INSERT INTO batch_leader_lock (lock_name, holder_id, expires_at)
VALUES ('settlement', :holder_id, NOW() + INTERVAL '30 second')
ON CONFLICT (lock_name)
DO UPDATE SET holder_id = EXCLUDED.holder_id,
              expires_at = EXCLUDED.expires_at
WHERE batch_leader_lock.expires_at < NOW();

예시 C: Spring 서비스에서 락 확인 후 실행

public void runIfLeader() {
    boolean acquired = leaderLockRepository.tryAcquire("settlement", instanceId, Duration.ofSeconds(30));
    if (!acquired) {
        return;
    }
    try {
        settlementService.execute();
    } finally {
        leaderLockRepository.release("settlement", instanceId);
    }
}

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

SELECT id, job_name, instance_id, status, started_at
FROM batch_job_execution
WHERE job_name = 'settlement'
  AND id > :last_id
ORDER BY id ASC
LIMIT 200;

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

상황: 네트워크 분할로 Redis 락 갱신이 실패했고, 두 인스턴스가 모두 자신이 리더라고 판단해 배치를 동시에 실행했다(Split-brain).

원인:

  • 락 획득만 있고 펜싱 토큰(fencing token)이 없었다.
  • 하위 Writer가 "최신 토큰만 유효" 규칙을 검증하지 않았다.
  • 락 TTL이 짧아 일시 지연 시 재선출이 빈번했다.

개선:

  1. 리더 락 발급 시 단조 증가 토큰 저장.
  2. Writer에서 토큰 검증 후 낮은 토큰 작업 거부.
  3. 락 TTL과 heartbeat 간격을 네트워크 지연 P99 기준으로 재설정.

5) 설계 체크리스트

  • 실행 주체를 K8s CronJob 또는 앱 내부 중 하나로 명확히 정의했는가?
  • concurrencyPolicy 또는 분산락으로 동시 실행을 차단하는가?
  • 리더 선출에 펜싱 토큰을 적용했는가?
  • 락 만료/갱신 실패 시 중단 및 복구 절차가 있는가?
  • 실행 이력과 리더 변경 이력을 추적하는가?
  • 플랫폼 팀 역량에 맞는 락 기술(DB/Redis/Zookeeper)을 선택했는가?

6) 요약

분산 배치 설계의 핵심은 "한 번만 실행"이 아니라 "중복 실행이 발생해도 안전"한 구조다. 실행 주체, 락, 멱등성, 펜싱 토큰을 함께 설계해야 운영 리스크를 통제할 수 있다.

7) 다음 편 예고

다음 편에서는 성능 최적화를 다룬다. 배치 사이즈, 커밋 간격, JVM 메모리 계산, Backpressure 설계를 실제 수치 모델로 설명한다.

참고 링크

시리즈 네비게이션

댓글