같은 데이터를 여러 노드에 복사해 두는 구성. 가장 흔한 형태가 리더-팔로워다.
무엇을 얻나
-
가용성 — 리더가 죽어도 팔로워를 승격해 서비스를 잇는다
-
읽기 확장 — 읽기를 여러 팔로워로 분산한다
-
지역 지연 — 사용자 가까운 곳에 팔로워를 둔다
-
쓰기는 확장되지 않는다 — 쓰기는 리더 한 곳으로 모인다
마지막 줄이 중요하다 — 쓰기 확장은 샤딩의 몫이다.
세 가지 복제 방식
| 커밋 시점 | 안전 | 속도 | |
|---|---|---|---|
| 동기 | 팔로워 확인까지 기다린다 | 유실 없음 | 느림. 팔로워가 죽으면 쓰기가 막힐 수 있다 |
| 반동기 | 최소 한 팔로워의 수신까지 | 절충 | 절충 |
| 비동기 | 기다리지 않는다 | 리더 사망 시 미전송분 유실 | 빠름 |
-- PostgreSQL: 동기 복제 설정
ALTER SYSTEM SET synchronous_standby_names = 'standby1';
ALTER SYSTEM SET synchronous_commit = 'on'; -- remote_write / remote_apply 도 있다
-- MySQL: 반동기 플러그인
SET GLOBAL rpl_semi_sync_source_enabled = 1;
복제 지연 — "방금 쓴 글이 안 보여요"
- 사용자가 글을 쓴다 — → 리더에 저장
- 곧바로 목록을 조회한다 — → 팔로워로 라우팅
- 팔로워가 아직 못 받았다 — → 내 글이 없다
비동기 복제의 필연적 결과다. 해결책은 셋이다.
- ① 쓰기 직후 일정 시간은 리더에서 읽는다 (read-your-writes)
- ② 사용자별로 세션 동안 같은 노드에 붙인다
- ③ 쓰기 위치(LSN)를 들고 다니며 그만큼 따라온 팔로워에서만 읽는다
-- 지연을 감시한다
-- PostgreSQL
SELECT client_addr, state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;
-- MySQL
SHOW REPLICA STATUS\G -- Seconds_Behind_Source
페일오버의 함정 — split-brain
네트워크가 끊겨 팔로워가 "리더가 죽었다" 고 판단한다
→ 팔로워를 승격시킨다
→ 그런데 원래 리더는 살아 있었다
→ 리더가 둘. 양쪽에 서로 다른 쓰기가 쌓인다 (split-brain)
합의 알고리즘이나 펜싱(옛 리더를 확실히 차단) 없이 자동 페일오버를 켜면 언젠가 겪는다.
면접 함정
- ❌ "복제하면 쓰기도 빨라진다" → 쓰기는 리더 한 곳이다. 오히려 동기 복제면 느려진다.
- ❌ "복제본이 있으니 백업은 불필요" → 실수한
DELETE는 복제본에도 전파된다.
읽기 분산의 함정
// ❌ 쓰기 직후 읽기를 팔로워로 보내면 내 글이 안 보인다
repository.save(post); // 리더
List<Post> list = repository.findAll(); // 팔로워 — 아직 못 받았을 수 있다
// ✅ 같은 트랜잭션·요청 안에서는 리더로 고정한다
@Transactional // 쓰기 트랜잭션이면 리더로 라우팅된다
Spring의 @Transactional(readOnly = true)로 읽기 전용을 표시하면 라우팅 판단에 쓸 수 있다. 다만 "방금 쓴 것"만은 예외 처리가 필요하다.
논리 복제 — 부분만, 버전을 넘어
-- PostgreSQL 논리 복제: 테이블 단위로 고를 수 있고 메이저 버전이 달라도 된다
CREATE PUBLICATION pub_orders FOR TABLE orders, order_item;
-- 구독 쪽에서
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=primary dbname=mydb' PUBLICATION pub_orders;
메이저 버전 업그레이드를 무중단으로 하는 표준 수단이기도 하다 — 신버전에 논리 복제로 따라잡게 한 뒤 전환한다.