콘텐츠로 이동

Bluetape4k Leader Part 4: Spring Boot와 Ktor 통합

Part 2에서는 LeaderElector, SuspendLeaderElector, AsyncLeaderElector, VirtualThreadLeaderElector의 실행 모델을 살펴봤습니다. Part 3에서는 단일 리더에서 복수 리더 슬롯과 전략 선출로 범위를 넓혔습니다.

Part 4에서는 “호출자가 매번 runIfLeader로 작업을 감싸야 하는가?”라는 질문을 다룹니다. Spring Boot 서비스에서는 예약 작업이나 서비스 메서드에 애너테이션을 붙이고, Ktor 서비스에서는 애플리케이션 생명주기 안에 리더 전용 백그라운드 작업을 등록할 수 있습니다. bluetape4k-leader는 이러한 실행 경계를 별도 모듈로 제공합니다.

Spring Boot 애너테이션 카드와 Ktor 서버 콘솔을 설정하는 작은 로봇들이 리더 선출 관리 패널을 바라보는 3D 작업대 일러스트
Part 4는 핵심 API가 프레임워크와 만나는 지점을 다룹니다. Spring에서는 애너테이션과 AOP, Ktor에서는 플러그인과 애플리케이션 작업으로 실행 권한 경계를 표현합니다.

Spring Boot: 애너테이션으로 실행 경계를 고정하기

섹션 제목: “Spring Boot: 애너테이션으로 실행 경계를 고정하기”

leader-spring-boot@LeaderElection@LeaderGroupElection을 제공합니다. 핵심은 메서드 실행 전체를 리더 선출 작업으로 취급한다는 점입니다. 잠금을 획득한 노드만 메서드 본문을 실행하고, 획득하지 못한 노드는 반환 형식에 맞춰 실행을 건너뜁니다.

@Service
class ReportJob {
@Scheduled(cron = "0 0 * * * *")
@LeaderElection(name = "hourly-report", leaseTime = "PT10M")
fun generateHourlyReport(): Report? {
return reportService.generate()
}
}

직접 API로 쓰면 다음 모양입니다.

val result = leaderElector.runIfLeader("hourly-report") {
reportService.generate()
}

애너테이션은 이러한 반복 코드를 프레임워크 경계로 옮깁니다. 대신 애너테이션이 감싸는 메서드의 반환 형식, 프록시와 AOP 경계, 잠금 이름 생성 규칙이 운영 계약이 됩니다. 코드는 간결해지지만 실행 권한을 획득하는 위치는 더욱 명확해야 합니다.

관련 소스:

Spring LeaderElectionAspect가 SpEL 잠금 이름 평가, 선출기 선택, 메서드 본문 실행, 메트릭 기록, 실패 모드 처리를 거쳐 결과를 반환하는 순서도
@LeaderElection은 간단해 보이지만 내부 실행 흐름은 복잡합니다. AOP 어드바이스는 잠금 이름을 평가하고 옵션과 팩토리 조합으로 선출기를 선택한 뒤, 백엔드 결과와 메서드 예외를 구분해 메트릭과 실패 모드 분기로 전달합니다.

애너테이션은 단순한 장식이 아닙니다. 실제 운영 경계는 애스펙트 내부에 있으며, 잠금 경합과 백엔드 오류, 메서드 실행 중 발생한 예외는 서로 다른 흐름으로 처리됩니다. Part 2에서 강조한 LeaderRunResult의 의미가 AOP에서도 그대로 중요해지는 이유입니다.

@LeaderGroupElection: 애너테이션으로 N개 슬롯 허용하기

섹션 제목: “@LeaderGroupElection: 애너테이션으로 N개 슬롯 허용하기”

Part 3에서 살펴본 그룹 선출도 애너테이션으로 표현할 수 있습니다. 하나의 메서드를 최대 maxLeaders개 노드에서 동시에 실행하려면 @LeaderGroupElection을 사용합니다.

@LeaderGroupElection(
name = "tenant-rollup",
maxLeaders = 3,
leaseTime = "PT5M",
)
fun rollupNextTenant(): RollupResult? {
return tenantRollupService.rollupNext()
}

maxLeaders <= 1은 그룹 선출의 의미가 없으므로 시작 시 검증에서 거부됩니다. 그룹 선출 애너테이션은 아직 Flux<T>Flow<T> 스트림 반환을 지원하지 않습니다. 슬롯마다 리스 연장과 완료 시점을 별도로 설계해야 하기 때문입니다. 장기 스트림을 그룹 슬롯 안에서 실행하면 이러한 운영 경계를 결정하지 않은 채 뒤로 미루게 됩니다.

