Leader 리스만 사용
동시에 실행되는 작업자 수는 줄이지만, 이전 작업자의 재실행을 차단하지 못합니다.
Leader 선출은 실행 후보를 정할 뿐입니다. 지연된 작업자의 데이터 변경을 차단하려면 Redis가 순서를 발급하고 PostgreSQL이 최종 반영 여부를 판정해야 합니다.
01 · 문제 파악
리스 TTL이 끝나도 이전 작업자의 스레드가 즉시 중단되는 것은 아닙니다. GC 일시 중지, 네트워크 단절, 긴 I/O가 끝나면 이전 작업자가 다시 실행될 수 있습니다.
동시에 실행되는 작업자 수는 줄이지만, 이전 작업자의 재실행을 차단하지 못합니다.
이름이 다른 작업이 같은 업무 자원을 변경하면 잠금 키가 충돌하지 않습니다.
첫 호출 결과가 불확실한 상태에서 새 요청을 만들면 외부 처리가 중복될 수 있습니다.
| 동시 실행 제한 | Redis Leader 리스와 자원 리스 | 한 시점의 실행 수를 줄입니다. |
|---|---|---|
| 작업 인계 | 리스 만료와 새 리스 획득 | 새 작업자가 처리를 이어받습니다. |
| 지연된 변경 차단 | 펜싱 토큰 + PostgreSQL | 이전 토큰의 변경을 거부합니다. |
| 재실행 안전성 | OperationId + 멱등 처리 | 같은 외부 처리를 다시 확인할 수 있습니다. |
| 처리 결과 영속화 | Exposed 트랜잭션 + Outbox | 업무 상태와 후속 작업을 함께 반영합니다. |
02 · 해결 방안
한 구성 요소가 모든 안전성을 담당하지 않습니다. 실행 조정, 최종 데이터 반영, 외부 처리 복구를 분리합니다.
Owner Token은 순서를 비교할 수 없는 식별자입니다.
리스 상태와 PostgreSQL 업무 상태가 불일치할 수 있습니다.
처리 여부를 모르는 첫 요청과 새 요청이 모두 반영될 수 있습니다.
새 작업자가 항상 더 큰 순번을 받습니다.
`incomingFence > lastAcceptedFence`인 경우에만 변경합니다.
불확실한 결과를 추측하지 않고 같은 요청을 조회합니다.
03 · 시스템 구조
한 구성 요소가 모든 안전성을 담당하지 않습니다. 실행 조정, 최종 데이터 반영, 외부 처리 복구를 분리합니다.
시나리오 선택 · SAFE/UNSAFE 격리 · 권한 검사
Leader 리스 · 충돌 자원 리스 · 펜싱 토큰 발급
현재 구성 재확인 · 조건부 갱신 · Checkpoint/Execution/Outbox
외부 시스템 호출 · 처리 확인 기록 · 결과 재확인
04 · 대화형 시스템 흐름
같은 시나리오를 SAFE와 UNSAFE로 전환하면 최종 상태가 달라지는 지점을 확인할 수 있습니다.
05 · 실제 구현
화면의 단계는 설명을 위해 새로 만든 흐름이 아니라 실제 서비스와 시나리오 코드의 동작을 재구성한 것입니다.
JobRunCoordinatorLeader 리스와 자원 리스를 순서대로 획득하고 해제합니다.
coordination/JobRunCoordinator.ktRedisJobFencingLeaseAdapterRedis Lua로 단조 증가하는 펜싱 토큰을 발급합니다.
coordination/redis/RedisJobFencingLeaseAdapter.ktFencedJobExecutionService현재 Tenant, Region, 버전, Namespace를 확인한 뒤 데이터 변경을 요청합니다.
execution/FencedJobExecutionService.ktJobSafetyRepositories조건부 갱신과 Checkpoint, 실행 이력, Outbox 저장을 담당합니다.
persistence/JobSafetyRepositories.ktOutboxEffectWorker트랜잭션 밖에서 외부 시스템을 호출하고 불확실한 결과를 재확인 대상으로 전환합니다.
effect/OutboxEffectWorker.ktJobSafetyScenarioService여섯 시나리오의 결정적인 SAFE/UNSAFE 결과를 제공합니다.
scenario/JobSafetyScenarioService.ktLeaseOverrunScenarioTest토큰 41의 변경 거부와 토큰 42의 최종 상태 유지FencedMutationPostgresIntegrationTestPostgreSQL 조건부 갱신에서 지연된 변경 차단JobSafetyEndToEndIntegrationTest업무 상태, Checkpoint, 실행 이력, Outbox의 트랜잭션 반영NonFenceableEffectScenarioTest안정적인 OperationId와 결과 재확인으로 중복 처리 방지UnsafeJobSafetyControllerConditionTestUNSAFE API의 Profile과 설정값 격리06 · 예제 실행
예제는 JDK 25, PostgreSQL 18 호환 서버와 Redis 8 호환 서버를 사용합니다. UNSAFE API는 비교 학습용으로만 별도 실행합니다.
07 · 적용 범위
이전 펜싱 토큰의 조건부 갱신을 PostgreSQL이 거부합니다.
현재 Tenant, Region, 버전과 Namespace를 트랜잭션에서 다시 확인합니다.
원래 OperationId를 조회하고 처리 확인 기록을 저장합니다.
정확히 한 번의 외부 처리를 보장할 수 없습니다. 수동 확인이나 보상 처리가 필요합니다.
백업으로 순번을 보존하거나 Namespace Epoch를 올려야 합니다.
예제는 시작 시 SchemaUtils를 사용합니다. 운영 환경에서는 Flyway나 Liquibase가 필요합니다.