프로그래밍/디자인패턴

리더 선출 패턴(Leader Election Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 21:03

"새벽 1시에 정산 배치가 돌아야 하는데, 서버가 5대 떠 있습니다. 별도 처리를 안 하면 5대 모두가 정산을 시작해서 돈이 5배로 빠져나갑니다."

분산 시스템에서는 "오직 하나의 인스턴스만 특정 작업을 수행하도록" 보장해야 할 때가 많습니다. 이를 위해 여러 서버 중 하나를 리더로 뽑아야 하는데, 이 투표소를 어디에 차리느냐가 핵심입니다.


1. 방식 1: 락 기반 선출 (Lock-based) - "깃발 꽂기"

가장 쉽고 흔한 방식입니다. RedisRDBMS 같은 외부 저장소를 이용해 **"가장 먼저 깃발을 꽂은(Lock을 획득한) 놈이 리더"**가 되는 방식입니다.

특징

  • 선착순: SETNX(Redis)나 INSERT(DB)가 성공한 서버가 리더가 됩니다.
  • TTL(Time To Live): 리더가 죽었을 때를 대비해, 깃발에 유효기간(TTL)을 설정합니다. 리더는 주기적으로 깃발을 갱신(Heartbeat)해야 합니다.

코드 예시 (Spring & Redis / Redisson)

직접 Redis 명령어를 쓰기보다, Redisson 같은 라이브러리를 쓰면 락 만료와 갱신을 자동으로 처리해 줍니다(Watchdog).

Java
 
@Component
public class BatchJob {

    private final RedissonClient redissonClient;

    @Scheduled(cron = "0 0 1 * * *") // 매일 새벽 1시
    public void runDailySettlement() {
        // "leader-lock"이라는 깃발 꽂기 시도
        RLock lock = redissonClient.getLock("settlement-leader-lock");

        // 락 획득 시도 (기다리지 않음, 획득하면 10초간 유효)
        try {
            boolean isLeader = lock.tryLock(0, 10, TimeUnit.SECONDS);
            
            if (isLeader) {
                System.out.println("내가 리더다! 정산 시작");
                processSettlement();
            } else {
                System.out.println("나는 리더가 아님. 대기.");
            }
        } catch (InterruptedException e) {
            // 예외 처리
        } finally {
            // 작업이 끝나면 락 해제 (혹은 배치 작업이 길면 TTL 연장 필요)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

장단점

  • 장점:
    • 구축 용이: 이미 Redis나 DB를 쓰고 있다면 추가 인프라가 필요 없습니다.
    • 가벼움: 단순한 Key-Value 연산이므로 빠릅니다.
  • 단점:
    • 시계 동기화 문제: 서버 간의 시간이 안 맞거나, GC로 인한 멈춤(Stop-the-world)이 길어지면 락이 만료되어 리더가 2명이 되는(Split-brain) 상황이 발생할 수 있습니다.
    • 단일 실패 지점: Redis가 죽으면 리더 선출이 불가능해집니다.

2. 방식 2: 합의 알고리즘 (Consensus / Zookeeper, Etcd) - "민주적 투표"

Kafka나 Hadoop 같은 거대 분산 시스템이 사용하는 방식입니다. ZookeeperEtcd 같은 코디네이터 시스템을 사용하여 강력한 일관성을 보장합니다.

특징

  • 임시 노드(Ephemeral Node): 리더가 연결을 맺으면 임시 노드를 만듭니다. 연결이 끊기면 노드가 즉시 사라지고, 다른 서버들이 이를 감지(Watch)하여 즉시 재투표합니다.
  • 순차적 처리: 선착순이 아니라 순번을 매겨서 관리하므로 경쟁 상태(Race Condition)를 더 정교하게 제어합니다.

코드 예시 (Spring Cloud Kubernetes / Zookeeper)

Spring Cloud는 LeaderInitiator를 통해 복잡한 로직을 추상화해 줍니다.

Java
// Spring Cloud Kubernetes / Zookeeper 사용 시
@Component
public class LeaderService implements ApplicationEventPublisherAware {

    // 내가 리더가 되면 이벤트가 날아옴
    @EventListener
    public void handleEvent(OnGrantedEvent event) {
        System.out.println("내가 리더로 선출되었습니다!");
        startWorker();
    }

    // 리더 자격을 박탈당하면 이벤트가 날아옴
    @EventListener
    public void handleEvent(OnRevokedEvent event) {
        System.out.println("리더 자격을 잃었습니다. 작업 중단.");
        stopWorker();
    }
}

장단점

  • 장점:
    • 강력한 신뢰성: 네트워크 분단이나 서버 장애 상황에서도 오직 1명의 리더만 존재함을 수학적(Paxos, Raft 알고리즘)으로 보장합니다.
    • 실시간 감지: 리더가 죽으면 TTL을 기다릴 필요 없이 즉시(세션 타임아웃 내) 감지하여 교체합니다.
  • 단점:
    • 운영 비용: Zookeeper나 Etcd 클러스터를 별도로 구축하고 관리해야 하므로 인프라 비용이 큽니다.
    • 복잡성: 구현 및 디버깅 난이도가 높습니다.

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Simple Lock (Redis/DB) Consensus (Zookeeper/Etcd)
판단 기준 Lock 획득 여부 (SETNX) 세션 유지 및 순서 (Ephemeral)
인프라 기존 Redis/DB 활용 별도 클러스터 필요
신뢰성 중간 (TTL 만료 이슈 존재) 최상 (강력한 일관성)
구현 난이도 낮음 (Redisson 라이브러리) 높음 (Curator 등)
추천 상황 일반적인 배치 스케줄러, 중복 실행돼도 치명적이지 않을 때 금융 거래, 데이터 샤딩 관리, 중복 실행이 절대 안 되는 경우

결론

"스케줄러가 가끔 두 번 돌아서 로그에 에러 좀 찍히는 건 괜찮다"거나 "인프라를 늘리기 싫다"면 Redis(Redisson) 락이 가장 가성비 좋은 선택입니다.

하지만 **"이 작업이 동시에 두 번 실행되면 고객 돈이 두 번 빠져나간다"**거나 **"Kafka 같은 분산 시스템의 마스터 노드를 관리해야 한다"**면, 반드시 Zookeeper나 Etcd를 이용한 합의 알고리즘 방식을 사용해야 합니다.