"새벽 1시에 정산 배치가 돌아야 하는데, 서버가 5대 떠 있습니다. 별도 처리를 안 하면 5대 모두가 정산을 시작해서 돈이 5배로 빠져나갑니다."
분산 시스템에서는 "오직 하나의 인스턴스만 특정 작업을 수행하도록" 보장해야 할 때가 많습니다. 이를 위해 여러 서버 중 하나를 리더로 뽑아야 하는데, 이 투표소를 어디에 차리느냐가 핵심입니다.
1. 방식 1: 락 기반 선출 (Lock-based) - "깃발 꽂기"
가장 쉽고 흔한 방식입니다. Redis나 RDBMS 같은 외부 저장소를 이용해 **"가장 먼저 깃발을 꽂은(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 같은 거대 분산 시스템이 사용하는 방식입니다. Zookeeper나 Etcd 같은 코디네이터 시스템을 사용하여 강력한 일관성을 보장합니다.
특징
- 임시 노드(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를 이용한 합의 알고리즘 방식을 사용해야 합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 인증 상태 관리 패턴: 세션(Session) vs 토큰(JWT) 완벽 비교 (0) | 2025.12.07 |
|---|---|
| 샤딩 패턴(Sharding Pattern) 완벽 정리 (0) | 2025.12.07 |
| 비동기 요청-응답 패턴(Asynchronous Request-Reply) 완벽 정리 (0) | 2025.12.07 |
| 사이드카 패턴(Sidecar Pattern) 완벽 정리 (0) | 2025.12.07 |
| 벌크헤드 패턴(Bulkhead Pattern) 완벽 정리 (0) | 2025.12.07 |