독자-저자 문제 (Readers-Writers Problem)
핵심 인사이트 (3줄 요약)
- 본질: 독자-저자 문제는 여러 독자 (Reader)가 동시에 읽을 수 있지만, 저자 (Writer)는 독점 쓰기를 요구하는 비대칭 접근 패턴에서, 기아 (Starvation) 없이 일관성을 보장하는 동기화 문제다.
- 가치: 제1유형(독자 우선)은 읽기 처리량을 극대화하지만 저자 기아를 유발하고, 제2유형(저자 우선)은 그 반대다. 이 트레이드오프는 데이터베이스 락, MVCC, RCU (Read-Copy-Update) 설계의 핵심 판단 기준이다.
- 융합: PostgreSQL MVCC, Linux RCU (Read-Copy-Update), Java ReadWriteLock, 리눅스 커널 Page Cache 등이 이 문제의 산업적 해법이다.
Ⅰ. 개요 및 필요성
독자-저자 문제는 E.W. 다익스트라가 정의한 고전적 동기화 문제로, 공유 데이터(파일, 데이터베이스 레코드, 캐시)에 대한 두 클래스의 비대칭 접근을 다룬다.
- 독자: 데이터를 읽기만 함. 여러 독자 동시 접근 허용 가능.
- 저자: 데이터를 수정함. 완전한 독점 접근 필요.
순진한 해법: 모든 접근에 mutex 사용 → 독자끼리도 순차 실행으로 처리량이 크게 저하된다. 정교한 해법이 필요하다.
💡 비유: 도서관 열람실 규칙을 상상하라. 같은 책을 여러 사람이 동시에 읽을 수 있지만, 누군가 내용을 수정(저자)하려면 독점 열람실을 사용해야 한다.
┌──────────────────────────────────────────────────────────┐
│ 독자-저자 접근 패턴 │
├──────────────────────────────────────────────────────────┤
│ │
│ [Reader1] ─┐ │
│ [Reader2] ─┤──▶ [공유 데이터] ← 동시 읽기 허용 ✅ │
│ [Reader3] ─┘ │
│ │
│ [Writer1] ──▶ [공유 데이터] ← 독점 쓰기 필요 │
│ (Reader 모두 차단, 다른 Writer도 차단) │
│ │
│ 제약 조건: │
│ ① Reader ↔ Writer: 상호 배제 │
│ ② Writer ↔ Writer: 상호 배제 │
│ ③ Reader ↔ Reader: 동시 허용 │
└──────────────────────────────────────────────────────────┘
📢 섹션 요약 비유: 독자-저자는 '다 함께 읽되, 홀로 쓰는' 원칙 — 이 단순한 규칙을 구현하는 방법에서 처리량과 공정성의 트레이드오프가 발생합니다.
Ⅱ. 아키텍처 및 핵심 원리
제1유형: 독자 우선 (Reader Priority)
// 공유 변수
int read_count = 0; // 현재 읽는 독자 수
semaphore mutex = 1; // read_count 보호
semaphore rw_mutex = 1; // 저자↔독자, 저자↔저자 상호 배제
// 독자 (Reader)
wait(mutex);
read_count++;
if (read_count == 1) // 첫 번째 독자만 rw_mutex 획득
wait(rw_mutex);
signal(mutex);
// --- 읽기 작업 ---
wait(mutex);
read_count--;
if (read_count == 0) // 마지막 독자만 rw_mutex 해제
signal(rw_mutex);
signal(mutex);
// 저자 (Writer)
wait(rw_mutex); // 독자·저자 완전 차단
// --- 쓰기 작업 ---
signal(rw_mutex);
제1유형 동작 흐름
┌──────────────────────────────────────────────────────────────┐
│ 제1유형 독자 우선 — 저자 기아 발생 시나리오 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 시간 흐름 ─────────────────────────────────────────────▶ │
│ │
│ R1: ──[읽기 시작]──────────────────────[읽기 종료]── │
│ R2: ──[읽기 시작]──────────────────────[읽기 종료]── │
│ R3: ──[읽기 시작]──────────────────────[읽기 종료]──│
│ │
│ W1: [대기 중...] ← 기아! │
│ ^ │
│ read_count > 0 내내 → rw_mutex 계속 잠김 │
│ │
│ ⚠ 연속적인 독자 유입 시 저자는 영원히 대기할 수 있음 │
└──────────────────────────────────────────────────────────────┘
[다이어그램 해설] 독자가 연속으로 들어오면 read_count가 0이 되지 않아 rw_mutex가 해제되지 않는다. 저자는 이론적으로 무한 대기할 수 있다. 실무에서는 이 기아 위험 때문에 반드시 타임아웃이나 저자 대기 카운터를 추가해야 한다.
제2유형: 저자 우선 (Writer Priority)
저자가 대기 중이면 새 독자 진입을 막아 저자가 빠르게 처리되도록 한다. 이번엔 독자 기아가 발생할 수 있다.
공정 해결 (Starvation-Free): 큐 기반
┌──────────────────────────────────────────────────────────┐
│ 공정 해결: 도착 순서 기반 큐 관리 │
├──────────────────────────────────────────────────────────┤
│ │
│ [도착 순서 큐] │
│ R1 → W1 → R2 → R3 → W2 ... │
│ │
│ 처리 순서: │
│ R1 실행 → W1이 대기 중이므로 신규 R 차단 │
│ W1 독점 실행 → R2, R3 동시 실행 → W2 독점 실행 │
│ │
│ Java ReadWriteLock (fair=true)이 이 방식을 구현함 │
└──────────────────────────────────────────────────────────┘
[다이어그램 해설] 공정 큐 방식은 대기 순서를 보존하므로 기아가 발생하지 않는다. 단, 독자 여럿을 연속 처리하지 못하므로 처리량이 감소할 수 있다. Java ReentrantReadWriteLock(fair=true)가 이를 구현하며, 저자 우선순위가 높은 환경에서는 NonfairReadWriteLock이 더 높은 처리량을 제공한다.
📢 섹션 요약 비유: 독자 우선은 바이킹 뷔페 — 독자들이 계속 들어와 저자는 굶을 수 있고, 공정 방식은 번호표 대기 — 도착 순서대로 공평하게 처리하지만 약간 느립니다.
Ⅲ. 융합 비교 및 다각도 분석
세 유형 비교
┌──────────────────┬──────────────┬──────────────┬──────────────┐
│ 항목 │ 독자 우선 │ 저자 우선 │ 공정 (큐) │
├──────────────────┼──────────────┼──────────────┼──────────────┤
│ 읽기 처리량 │ 최고 │ 낮음 │ 중간 │
│ 저자 기아 │ 발생 가능 │ 없음 │ 없음 │
│ 독자 기아 │ 없음 │ 발생 가능 │ 없음 │
│ 구현 복잡도 │ 중간 │ 높음 │ 높음 │
│ 실무 적용 │ 읽기 집중 DB │ 쓰기 중요 DB │ 일반 목적 │
└──────────────────┴──────────────┴──────────────┴──────────────┘
리눅스 RCU (Read-Copy-Update) — 궁극적 해법
RCU는 독자에게 락 없이(lock-free) 읽기를 허용하고, 저자는 복사(Copy) → 수정 → 포인터 교체(Update) 방식으로 일관성을 보장한다. 리눅스 커널에서 수천 곳에 사용되며 독자 처리량을 극대화한다.
[Writer]
원본 데이터: A → [수정 복사본: A'] 생성
RCU 포인터 교체: ptr = A' (원자적)
모든 기존 독자가 A 사용 완료 후 A 해제 (Grace Period)
[Reader]
rcu_read_lock();
ptr_local = rcu_dereference(ptr); // 현재 버전 읽기
// 읽기 작업 - 락 없이 수행
rcu_read_unlock();
📢 섹션 요약 비유: 독자-저자 트레이드오프는 읽기 처리량과 쓰기 공정성 사이의 시소 게임 — RCU는 이 시소를 완전히 없앤 혁신적 해법입니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
- PostgreSQL MVCC: 독자는 트랜잭션 시작 시점의 스냅샷을 읽고, 저자는 새 버전을 생성. 독자와 저자가 서로를 차단하지 않아 높은 동시성 달성.
- Java ConcurrentHashMap:
ReadWriteLock이 아니라 세그먼트별 락으로 더 세밀한 동시성을 구현함. 이는 독자-저자 문제를 더 작은 단위로 분해한 것. - 리눅스 커널 VFS: 파일 시스템 메타데이터 접근에 RCU를 적극 적용하여 SMP 환경에서 락 경합을 최소화.
안티패턴
- 저자 기아 무시: 로그 수집기가 무한 읽기를 허용하면 설정 변경 저자가 영원히 대기하여 설정이 적용되지 않는 장애 발생.
- RCU의 잘못된 사용: Grace Period 이전에 원본 해제 → 사용 중인 독자가 해제된 메모리 접근 → Use-After-Free 취약점.
📢 섹션 요약 비유: 독자-저자 문제의 잘못된 구현은 도서관에서 누군가 책을 태워버렸는데 다른 사람이 그 책 페이지를 읽고 있는 상황과 같습니다.
Ⅴ. 기대효과 및 결론
| 구분 | 단순 Mutex | 독자 우선 | MVCC/RCU |
|---|---|---|---|
| 읽기 처리량 | 낮음 (직렬화) | 높음 | 매우 높음 (락 프리) |
| 저자 기아 | 없음 | 가능 | 없음 |
| 구현 복잡도 | 낮음 | 중간 | 높음 |
📢 섹션 요약 비유: 독자-저자 문제 해법의 진화는 자물쇠(Mutex)→회전문(RWLock)→홀로그램 복사본(MVCC/RCU)의 순서로, 점점 더 많은 사람이 동시에 도서관을 이용하는 방향으로 진화했습니다.
📌 관련 개념 맵
| 개념 | 관계 |
|---|---|
| 세마포어 (Semaphore) | 제1·2유형 구현의 기본 도구 |
| RCU (Read-Copy-Update) | 리눅스 커널의 락 프리 독자 우선 해법 |
| MVCC | 데이터베이스의 독자-저자 분리 해법 |
| ReadWriteLock | Java/C++ 표준 라이브러리 구현 |
| 기아 상태 (Starvation) | 독자 우선/저자 우선 각각의 치명적 위험 |
👶 어린이를 위한 3줄 비유 설명
- 같은 책을 친구 여럿이 같이 읽을 수 있지만, 글씨를 고치려면 모두 자리를 비워줘야 해요.
- 독자 우선 규칙은 친구들이 계속 읽으러 오면 고치려는 친구가 계속 기다려야 해서 불공평해요.
- RCU는 제일 똑똑한 해결책 — 책을 복사해서 수정하고, 다 읽은 친구들이 떠나면 조용히 원본과 교체해요!