분산 락이 갑자기 풀리는 이유? GC 일시정지 15초가 동시성을 무너뜨린 실제 사례
# 서론 – “왜 우리 서비스가 멈추나요?”
분산 시스템에서 동시성(Concurrency) 을 제어하기 위해 Redis 와 같은 외부 저장소를 이용해 분산 락(Distributed Lock) 을 자주 사용합니다. 하지만 어느 순간 “락이 풀리지 않아 서비스가 멈춘다”는 보고를 받게 되면, 원인을 찾기 어려워 좌절하게 됩니다.
이번 글에서는 Java 애플리케이션 이 가비지 컬렉션(GC) 일시정지 때문에 15초 동안 멈추면서 이미 잡은 Redis 락을 해제하지 못해 발생한 분산 락 실패 사례를 중심으로, 왜 이런 일이 일어나는지, 어떤 위험이 있는지, 그리고 예방·대응 방법을 초보자도 이해하기 쉽게 정리합니다.
핵심 키워드:
분산 락,가비지 컬렉션,동시성 문제
# 1️⃣ 분산 락이란? – 기본 개념과 동작 원리
# 1-1. 락의 목적
여러 서비스 인스턴스가 같은 데이터(예: 은행 계좌 잔액)를 동시에 수정하면 데이터 정합성이 깨집니다. 이를 방지하기 위해 락을 걸어 하나의 인스턴스만 해당 작업을 수행하도록 합니다.
# 1-2. Redis 로 구현하는 간단한 락
redis
SET lock_key my_id NX PX 10000 # 10초 동안만 유효한 락 생성
NX: 키가 없을 때만 생성PX 10000: 10 000 ms(10초) 뒤 자동 만료
# 1-3. 일반적인 흐름
락 요청 → 성공하면 비즈니스 로직 수행
작업 완료 →
DEL lock_key로 락 해제예외 발생 → TTL 이 지나면 자동 해제
이때 TTL 은 “시간 제한”에 의존하기 때문에, 작업이 예상보다 오래 걸리면 락이 예정된 시간보다 더 오래 유지될 위험이 있습니다.
# 2️⃣ 가비지 컬렉션(GC) 일시정지란?
# 2-1. GC가 하는 일
JVM 은 힙 메모리에서 더 이상 사용되지 않는 객체를 자동으로 정리합니다. 이 과정이 가비지 컬렉션이며, 크게 두 가지 방식이 있습니다.
| 방식 | 특징 | 일시정지 영향 |
|------|------|----------------|
| Stop‑The‑World (STW) | 모든 애플리케이션 스레드를 멈추고 GC 전용 스레드만 실행 | 전체 서비스가 잠시 멈춤 |
| Concurrent | 애플리케이션 스레드와 동시에 GC 수행 | 일시정지는 짧지만 CPU 사용량 상승 |
# 2-2. 왜 15초가 나왔을까?
힙 사이즈가 크고(8~16 GB), Full GC 가 트리거되면서 Mark‑Sweep‑Compact 단계가 실행됨
객체 수가 많아 전체 힙을 스캔하는 데 수 초가 소요되고, 최악의 경우 수십 초까지 늘어남
JVM 옵션이 최적화되지 않아 STW 일시정지가 길어짐
결과적으로 15초 동안 모든 비즈니스 스레드가 멈추어 락을 잡은 상태가 그대로 유지됩니다.
# 3️⃣ 실제 사고 분석 – Reddit 에서 공개된 사례
# 3-1. 사고 흐름
| 단계 | 발생 상황 | 문제점 |
|------|----------|--------|
| 1️⃣ 락 획득 | Process A 가 SET lock_key A NX PX 15000 로 락 잡음 | 정상 |
| 2️⃣ 비즈니스 로직 | 출금 처리 시작 (DB 업데이트, 외부 API 호출) | 진행 중 |
| 3️⃣ GC 발생 | JVM 이 Full GC 실행 → 15초 STW | 모든 스레드 멈춤 |
| 4️⃣ 락 유지 | TTL 은 감소하지만 로직이 멈춰 해제되지 않음 | 다른 인스턴스가 락을 못 잡음 |
| 5️⃣ 서비스 장애 | 사용자는 출금 요청에 대한 응답을 받지 못함 | 시스템 전체 정지 |
# 3-2. 핵심 교훈
GC 일시정지는 락 유지 시간보다 길어질 수 있다.
TTL 만으로는 충분히 보호되지 않는다.
모니터링과 알림이 없으면 문제를 조기에 발견하기 어렵다.
# 4️⃣ 예방·대응 전략 – 지금 바로 적용 가능한 체크리스트
# 4-1. GC 튜닝
Heap Size 최적화 – 필요 이상으로 큰 힙은 피한다.
Low‑Pause GC 사용 –
-XX:+UseG1GC,-XX:+UseZGC등 STW 시간을 최소화한다.JVM 옵션 예시
bash
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
- Heap Dump 와 jvisualvm 로 메모리 사용 패턴을 분석하고, 객체 생성량을 줄인다.
# 4-2. 락 설계 보강
작업 타임아웃을 락 TTL 보다 짧게 설정한다.
Watchdog 스레드 도입 → 락이 일정 시간 이상 유지되면 자동 해제 또는 알림 전송.
Redlock 등 다중 Redis 인스턴스로 내구성 강화.
# 4-3. 모니터링·알림
| 항목 | 도구 | 알림 기준 |
|------|------|-----------|
| GC Pause Time | Prometheus + Grafana (jvm_gc_pause_seconds) | 100 ms 초과 시 Slack 알림 |
| Lock Duration | Redis INFO + custom metric | TTL 초과 시 PagerDuty |
| Application Latency | OpenTelemetry | 95th percentile > 2 s |
# 4-4. 테스트와 검증
Chaos Engineering :
Chaos Mesh혹은Gremlin으로 인위적인 GC 지연을 발생시켜 복원력을 검증한다.Load Test :
k6·JMeter로 동시에 여러 인스턴스가 락을 잡는 상황을 시뮬레이션한다.
# 5️⃣ 결론 – “GC와 락, 두 마리 토끼를 동시에 잡아라”
가비지 컬렉션 일시정지는 눈에 보이지 않지만, 분산 락과 결합될 때 시스템 전체를 멈추게 하는 치명적인 원인이 됩니다.
1️⃣ GC 튜닝으로 STW 시간을 최소화하고,
2️⃣ 락 설계를 보강해 타임아웃을 명확히 하며,
3️⃣ 모니터링·알림 체계를 구축하면 대부분의 위험을 사전에 차단할 수 있습니다.
지금 바로
프로젝트에
-XX:+UseG1GC옵션을 추가하고, GC 로그를 확인해 보세요.기존 Redis 락 코드를 Watchdog 로 감싸서 오래 지속되는 락을 자동 해제하도록 수정해 보세요.
보안과 안정성은 선택이 아니라 필수입니다. 이번 포스트를 팀 회의에서 공유하고, 실제 서비스에 적용한다면 동시성 문제 로 인한 장애를 크게 줄일 수 있을 것입니다.
SEO 키워드: 분산 락, 가비지 컬렉션, 동시성 문제
Comments
Join the discussionNo comments yet. Be the first to comment.