Bluetape4k Leader Part 1: 리더 선출의 기본 모델

이 글은 bluetape4k-leader 시리즈의 첫 편입니다. 저장소의 전체 구조를 살펴본 뒤 핵심 API, 복수 리더와
전략 선출, Spring Boot·Ktor 통합, 백엔드와 운영 기능을 차례로 다룹니다.
분산 리더 선출이 해결하는 문제는 구체적입니다. 서버를 3대로 늘리면 야간 정산 작업과 스키마 마이그레이션도 3번 실행될 수 있고, 웹훅 폴러 3개가 같은 이벤트를 선점하려고 경쟁할 수 있습니다. 서비스를 수평 확장해도 “한 번만 실행해야 하는 작업”이 자동으로 단일 실행되는 것은 아닙니다. 작업을 수행할 노드 하나를 정해야 합니다.
bluetape4k-leader는 이러한 결정을 Kotlin/JVM 서비스에서 일관된 API로 처리하는 독립 라이브러리입니다.
Redis, Exposed JDBC/R2DBC, MongoDB, DynamoDB, etcd, Kubernetes Lease, Hazelcast, ZooKeeper 백엔드를 지원하며,
블로킹, CompletableFuture, 코루틴, 가상 스레드 API를 제공합니다.
문제: 레플리카는 여러 개지만 작업은 한 번만
섹션 제목: “문제: 레플리카는 여러 개지만 작업은 한 번만”대표적인 사례는 스케줄 작업입니다. @Scheduled는 각 JVM에서 독립적으로 실행되므로 Pod가 3개면 동일한
스케줄 신호도 3번 처리됩니다.
val result = leaderElector.runIfLeader("nightly-settlement") { settlementService.processYesterday()}
if (result == null) { log.info { "다른 인스턴스가 실행 중이므로 건너뜁니다" }}핵심은 null 반환입니다. 리더로 선출되지 않은 호출은 예외를 던지지 않고 작업을 건너뜁니다. ShedLock의
“경합 시 건너뛰기”와 같은 동작입니다. 잠금 경합에서 밀린 호출은 실패가 아니므로 모두 오류 로그로 기록하면
실제 장애 신호를 식별하기 어려워집니다.
runIfLeader의 기본 계약은 단순합니다.
- 잠금을 획득한 호출만
action을 실행합니다. - 잠금을 획득하지 못하면
null을 반환합니다. action에서 발생한 예외는 호출자에게 전파합니다.- 실행이 끝나거나 리스가 만료되면 백엔드 정책에 따라 리더 권한을 해제하거나 만료 처리합니다.
실행 결과를 명시적으로 구분해야 한다면 LeaderRunResult를 사용합니다. action이 null을 반환할 수 있으면
단순한 T?만으로는 “잠금 미획득”과 “action이 null을 반환한 경우”를 구분할 수 없습니다.
when (val result = leaderElector.runIfLeaderResult("daily-job") { runJob() }) { is LeaderRunResult.Elected -> log.info { "실행 완료=${result.value}" } LeaderRunResult.Skipped -> log.info { "다른 노드가 리더입니다" } is LeaderRunResult.ActionFailed -> log.warn(result.cause) { "리더 작업 실패" }}관련 소스: LeaderElector.kt, LeaderRunResult.kt
저장소 구조
섹션 제목: “저장소 구조”bluetape4k-leader를 bluetape4k-projects에서 독립 저장소로 분리한 이유는 관리 범위가 넓기 때문입니다.
핵심 API 자체는 작지만 운영 환경에서는 백엔드 간 동작 일관성, 리스 의미론, Spring·Ktor 통합, 메트릭,
예제, 벤치마크를 함께 관리해야 합니다.