관련 소스:

잠금 이름에 SpEL을 사용하되 구문은 엄격하게

섹션 제목: “잠금 이름에 SpEL을 사용하되 구문은 엄격하게”

정적 잠금 이름은 간단합니다.

@LeaderElection(name = "daily-settlement")
fun settle()

동적 잠금 이름이 필요하면 SpEL 표현식을 사용합니다.

@LeaderElection(name = "'process-' + #region")
fun process(region: String)

주의할 점은 process-#region이 아니라 "'process-' + #region"이라는 점입니다. 따옴표가 없는 process-는 문자열 접두사가 아니라 SpEL 식별자로 해석됩니다. Spring 속성 자리표시자도 사용할 수 있습니다.

@LeaderElection(name = "\${spring.application.name}-warmup")
fun warmup()

AOP 속성의 기본 접두사는 bluetape4k.leader.aop.*입니다. 기본 대기 시간과 리스 시간, 엄격한 검증, 실패 모드, 잠금 이름 접두사, SpEL 메서드 호출 허용 여부를 여기서 조정합니다. 기본 잠금 이름 접두사는 ${spring.application.name:}:입니다. 여러 애플리케이션이 같은 백엔드를 공유할 때 서비스 이름을 잠금 키에 포함해 충돌 가능성을 줄입니다.

bluetape4k:
leader:
aop:
enabled: true
strict: false
failure-mode: RETHROW
default-wait-time: PT5S
default-lease-time: PT1M
lock-name-prefix: "${spring.application.name:}:"
spel:
allow-method-invocation: false

관련 소스:

failureMode: 잠금 경합과 백엔드 장애 구분하기

섹션 제목: “failureMode: 잠금 경합과 백엔드 장애 구분하기”

잠금 경합으로 실행하지 못하는 것은 정상적인 결과입니다. 백엔드 장애는 이와 다릅니다. Redis, SQL, MongoDB, Kubernetes API 같은 백엔드가 실패했을 때 서비스 메서드를 어떻게 처리할지 failureMode로 정합니다.

모드의미
RETHROW백엔드 예외를 리더 선출 예외로 감싸 호출자에게 전파
SKIP백엔드 장애도 실행하지 않은 것으로 처리
FAIL_OPEN_RUN잠금 경로가 실패해도 메서드 본문 실행

FAIL_OPEN_RUN은 조심해서 써야 합니다. 리더 선출은 중복 실행을 막기 위한 장치이므로, 실패 시 허용 방식은 장애 상황에서 그 보호를 의도적으로 낮춥니다. 통계 캐시 워밍처럼 중복 실행을 허용할 수 있는 작업에는 실용적일 수 있습니다. 정산, 결제, 스키마 마이그레이션처럼 중복 실행이 위험한 작업에는 적합하지 않습니다.

@LeaderElection(
name = "dashboard-job",
failureMode = LeaderAspectFailureMode.FAIL_OPEN_RUN,
)
fun refreshDashboardSnapshot()

관련 소스:

메트릭으로 애너테이션 경계 관측하기

섹션 제목: “메트릭으로 애너테이션 경계 관측하기”

Spring Boot에서 애너테이션을 사용하면 메서드 실행 여부보다 세분화된 사건을 관측해야 합니다. 잠금 획득 시도와 성공, 경합이나 백엔드 오류로 실행하지 못한 경우, 메서드 실행 시간과 실패, 현재 실행 상태가 이에 해당합니다.

leader-micrometerMicrometerLeaderAopMetricsRecorderMeterRegistry가 있으면 AOP 콜백을 미터로 변환합니다. Prometheus가 수집할 때는 leader_aop_* 이름으로 노출됩니다.

미터의미
leader.aop.attempts잠금 획득 시도
leader.aop.acquired리더 선출 성공
leader.aop.lock.not.acquired실행하지 못한 횟수. reason 태그로 CONTENTION, BACKEND_ERROR 등을 구분
leader.aop.execution.duration메서드 실행 시간
leader.aop.task.failed메서드 실행 실패
leader.aop.active현재 실행 중인 리더 작업

prometheus-dashboard 예제는 dashboard-job이라는 @LeaderElection 작업을 등록하고 /actuator/prometheus에서 leader_aop_* 메트릭을 확인합니다. 운영 환경에서는 잠금 이름의 카디널리티를 제한해야 합니다. 테넌트 ID나 요청 ID를 잠금 이름 태그에 그대로 사용하면 시계열 수가 급증해 저장소와 질의 비용이 커집니다.

