# 분산 락이 갑자기 풀리는 이유? 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. 일반적인 흐름

1. **락 요청** → 성공하면 비즈니스 로직 수행

2. **작업 완료** → `DEL lock_key` 로 락 해제

3. **예외 발생** → 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 튜닝

1. **Heap Size 최적화** – 필요 이상으로 큰 힙은 피한다.

2. **Low‑Pause GC** 사용 – `-XX:+UseG1GC`, `-XX:+UseZGC` 등 STW 시간을 최소화한다.

3. **JVM 옵션 예시**

```
bash

-Xms4g -Xmx4g \

-XX:+UseG1GC \

-XX:MaxGCPauseMillis=200 \

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

```

4. **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 키워드: **분산 락**, **가비지 컬렉션**, **동시성 문제***