| 영역 | 대표 모듈 | 읽어야 하는 순간 |
|---|---|---|
| 핵심 API | leader-core | LeaderElector, SuspendLeaderElector, 그룹 선출, 결과 타입을 파악할 때 |
| Redis | leader-redis-lettuce, leader-redis-redisson | Redis로 분산 잠금과 리스를 구성할 때 |
| SQL·코루틴 | leader-exposed-jdbc, leader-exposed-r2dbc | 기존 관계형 데이터베이스에 행 기반 리스를 저장할 때 |
| 인프라 네이티브 | leader-etcd, leader-k8s, leader-consul | 플랫폼의 리스·세션 기능을 직접 사용할 때 |
| 프레임워크 | leader-spring-boot, leader-ktor, leader-micrometer | 애너테이션, 스케줄러, Actuator, 메트릭과 연동할 때 |
| 예제·벤치마크 | examples/*, benchmark | 운영 시나리오와 백엔드 비용을 함께 비교할 때 |
현재 README에서 Redis Lettuce/Redisson, Exposed JDBC/R2DBC, MongoDB, Hazelcast, ZooKeeper는 안정 백엔드로, DynamoDB, etcd, Consul, Kubernetes Lease는 프리뷰 백엔드로 분류합니다.
실행 모델이 중요한 이유
섹션 제목: “실행 모델이 중요한 이유”ShedLock 형태의 애너테이션과 “경합 시 건너뛰기”만 필요하다면 여러 선택지가 있습니다. bluetape4k-leader는
서비스 코드가 리더 선출 때문에 기존 실행 모델을 바꾸지 않도록 각 실행 방식에 대응하는 API를 제공합니다.
같은 리더 선출이라도 Kotlin/JVM 서비스의 호출 지점에 따라 실행 방식이 달라집니다.
| 실행 모델 | API | 어울리는 코드 |
|---|---|---|
| 블로킹 | LeaderElector | 기존 MVC, 배치, 일반 JVM 작업 |
| 비동기 | AsyncLeaderElector | CompletableFuture 기반 어댑터 |
| 코루틴 | SuspendLeaderElector | suspend 서비스, R2DBC, Ktor 핸들러 |
| 가상 스레드 | VirtualThreadLeaderElector | Java 21 가상 스레드 실행 경계 |
코루틴 API의 action은 suspend 함수입니다. 취소 요청을 받으면 CancellationException을 감추지 않고
다시 던져야 합니다. 장기 실행 작업에서 취소 예외를 삼키면 서비스가 종료되는 동안에도 리더 작업이 계속 실행될 수 있습니다.
val result = suspendLeaderElector.runIfLeader("tenant-aggregator") { metricsService.aggregateTenant(tenantId)}관련 소스: SuspendLeaderElector.kt, LeaderElectionOptions.kt
ShedLock과 비교하면
섹션 제목: “ShedLock과 비교하면”ShedLock은 스케줄 작업의 중복 실행을 막는 도구입니다. bluetape4k-leader도 잠금을 획득하지 못하면 작업을
건너뛰는 핵심 동작을 공유합니다. 그러나 지원 범위는 Spring 스케줄 메서드 보호에 한정되지 않습니다.
| 항목 | ShedLock 방식 | bluetape4k-leader 방식 |
|---|---|---|
| 경합 처리 | 잠금 미획득 시 건너뜀 | runIfLeader는 null, 결과 API는 Skipped 반환 |
| Spring 애너테이션 | 주 사용 경로 | 지원하지만 핵심 API와 분리 |
| 코루틴 | 별도 어댑터 필요 | SuspendLeaderElector 제공 |
| 비동기 | 별도 어댑터 필요 | AsyncLeaderElector 제공 |
| 가상 스레드 | 일반 블로킹 코드로 사용 | 전용 VirtualThreadLeaderElector 제공 |
| 복수 리더 | 주 목적 아님 | LeaderGroupElector로 N개 동시 리더 허용 |
| 전략 선출 | 주 목적 아님 | FIFO, 무작위, 점수, 가중치 기반 선출 |
| 지원 백엔드 | 여러 잠금 공급자 | Redis, SQL/R2DBC, MongoDB, DynamoDB, etcd, Kubernetes, Hazelcast, ZooKeeper |
Spring 스케줄 메서드 하나의 중복 실행만 막는다면 ShedLock과 해결 범위가 겹칩니다. 코루틴 작업자,
테넌트별 폴러, N개 슬롯을 공유하는 작업, Kubernetes Lease 기반 오퍼레이터, Ktor 스케줄러까지 하나의
API로 다뤄야 한다면 bluetape4k-leader가 더 넓은 실행 모델을 제공합니다.
예제에서 보는 실제 사용처
섹션 제목: “예제에서 보는 실제 사용처”다음 6개 예제는 각각 하나의 운영 실패 시나리오와 이를 방지하는 실행 경계를 보여줍니다.
배치 스케줄러 — 야간 정산 작업이 레플리카 수만큼 실행되는 문제를 다룹니다. Lettuce Redis 잠금으로 한 노드만 작업을 수행합니다.

마이그레이션 게이트 — 여러 Pod가 시작할 때 스키마 마이그레이션을 동시에 시도하는 문제를 다룹니다. Exposed JDBC 잠금으로 한 Pod만 마이그레이션을 실행하고 나머지는 완료 여부를 확인합니다.

웹훅 폴러 — 여러 폴러가 같은 웹훅 이벤트를 중복 선점하는 문제를 다룹니다. MongoDB 기반
SuspendLeaderElector로 한 폴러만 이벤트를 가져가게 합니다.

캐시 워머 — 파티션별 캐시 워밍이 여러 노드에서 중복 실행되는 문제를 다룹니다. 파티션마다 독립된 잠금을 사용해 캐시 워밍을 분산 실행합니다.

테넌트 집계기 — 테넌트별 코루틴 폴링이 여러 노드에서 동시에 실행되는 문제를 다룹니다.
lockNamePrefix와 테넌트 ID로 독립된 잠금 이름을 만들고 SuspendLeaderElector로 실행을 제어합니다.

Kubernetes 오퍼레이터 — 여러 오퍼레이터 레플리카가 같은 reconcile 루프를 동시에 실행하는 문제를
다룹니다. Kubernetes Lease API를 직접 사용하는 KubernetesLeaseLeaderElector로 실행을 제어합니다.

관련 소스: examples/batch-scheduler, examples/tenant-aggregator, examples/k8s-operator
선택 기준
섹션 제목: “선택 기준”백엔드 선택에는 운영 중인 인프라와 작업의 동시 실행 한도를 먼저 반영해야 합니다. 자세한 비교는 Part 5에서 다루며, 여기서는 초기 선택 기준을 정리합니다.
- Redis를 이미 운영하고 있다면 Lettuce나 Redisson으로 시작할 수 있습니다.
- RDB만 있는 서비스라면 Exposed JDBC/R2DBC가 운영 부담을 줄입니다.
- Kubernetes 오퍼레이터나 플랫폼 네이티브 컨트롤러라면 Kubernetes Lease를 검토합니다.
- etcd, Consul, ZooKeeper를 이미 컨트롤 플레인으로 사용한다면 해당 세션·리스 모델을 활용할 수 있습니다.
- 같은 작업을 최대 N개 노드에서 실행해야 한다면 단일 리더 대신
LeaderGroupElector를 사용합니다. - 가장 오래 유휴 상태였던 노드나 가중치가 높은 후보를 선출해야 한다면 전략 선출을 사용합니다.
참고 링크
섹션 제목: “참고 링크”- 저장소: bluetape4k-leader
- 영문 안내서: README.md
- 한국어 안내서: README.ko.md
- BOM 안내서: bluetape4k-leader-bom/README.md
마무리
섹션 제목: “마무리”리더 선출의 목적은 한 번만 실행해야 하는 작업을 실제로 단일 실행하는 데 있습니다. 스케줄 작업, 마이그레이션,
폴러, 캐시 워머, 오퍼레이터의 reconcile 루프는 모두 현재 레플리카가 작업을 실행해도 되는지 판단해야 합니다.
bluetape4k-leader는 백엔드별 API 대신 공통 실행 모델로 이 판단을 처리합니다. Part 2에서는
LeaderElector, SuspendLeaderElector, LeaderElectionOptions, LeaderRunResult의 계약을 살펴봅니다.
핵심은 작업을 실행한 노드와 경합에서 건너뛴 노드의 결과를 구분하는 것입니다.
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.