관련 소스:

Ktor: 플러그인과 애플리케이션 생명주기

섹션 제목: “Ktor: 플러그인과 애플리케이션 생명주기”

Ktor 통합은 Spring AOP 대신 명시적인 애플리케이션 경계를 사용합니다. LeaderElectionPluginSuspendLeaderElector를 주입하고 leaderScheduled로 주기 작업을 등록합니다.

fun Application.module() {
val elector = createLettuceSuspendLeaderElector()
install(LeaderElectionPlugin) {
leaderElection = elector
managementRouteEnabled = true
}
leaderScheduled("hourly-stats-aggregation", period = 1.hours) {
statsAggregator.aggregate()
}
}

leaderScheduled는 애플리케이션 코루틴에서 반복 작업을 실행합니다. 주기마다 SuspendLeaderElector.runIfLeader(lockName) { ... }를 호출하며, 애플리케이션 코루틴 범위에서 실행되므로 애플리케이션이 종료될 때 함께 취소됩니다. CancellationException은 다시 던지고, 일반 예외는 로그로 남긴 뒤 다음 주기를 계속합니다. 이 동작은 주기적으로 반복하는 백그라운드 작업에 적합합니다. 한 번의 실패가 애플리케이션 전체를 중단시키지는 않지만 종료를 위한 취소 신호는 삼키지 않습니다.

관련 소스:

leader-ktor는 관리 경로도 제공합니다. 기본 경로는 /management/leaderElection입니다. 관리 경로는 등록된 잠금 이름을 순회하며 state(lockName)의 스냅샷을 JSON으로 반환합니다.

install(LeaderElectionPlugin) {
leaderElection = elector
managementRouteEnabled = true
managementRoutePath = "/management/leaderElection"
}

여기서 중요한 점은 조회 결과가 스냅샷이라는 사실입니다. 관리 경로는 현재 잠금을 보유한 주체를 확인하는 관측 도구이며, 실행 여부를 결정하는 API가 아닙니다. 실행 여부는 잠금 획득과 메서드 본문 실행을 하나의 원자적 경계로 묶은 runIfLeader 경로에서 결정해야 합니다. 상태를 조회한 뒤 별도로 메서드 본문을 실행하면 조회와 실행 사이에 상태가 달라지는 TOCTOU 경쟁 조건이 발생합니다. 또한 관리 경로 자체에는 인증이나 권한 검사가 없으므로 내부 관리 포트, 네트워크 정책, 인증 계층 등 신뢰할 수 있는 관리 경계 뒤에 노출해야 합니다.

관련 소스:

상황Spring BootKtor
기존 @Scheduled 메서드 보호@LeaderElection기존 스케줄러 작업을 leaderScheduled로 이전
클러스터 전체에서 최대 N개 작업 허용@LeaderGroupElection(maxLeaders = N)핵심 그룹 선출기를 직접 구성
코루틴 기반 애플리케이션 작업suspend 함수 애너테이션 또는 SuspendLeaderElector 직접 호출LeaderElectionPlugin + leaderScheduled
Prometheus 메트릭leader-micrometer + AOP 기록기핵심 API의 Micrometer 래퍼 또는 경로별 메트릭
상태 확인 경로Spring Actuator, 상태 검사, 메트릭 중심/management/leaderElection

Spring 애너테이션은 서비스 메서드 경계가 이미 명확할 때 적합합니다. Ktor 보조 함수는 애플리케이션 생명주기 안에서 백그라운드 작업을 명시적으로 실행할 때 적합합니다. 두 방식의 원칙은 같습니다. 잠금 경합은 정상 결과이고 상태 조회는 관측일 뿐이며, 실행 결정은 잠금 획득과 메서드 본문 실행을 하나로 묶은 API에서 내려야 합니다.

Part 4의 핵심은 리더 선출을 프레임워크 안에 숨기는 데 있지 않습니다. 실행 권한을 획득하는 경계를 프레임워크의 생명주기에 맞춰 검토하고 운영하기 쉬운 위치로 옮기는 데 있습니다. Spring Boot에서는 애너테이션과 AOP가, Ktor에서는 플러그인과 애플리케이션 작업이 그 경계입니다.

Part 5에서는 백엔드와 운영 기능을 살펴봅니다. Redis, SQL/R2DBC, MongoDB, Hazelcast, Kubernetes, etcd, Consul 백엔드가 리스와 TTL, 장애 처리, 메트릭 측면에서 어떻게 다른지 정리합니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.