Software Architecture/Distributed Systems
Distributed Systems - 06. 복제 패턴
복제 패턴 — 데이터 복사본의 일관성과 가용성
2017년 한 서비스가 읽기 부하를 분산시키려고 PostgreSQL에 읽기 복제본을 3개 추가했다. 마스터 DB의 쓰기는 비동기로 복제본에 복제됐다. 며칠 뒤 사용자 불만이 접수됐다 — "방금 게시글 썼는데 피드에 안 떠요". 조사해보니, 사용자의 쓰기는 마스터에 갔지만, 같은 사용자의 직후 읽기는 복제본에 갔고, 복제 지연(보통 수백 ms, 부하 시 수 초)이 발생했다. 사용자는 자기가 방금 쓴 글이 안 보이는 현상을 겪은 것이다. read-your-writes 위반의 전형적 사례. 복제는 데이터 가용성과 읽기 성능을 높이지만, 복제 지연이라는 새로운 일관성 문제를 만든다. 이 글은 복제의 세 가지 형태와 그 trade-off를 다룬다.
비유로 감 잡기 — 사무실 문서의 여러 복사본
사무실의 중요 문서를 여러 책상에 복사해둔다고 상상하자. 누군가 원본(마스터)을 수정하면, 다른 책상의 복사본(복제본)도 바꿔야 한다. 그 사이에 시간이 있다 — 수정 즉시 복사본이 바뀌지 않는다. 이 시간 동안 다른 직원이 복사본을 보면 낡은 정보를 본다.
이 "복사본을 어떻게 동기화할까"의 방식이 복제 패턴이다. 세 가지가 있다.
- 원본을 하나 두고 (단일 리더) — 모든 수정은 원본에만, 복사본은 원본에서 읽어와 바꿈. 단순하지만 원본이 병목.
- 수정 가능한 복사본을 여러 (다중 리더) — 어느 책상에서든 수정 가능, 다른 책상에 전파. 유연하지만 충돌 해결 필요.
- 원본 개념 자체를 없애고 (리더 없는) — 어느 책상에서든 수정하고 동시에 여러 책상에 알림. 가장 분산적이지만 일관성 모델이 약해짐.
이 세 가지가 복제 패턴의 기본 형태다. 각각의 trade-off가 다르고, 사용 사례에 따라 적합한 것이 다르다.
비유의 한계 — 사무실 비유는 '물리적 문서'를 강조하지만, 데이터베이스 복제는 일관성 모델(03편)·합의(04편)·지연·처리량이라는 다축 목표를 다룬다. 또한 복사본은 동시 수정 시 충돌이 명시적인데, 사무실 문서는 사람이 알아서 조율한다. 비유는 '충돌 해결 자동화'에서 무너진다.
왜 복제를 하는가
복제의 세 가지 주된 동기.
- 고가용성 — 한 노드가 죽어도 다른 복제본이 서비스 지속. 02편(CAP)에서 가용성이 중요한 이유.
- 읽기 성능 — 읽기 부하를 여러 복제본에 분산. 지리적으로 떨어진 복제본은 사용자와 가까운 곳에 둬 지연 감소.
- 스케일 — 읽기 처리량을 수평 확장. 쓰기는 단일 마스터에 국한되는 게 흔하지만(단일 리더), 읽기는 복제본으로 늘릴 수 있다.
복제의 비용 — 일관성 문제. 복제본이 마스터와 어떤 관계인지, 변경이 어떻게 전파되는지, 충돌을 어떻게 다루는지를 결정해야 한다. 이 비용을 다루는 패턴들이 이 장의 본론이다.
복제의 세 가지 형태
Kleppmann이 DDIA Ch.5에서 정리한 세 형태를 본다.
1. 단일 리더 (Single-Leader / Primary-Secondary)
하나의 노드가 리더(마스터)이고, 나머지는 팔로워(복제본)다. 모든 쓰기는 리더에만, 읽기는 리더나 팔로워 어디서든 가능. PostgreSQL, MySQL, MongoDB의 기본 모드.
flowchart TD
W[쓰기 요청] --> L[Leader]
L -->|로그 복제| F1[Follower 1]
L -->|로그 복제| F2[Follower 2]
L -->|로그 복제| F3[Follower 3]
R[읽기 요청] --> F1
R --> F2
R --> F3
동기 vs 비동기 복제. 리더가 팔로워의 확인을 기다리고 커밋하면 동기(강한 일관성, 느림), 기다리지 않으면 비동기(빠름, 일관성 약함). PostgreSQL의 synchronous_commit 설정이 이 조절.
// 비동기 복제의 전형적 흐름
class AsyncReplication {
void write(WriteRequest req) {
master.apply(req); // 마스터에 즉시 적용
log.append(req); // 로그에 append
// 커밋 — 팔로워 확인 안 기다림
return; // 클라이언트에 응답
// 팔로워에게는 백그라운드에서 비동기 전파
}
}
// 동기 복제
class SyncReplication {
void write(WriteRequest req) {
master.apply(req);
for (Follower f : followers) {
f.sendAndWaitAck(req); // 팔로워 확인 기다림
}
commit(); // 모든 팔로워 확인 후 커밋
}
}
동기 복제는 일관성이 강하지만 — 한 팔로워가 느리면 전체가 느려진다. 그래서 실제로는 "반동기(semi-sync)" — 일정 수의 팔로워만 기다리는 형태가 흔하다. MySQL의 semi-sync, PostgreSQL의 synchronous replication이 이 형태.
2. 다중 리더 (Multi-Leader)
여러 노드가 리더 역할을 한다. 쓰기는 어느 리더에게나 가능하고, 리더들 사이에 서로 변경을 전파한다. 지리적으로 분산된 데이터센터, 오프라인 작업(모바일 앱)에 자연스럽다.
flowchart LR
C1[Client 쓰기] --> L1[Leader A]
C2[Client 쓰기] --> L2[Leader B]
L1 <-->|변경 전파| L2
L1 --> F1[Followers of A]
L2 --> F2[Followers of B]
장점 — 데이터센터 장애 시 다른 데이터센터가 서비스 지속, 쓰기 처리량 증가, 지연 감소(가까운 리더로 쓰기).
단점 — 쓰기 충돌. 두 리더가 같은 데이터를 동시에 다르게 쓰면 충돌이 발생. 해결 전략이 필요.
// 쓰기 충돌 — 두 사용자가 같은 문서를 동시 수정
class ConflictResolution {
// LWW (Last Write Wins) — 타임스탬프 늦은 쪽으로 덮어쓰기
Document resolveLWW(Document a, Document b) {
return a.modifiedAt().isAfter(b.modifiedAt()) ? a : b;
}
// 단점: 사용자 A의 수정이 사라짐
// 3-way merge — 공통 조상 기준 병합 (git과 비슷)
Document resolveMerge(Document base, Document a, Document b) {
return mergeAlgo.merge(base, a, b);
}
// 단점: 자동 병합 안 되면 사람이 개입
// CRDT — 자료구조 자체가 충돌 없음
GCounter resolveCRDT(GCounter a, GCounter b) {
return a.merge(b); // 항상 수렴
}
}
다중 리더는 충돌 해결 비용이 크기 때문에 — 단일 데이터센터 안에서는 잘 안 쓰이고, 지리적 분산이나 오프라인 동기화(예: Notion, Google Docs)에 주로 쓰인다.
3. 리더 없는 (Leaderless)
리더 개념 자체가 없다. 어느 노드에나 쓸 수 있고, 읽기도 여러 노드에서 동시에 읽어 최신을 고른다. Dynamo(DeCandia et al. 2007), Cassandra, Riak이 이 형태.
flowchart LR
C[Client 쓰기] -->|모든 복제본에| N1[Node 1]
C --> N2[Node 2]
C --> N3[Node 3]
R[Client 읽기] -->|여러 노드에서| N1
R --> N2
R --> N3
N1 -. 최신값 선택 .-> R
쿼럼 (Quorum). N개 복제본 중 W개에 쓰고 R개에서 읽으면, W + R > N을 만족할 때 읽기가 항상 최신 쓰기를 포함한다. 예: 5개 복제본에 3개 쓰기(W=3), 3개 읽기(R=3) → W+R=6 > 5 → 강한 일관성. W나 R을 줄이면 일관성은 약해지지만 성능은 올라간다.
class QuorumStore {
private final int N; // 복제본 수
private final int W; // 쓰기 정족수
private final int R; // 읽기 정족수
public void write(Key key, Value value) {
List<Node> targets = selectReplicas(key, N);
AtomicInteger acks = new AtomicInteger(0);
for (Node n : targets) {
n.writeAsync(key, value).thenRun(() -> {
if (acks.incrementAndGet() == W) {
// W개 확인 도달 — 쓰기 성공
}
});
}
// W개 ack 기다림
}
public Value read(Key key) {
List<Node> targets = selectReplicas(key, N);
List<VersionedValue> responses = new ArrayList<>();
for (Node n : targets) {
responses.add(n.read(key));
}
// R개 응답 모으면 가장 최신 버전 선택
return responses.stream()
.max(VersionedValue::compareTo)
.map(VersionedValue::value)
.orElse(null);
}
}
Dynamo 논문(DeCandia et al. 2007)이 이 패턴을 정립했고, (n, w, r) 조합이 일관성·지연·가용성 trade-off를 결정한다는 통찰을 줬다.
세 형태 비교
| 형태 | 쓰기 병목 | 충돌 가능 | 일관성 기본 | 복잡성 | 대표 시스템 |
|---|---|---|---|---|---|
| 단일 리더 | 리더 하나 | 없음 | 강한 (동기 시) | 낮음 | PostgreSQL, MySQL, MongoDB |
| 다중 리더 | 분산 | 있음 (해결 필요) | eventual | 중간 | MySQL Galera, CouchDB |
| 리더 없는 | 분산 | 있음 (쿼럼으로 회피) | eventual~강한 (W,R에 따라) | 높음 | Cassandra, Riak, DynamoDB |
복제 지연 문제
비동기 복제에서 발생하는 문제들. 앞서 도입부의 사례 같은 현상.
- Read-your-writes 위반 — 사용자가 쓴 데이터를 복제본에서 못 봄. 03편 참조.
- 복제 지연 증가 — 부하 시 마스터에서 복제본으로의 전파가 수 초 지연.
- 읽기 일관성 — 같은 데이터를 연속으로 읽었는데 다른 값이 나옴 (서로 다른 복제본에서 읽어서).
이 문제를 다루는 패턴:
- 읽기 일관성 강화 — 사용자가 쓰기를 한 뒤 일정 시간 마스터에서 읽기 (세션 고정).
- 쿼럼 읽기 — 리더 없는 시스템에서 충분한 수의 복제본에서 읽어 최신 보장.
- 동기 복제로 전환 — 일관성 필수 데이터는 동기 모드로. 성능 희생.
설계 사례 — 복제 토폴로지 설계
전자상거래 시스템의 데이터 종류에 따라 다른 복제 전략을 설계해 본다.
class ReplicationTopology {
// 주문 원장 — 단일 리더, 동기 복제 (강한 일관성 필수)
static class OrdersDB {
Topology topology = Topology.singleLeader()
.withReplicas(3)
.withSyncMode(SyncMode.SEMI_SYNC); // 2개 ack 대기
}
// 사용자 세션 — 단일 리더, 비동기 복제 + 세션 고정
static class SessionsDB {
Topology topology = Topology.singleLeader()
.withReplicas(5)
.withSyncMode(SyncMode.ASYNC)
.withReadConsistency(ReadConsistency.READ_YOUR_WRITES);
}
// 상품 조회수 — 리더 없는, eventual consistency
static class ViewCounterDB {
Topology topology = Topology.leaderless()
.withReplicas(5)
.withWriteQuorum(1) // W=1, 빠른 쓰기
.withReadQuorum(1); // R=1, 빠른 읽기
// 일관성 약하지만 카운터는 eventual로 충분
}
// 글로벌 캐시 — 다중 리더 (지역별 리더)
static class GlobalCache {
Topology topology = Topology.multiLeader()
.withLeaders(Map.of(
Region.US_EAST, 3,
Region.EU_WEST, 3,
Region.AP_NORTHEAST, 3
))
.withConflictResolution(ConflictStrategy.LWW); // 시간순
}
}
이 토폴로지에서 — 주문은 일관성 필수라 동기 단일 리더, 조회수는 빠른 응답이 중심이라 리더 없는 eventual로, 글로벌 캐시는 지역 분산이라 다중 리더로. 한 시스템 안에 세 가지 토폴로지가 공존한다.
복제 위반/오용 감지
| 신호 | 의미 |
|---|---|
| 사용자가 방금 쓴 데이터가 안 보임 | Read-your-writes 위반 — 비동기 복제 + 세션 고정 부재 |
| 읽기마다 값이 왔다 갔다 함 | 여러 복제본에서 무작위 읽기 — monotonic reads 위반 |
| 마스터 장애 시 데이터 손실 | 동기 복제 안 함 — failover 시 커밋 안 된 쓰기 유실 |
| 쓰기 충돌이 자동 해결 안 됨 | 다중 리더에서 충돌 해결 전략 부재 |
| 쿼럼 설정이 W+R ≤ N | 최신 읽기 보장 안 됨 — 일관성 약화 의도적이어야 |
| 복제 지연이 수 초~수 분 | 마스터 과부하 또는 네트워크 병목 |
복제와 합의의 관계
단일 리더 시스템의 핵심 질문 — "누가 리더인가". 리더가 죽으면 새 리더를 선출해야 한다. 이 선출이 합의(04편)의 대표적 사례다. Split-brain(두 리더가 동시에 존재)을 막기 위해 합의 알고리즘(Raft, Paxos)이 단일 리더 시스템에 내장돼 있다. etcd, Zookeeper, Consul이 이 역할을 한다. Kubernetes의 모든 클러스터 상태가 etcd에 저장되는 이유가 이것이다 — etcd가 Raft 기반 합의로 일관성을 보장한다.
리더 없는 시스템은 합의가 덜 필요하지만 — 쿼럼을 계산하고 버전 충돌을 다루는 자체적 복잡성이 있다. 두 접근 모두 "복제의 일관성을 어떻게 보장할까"라는 같은 질문에 대한 다른 답이다.
다음으로 — 분산 트랜잭션
복제는 단일 객체의 복사본을 다뤘다면, 분산 트랜잭션(07편)은 여러 객체에 걸친 원자적 변경을 다룬다. 주문 생성과 재고 감소와 결제가 모두 성공하거나 모두 실패해야 하는 상황 — 이걸 분산 환경에서 어떻게 보장하나. 2PC, Saga, outbox 패턴이 그 답이다. 복제 지연·쿼럼·일관성 모델은 모두 분산 트랜잭션의 복잡성을 키우는 요소다.
복제의 핵심 통찰 — 데이터 복사본을 두는 건 쉽지만, 복사본 사이의 일관성을 다루는 게 어렵다. 동기 복제는 일관성을 주지만 느리고, 비동기는 빠르지만 일관성이 약하다. 다중 리더는 유연하지만 충돌을 다뤄야 한다. 리더 없는은 가장 분산적이지만 쿼럼 계산이 복잡하다. 어느 쪽이 맞는지는 데이터의 성격(일관성 요구)과 시스템의 목표(가용성·지연)에 따라 다르다.
참고
- Kleppmann —
(O'Reilly, 2017), Ch.5 (복제) — 접근 2026-07-20 - DeCandia et al. — "Dynamo: Amazon's Highly Available Key-value Store" (SOSP 2007) — 접근 2026-07-20
- Lamport — "Time, Clocks, and the Ordering of Events in a Distributed System" (CACM, 1978), happens-before 원 제안 — 접근 2026-07-20
- Shapiro et al. — "Conflict-free Replicated Data Types" (SSS 2011), CRDT 원 논문 — 접근 2026-07-20
- PostgreSQL Documentation — "High Availability, Load Balancing, and Replication" (동기/비동기/스트리밍 복제) — 접근 2026-07-20
- MySQL Documentation — "Replication" (async/semi-sync, GTID) — 접근 2026-07-20
'Software Architecture > Distributed Systems' 카테고리의 다른 글
| Distributed Systems - 08. 멱등성·정확히 한 번 (0) | 2026.07.20 |
|---|---|
| Distributed Systems - 07. 분산 트랜잭션 (0) | 2026.07.20 |
| Distributed Systems - 05. 파티셔닝과 샤딩 (0) | 2026.07.20 |
| Distributed Systems - 04. 합의 알고리즘 (0) | 2026.07.20 |
| Distributed Systems - 03. 일관성 모델 (0) | 2026.07.20 |