읽기-쓰기 락 (Read-Write Lock)
핵심 인사이트 (3줄 요약)
- 본질: 읽기-쓰기 락 (Read-Write Lock, RW Lock)은 다수의 독자가 동시에 읽을 수 있으며 단 하나의 저자만 독점적으로 쓸 수 있는 동기화 원시 객체로, 뮤텍스보다 읽기 집중 환경에서 높은 동시성을 제공한다.
- 가치: 읽기 연산이 쓰기보다 훨씬 빈번한 공유 자료구조(설정 객체, 캐시, 라우팅 테이블 등)에서 뮤텍스 대비 처리량을 N배(동시 독자 수만큼) 향상시킬 수 있다.
- 융합: POSIX
pthread_rwlock_t, JavaReentrantReadWriteLock, C++std::shared_mutex, Linux 커널의rwlock_t와rw_semaphore가 표준 구현이며, RCU (Read-Copy-Update)는 독자 측 락조차 제거한 진화 형태다.
Ⅰ. 개요 및 필요성
뮤텍스는 모든 접근을 직렬화하므로, 읽기 작업도 순차적으로 처리된다. 하지만 읽기는 데이터를 수정하지 않으므로 여러 스레드가 동시에 읽어도 일관성이 깨지지 않는다. 읽기-쓰기 락은 이 특성을 활용하여 독자들의 동시 읽기를 허용하고, 저자만 독점 접근을 요구한다.
접근 규칙:
- 독자 N명 ↔ 독자 M명: 허용 (동시 읽기)
- 독자 N명 ↔ 저자 1명: 상호 배제 (읽기-쓰기 분리)
- 저자 1명 ↔ 저자 1명: 상호 배제 (쓰기 독점)
💡 비유: 도서관의 참고서 — 여러 학생이 동시에 볼 수 있지만, 선생님이 내용을 수정할 때는 모두 자리를 비워야 한다.
┌──────────────────────────────────────────────────────────┐
│ 읽기-쓰기 락 접근 매트릭스 │
├──────────────────────────────────────────────────────────┤
│ │
│ │ Reader │ Writer │
│ ─────────┼──────────────┼────────── │
│ Reader │ ✅ OK │ ❌ Block │
│ Writer │ ❌ Block │ ❌ Block │
│ │
│ 처리량 향상: │
│ 뮤텍스: 최대 1× (항상 직렬화) │
│ RWLock: 최대 독자수× (동시 읽기) │
└──────────────────────────────────────────────────────────┘
📢 섹션 요약 비유: 읽기-쓰기 락은 도서관 열람실 — 여러 학생이 동시에 책을 펼칠 수 있지만, 사서가 서가를 재배치할 때는 모두 나가야 합니다.
Ⅱ. 아키텍처 및 핵심 원리
POSIX 구현
#include <pthread.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 독자 (Reader)
pthread_rwlock_rdlock(&rwlock); // 읽기 락 획득 (다른 독자와 공유)
read_shared_data();
pthread_rwlock_unlock(&rwlock);
// 저자 (Writer)
pthread_rwlock_wrlock(&rwlock); // 쓰기 락 획득 (독점)
update_shared_data();
pthread_rwlock_unlock(&rwlock);
내부 상태 기계
┌──────────────────────────────────────────────────────────────┐
│ 읽기-쓰기 락 내부 상태 전이 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 상태: [Idle] ──rdlock──▶ [ReadLocked: count=N] │
│ │ │ │
│ │ wrlock 모든 독자 unlock │
│ ▼ ▼ │
│ [WriteLocked] ◀── [Idle] │
│ │ │
│ writlock 해제 │
│ ▼ │
│ [Idle] │
│ │
│ 변수: │
│ read_count : 현재 읽고 있는 독자 수 │
│ write_pending : 대기 중인 저자 수 │
│ write_locked : 쓰기 락 획득 여부 │
│ │
│ 독자 우선 정책: write_pending 무시하고 새 독자 진입 허용 │
│ 저자 우선 정책: write_pending > 0이면 새 독자 차단 │
└──────────────────────────────────────────────────────────────┘
[다이어그램 해설] RW Lock의 내부 구현은 독자 카운터와 쓰기 잠금 플래그를 원자적으로 관리한다. 독자 우선(Reader-Preference) 구현에서는 독자가 있으면 새 독자가 즉시 진입하여 저자 기아(Writer Starvation)가 발생할 수 있다. 저자 우선(Writer-Preference)에서는 그 반대다. 공정(Fair) 구현은 대기 큐로 기아를 방지하지만 처리량이 감소한다.
Java ReentrantReadWriteLock
import java.util.concurrent.locks.*;
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 캐시 읽기 (여러 스레드 동시)
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
// 캐시 갱신 (단독)
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
읽기→쓰기 락 업그레이드 문제
┌──────────────────────────────────────────────────────────┐
│ 읽기→쓰기 업그레이드 교착 상태 시나리오 │
├──────────────────────────────────────────────────────────┤
│ │
│ T1: readLock() 획득 │
│ T2: readLock() 획득 │
│ │
│ T1: writeLock() 시도 → T2의 readLock 해제 대기 │
│ T2: writeLock() 시도 → T1의 readLock 해제 대기 │
│ │
│ → 교착 상태! 두 스레드가 서로를 기다림 │
│ │
│ 해결: 읽기를 포기하고 쓰기 락을 처음부터 획득 │
│ 또는 StampedLock (Java 8+)의 tryConvertToWriteLock() │
└──────────────────────────────────────────────────────────┘
[다이어그램 해설] 락 업그레이드(읽기→쓰기 변환)는 대부분의 RW Lock 구현에서 지원하지 않거나 교착 상태를 유발한다. Java의 StampedLock은 낙관적 읽기(Optimistic Read)와 tryConvertToWriteLock()으로 이 문제를 보다 안전하게 처리한다.
📢 섹션 요약 비유: 읽기→쓰기 업그레이드 교착은 두 학생이 서로 "당신이 책을 놓으면 내가 수정하겠다"고 고집하다가 영원히 기다리는 상황입니다.
Ⅲ. 융합 비교 및 다각도 분석
RW Lock vs Mutex vs RCU 비교
┌──────────────────┬────────────┬────────────┬──────────────┐
│ 항목 │ Mutex │ RW Lock │ RCU │
├──────────────────┼────────────┼────────────┼──────────────┤
│ 동시 읽기 │ 불가 │ 가능 │ 가능 │
│ 독자 오버헤드 │ 락비용 │ 카운터비용 │ 사실상 0 │
│ 저자 레이턴시 │ 즉시(다음) │ 즉시(다음) │ Grace Period │
│ 기아 방지 │ 정책 필요 │ 정책 필요 │ 없음 │
│ 구현 복잡도 │ 낮음 │ 중간 │ 높음 │
│ 적합 환경 │ 균등 R/W │ 읽기 집중 │ SMP 읽기 집중│
└──────────────────┴────────────┴────────────┴──────────────┘
📢 섹션 요약 비유: Mutex는 단차선, RW Lock은 버스 전용 차선(읽기)/일반 차선(쓰기) 분리, RCU는 차선 없는 자율 주행 고속도로입니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
- 설정 객체 동기화: 마이크로서비스에서 수백 개 스레드가 설정을 초당 수만 번 읽고, 관리자가 분당 1번 업데이트. RW Lock으로 읽기 처리량 최대화.
- 데이터베이스 인덱스: B-Tree 인덱스의 읽기 접근에 RW Lock을 적용하여 SELECT 동시성 최대화, INSERT/UPDATE/DELETE만 독점.
도입 체크리스트
- 읽기:쓰기 비율 측정: 90:10 이상이면 RW Lock 효과적. 50:50이면 Mutex와 성능 차이 미미.
- 기아 방지 정책: 저자 기아가 허용되지 않는다면 공정 모드 설정 필요.
- 업그레이드 없음 원칙: 읽기 락 보유 중 쓰기 락 획득 시도 금지.
안티패턴
- 쓰기 집중 환경: 쓰기가 잦으면 독자 우선 RW Lock이 계속 락-언락을 반복해 오히려 뮤텍스보다 느림.
- 읽기 락 범위 과대: 락을 오래 보유하면 저자 기아 심화.
📢 섹션 요약 비유: 읽기-쓰기 락은 쓰기가 매우 드문 환경에서만 약이 되는 처방 — 쓰기가 잦은 환경에서는 오히려 독이 됩니다.
Ⅴ. 기대효과 및 결론
| 구분 | Mutex | RW Lock (공정) |
|---|---|---|
| 독자 8개 동시 처리량 | 1× | 최대 8× |
| 저자 기아 | 없음 | 정책 필요 |
| 구현 복잡도 | 낮음 | 중간 |
읽기-쓰기 락은 읽기 집중 환경에서 동시성을 극적으로 향상시키는 강력한 도구다. 단, 쓰기 비율, 기아 허용 여부, 락 업그레이드 필요성을 사전에 분석해야 올바른 선택이 가능하다.
📌 관련 개념 맵
| 개념 | 관계 |
|---|---|
| 독자-저자 문제 | RW Lock이 해결하는 이론적 원형 |
| RCU | RW Lock의 진화형 (독자 측 락 제거) |
| Java StampedLock | 낙관적 읽기 + 업그레이드 지원 확장 |
| 저자 기아 | RW Lock 설계 시 반드시 고려해야 할 위험 |
pthread_rwlock_t | POSIX 표준 구현 |
👶 어린이를 위한 3줄 비유 설명
- 읽기-쓰기 락은 도서관 규칙 — 여러 친구가 동시에 같은 책을 읽어도 되지만, 내용을 고치려면 모두 나가야 해요.
- 읽기가 쓰기보다 훨씬 많을 때(예: 읽기 100번, 쓰기 1번) 뮤텍스보다 훨씬 빠른 처리가 가능해요.
- 주의: 읽고 있던 사람이 "고칠게요"로 바꾸려 하면 교착 상태가 생길 수 있어요 — 항상 다시 줄을 서야 해요!