DB 복제와 샤딩의 핵심 구조
Replication은 데이터를 복사해 읽기 부하와 장애 대응을 분산하고, Sharding은 데이터를 나누어 쓰기 부하와 용량 한계를 넘는 방식이다. 이 둘은 Replica Lag, Hot Shard, CAP 선택처럼 분산 시스템의 성능과 일관성 문제로 이어진다.
Script Companion
오디오와 함께 스크립트 보기
- 01
서비스가 성장하면 단일 DB 서버는 결국 병목이 된다. 조회 쿼리는 DB CPU를 포화시키고, 쓰기 요청은 락 경합을 늘리며, 장애가 나면 데이터 손실과 서비스 중단이 함께 발생할 수 있다. 서버 사양을 올리는 수직 확장은 비용이 급격히 커지고 물리적 한계도 있다. 그래서 읽기와 장애를 분산하는 Replication, 데이터와 쓰기 부하를 나누는 Sharding이 필요해진다. 이 둘은 단순한 성능 기법이 아니라, 서비스가 커질 때 어떤 한계를 어디로 옮길지 정하는 구조적 선택이다.
- 02
Replication은 원본 DB인 Primary와 복사본인 Replica를 두는 구조다. 쓰기는 Primary에만 들어가고, 읽기는 Replica로 보낼 수 있다. Primary는 변경 내용을 MySQL의 Binary Log나 PostgreSQL의 WAL에 기록하고, Replica는 그 로그를 읽어 같은 변경을 적용한다. 데이터 파일 전체를 매번 복사하지 않고 로그를 재생하는 이유는 변경분만 다루기 위해서다. 수 GB에서 TB 규모의 파일을 통째로 옮기는 대신, 작은 로그를 네트워크로 스트리밍하면 복제가 더 현실적이고 빠르게 동작한다.
- 03
Replication Lag은 Primary의 변경이 Replica에 반영되기까지의 지연이다. PostgreSQL의 Streaming Replication에서는 sent_lsn, write_lsn, flush_lsn, replay_lsn 같은 LSN, 즉 Log Sequence Number가 단계별 위치를 나타낸다. sent_lsn과 write_lsn 차이는 네트워크 지연을, write_lsn과 flush_lsn 차이는 Replica 디스크 I/O 병목을, flush_lsn과 replay_lsn 차이는 CPU 병목을 보여준다. MySQL은 기본적으로 SQL 재실행이나 행 변경 재적용에 가까운 논리적 복제를 쓰고, PostgreSQL은 WAL 바이트 스트림을 재생하는 물리적 복제를 쓴다. 물리적 복제는 빠르고 일관성이 높지만 Major 버전이 다른 서버 간에는 사용할 수 없는 제약이 있다.
- 04
복제 방식은 동기와 비동기로 나뉜다. 동기 복제는 Replica 확인 후 커밋을 완료하므로 일관성이 강하고 장애 시 데이터 손실이 없지만, 네트워크 왕복을 기다려야 해서 느리다. 비동기 복제는 Primary 커밋 후 나중에 Replica로 전파하므로 빠르지만 Replica Lag과 최근 변경 손실 가능성이 있다. AWS RDS는 기본적으로 비동기 복제를 사용하고, Multi-AZ 배포는 동기 복제로 고가용성을 보장한다. 다만 Multi-AZ는 읽기 분산용 Replica가 아니라 Standby이므로, 읽기 분산에는 별도의 Read Replica가 필요하다. MySQL의 반동기 복제는 최소 1개의 Replica가 WAL을 수신했음을 확인한 뒤 커밋해 완전 동기와 완전 비동기 사이를 잡는다.
- 05
애플리케이션에서는 읽기와 쓰기를 분리해 Replication의 효과를 얻는다. TypeORM Replication에서는 find, findOne, createQueryBuilder 같은 읽기 동작이 기본적으로 slave, 즉 Replica를 사용하고, save, insert, update, delete 같은 쓰기 동작은 항상 master, 즉 Primary를 사용한다. Replica가 여러 개라면 라운드 로빈 방식으로 부하를 분산한다. 이 구조는 조회가 많은 서비스에서 특히 중요하다. API 응답이 느릴 때 읽기 쿼리가 Primary에 몰려 있는지 확인하고, 배치 작업이 대용량 데이터를 읽을 때 Read Replica를 사용하면 Primary의 쓰기 부하를 보호할 수 있다.
- 06
Sharding은 데이터 자체를 여러 DB 서버, 즉 Shard로 나누는 수평 분할이다. Range Sharding은 값의 범위로 나누기 때문에 2024년 1월 데이터처럼 범위 쿼리가 효율적이지만, 최신 데이터가 항상 마지막 Shard에 쌓이면 특정 Shard에 쓰기가 몰릴 수 있다. Hash Sharding은 해시값으로 분산해 데이터가 고르게 퍼지지만, 범위 쿼리에서는 모든 Shard를 봐야 할 수 있고 Shard 추가 시 재분배 문제가 생긴다. 그래서 Shard Key 선택이 핵심이다. 좋은 Shard Key는 높은 카디널리티를 가져야 하고, 트래픽이 균등해야 하며, 대부분의 쿼리에서 WHERE 조건에 포함되어야 한다.
- 07
Hot Shard는 Shard Key를 잘못 골라 특정 Shard에 요청이 집중되는 문제다. DynamoDB에서는 파티션 키가 Shard Key 역할을 하며, 단일 파티션당 최대 3,000 RCU와 1,000 WCU라는 하드 리밋이 있다. 테이블 전체 용량이 아무리 커도 하나의 파티션이 이 한계를 넘으면 쓰로틀링이 발생하고 요청 자체가 거부될 수 있다. timestamp를 파티션 키로 쓰면 모든 쓰기가 현재 시간대 파티션에 몰릴 수 있으므로, userId처럼 분산이 잘 되는 키가 필요하다. 하나의 파티션 키로 쓰기가 집중될 때는 랜덤 접미사를 붙이는 Write Sharding으로 여러 파티션에 부하를 나눌 수 있다.
- 08
Consistent Hashing은 Shard 수가 바뀔 때 데이터 이동을 최소화하기 위한 방식이다. 일반 Hash Sharding에서 user_id를 Shard 수로 나눈 나머지를 쓰면, Shard가 3개에서 4개로 늘어나는 순간 대부분의 데이터 매핑이 달라진다. Consistent Hashing은 해시 링 위에 Shard를 배치하고, 데이터가 자신의 위치에서 시계 방향으로 가장 가까운 서버에 저장되게 한다. DynamoDB와 Cassandra가 내부적으로 이 원리를 쓰는 이유도 Shard나 파티션 추가 시 재분배를 줄이기 위해서다. 다만 물리 노드만 링에 놓으면 통계적으로 불균등할 수 있어 Cassandra는 노드당 기본 256개의 가상 노드를 사용한다.
- 09
Consistent Hashing도 만능은 아니다. 가상 노드가 없으면 노드 수가 적을수록 링 위 위치가 불균등해져 특정 노드에 데이터가 몰릴 수 있다. 가상 노드 수가 충분하면 분산이 안정되지만, Access Skew는 가상 노드로 해결되지 않는다. 특정 키에 트래픽이 폭발적으로 몰리면 그 키를 담당하는 노드 하나가 CPU 100%로 다운되고, 링의 특성상 다음 노드로 부하가 넘어가 연쇄 장애가 발생할 수 있다. 이때는 핫 키 전용 읽기 복제본이나 애플리케이션 레벨 캐시 Redis가 필요하다. 노드 제거 시에도 제거된 노드의 범위가 다음 노드로 몰리므로 미리 용량을 확보해야 한다.
- 10
Vertical Partitioning은 행을 나누는 Sharding과 달리 컬럼을 기준으로 테이블을 분리한다. 도서관에서 책의 기본 정보와 상세 정보를 다른 선반에 두는 것처럼, 자주 쓰는 컬럼과 큰 텍스트 또는 JSON 컬럼을 분리한다. 이렇게 하면 기본 조회에서 읽어야 하는 행 크기가 작아져 캐시 효율이 좋아지고, 큰 컬럼 때문에 발생하는 I/O를 줄일 수 있다. 또 프로필이나 개인정보처럼 민감한 정보를 별도 테이블로 분리하면 접근 제어도 쉬워진다. 용량과 쓰기 부하를 여러 서버로 나누는 Sharding과 달리, 수직 분할은 한 테이블 안의 읽기 패턴과 컬럼 특성을 다루는 기법이다.
- 11
CAP Theorem은 Replication과 Sharding이 왜 단순한 확장 기술이 아닌지 보여준다. Consistency는 모든 노드가 동시에 같은 데이터를 반환하는 것이고, Availability는 일부 노드에 장애가 있어도 모든 요청이 응답을 받는 것이며, Partition Tolerance는 네트워크가 분리되어도 시스템이 계속 동작하는 것이다. 분산 시스템에서 네트워크 파티션은 언제든 발생할 수 있으므로 Partition Tolerance는 포기하기 어렵다. 결국 실무에서는 일관성 C와 가용성 A 중 어느 쪽을 우선할지 선택해야 한다. 네트워크 파티션 상황에서 두 Master가 독립적으로 같은 데이터를 수정하면 Split-Brain과 쓰기 충돌이 발생할 수 있으며, Multi-Master 복제가 잘 쓰이지 않는 이유도 여기에 있다.
- 12
AWS 환경에서는 RDS Read Replica와 Aurora Read Replica의 차이가 중요하다. RDS Read Replica는 Primary가 Binary Log를 보내고 Replica가 SQL을 재실행하는 비동기 구조라 Replica Lag이 수초에서 수분까지 날 수 있다. Aurora는 Primary와 Replica가 동일한 분산 스토리지, 6개 복사본과 3 AZ를 공유하므로 복제 지연이 구조적으로 작고 수십ms 이하로 설명된다. Aurora Global Database는 단일 클러스터를 여러 AWS 리전에 걸쳐 확장하며, Primary 리전의 쓰기를 보조 리전에 1초 이내, 일반적으로 약 100ms로 복제한다. 재해 복구, 글로벌 읽기 성능, 규제 준수 같은 시나리오에서 이 차이가 선택 기준이 된다.
- 13
읽기 트래픽이 압도적으로 많은 서비스라면 Sharding보다 Read Replica 확장이 먼저일 수 있다. 문서의 OpenAI 사례는 단일 PostgreSQL Primary와 약 50개의 Read Replica로 ChatGPT 8억 명의 트래픽을 처리하고, 트래픽의 95%가 읽기 요청이라는 전제를 강조한다. 읽기 95%, 쓰기 5% 구조에서는 Primary를 크게 키우는 것보다 Replica를 늘리는 편이 비용 대비 효과가 클 수 있다. 다만 Replica Fan-out은 Primary 1대가 여러 Replica에 WAL을 동시에 스트리밍한다는 뜻이므로, Replica 수가 늘수록 Primary I/O 부담도 커진다. 그래서 Cascading Replication처럼 Primary에서 중간 Replica, 다시 하위 Replica로 이어지는 계층 구조가 확장 포인트가 된다.
- 14
운영에서는 지표와 장애 모드를 함께 봐야 한다. AWS RDS에서 Logical Replication, 예를 들어 DMS나 Debezium을 쓸 때 슬롯이 WAL을 계속 보존하면 OldestReplicationSlotLag가 증가하고 디스크가 가득 찰 위험이 있다. Aurora PostgreSQL에서 Debezium이나 DMS를 사용할 때 Write-Through Cache 기능인 rds.logical_wal_cache 파라미터는 WAL 디코딩 중 반복적인 스토리지 I/O를 캐시로 대체해 복제 Lag을 최대 17배 개선할 수 있다. CDC Lag이 문제라면 이 값을 기본 64MB에서 최대 2GB까지 늘리는 것이 첫 번째 튜닝 포인트로 제시된다. CloudWatch뿐 아니라 RDS Performance Insights도 Sharding과 Replication 문제를 정밀하게 진단하는 도구가 된다.
- 15
트러블슈팅에서는 증상보다 원인을 단계별로 좁히는 것이 중요하다. Replica Lag은 Primary에 쓰기가 완료됐지만 Replica에 아직 반영되지 않은 상태이며, 네트워크 지연, Replica I/O 병목, CPU 부족, Primary의 대량 쓰기, 장시간 Access Exclusive Lock이 원인이 될 수 있다. Hot Shard는 Shard Key 설계 문제로 특정 값이 하나의 Shard에 몰릴 때 발생한다. Re-sharding 문제는 Hash Sharding에서 Shard 수가 바뀌면 매핑이 달라져 데이터 재분배가 필요해지는 현상이다. Replication Slot이 남아 있거나 Replica가 오래 끊기면 WAL이 삭제되지 않고 쌓여 디스크를 채울 수 있으므로, CDC 슬롯 장애도 반드시 확인해야 한다.
- 16
이 문서의 핵심 원리는 다른 분산 시스템으로도 이어진다. PostgreSQL WAL의 LSN은 어디까지 처리했는가를 나타내는 단조 증가 포인터이고, Kafka의 파티션 Offset도 같은 역할을 한다. Debezium CDC가 PostgreSQL WAL을 읽어 Kafka에 쓸 때 LSN을 Kafka 메시지 메타데이터로 포함하면, Consumer가 장애 후 재시작해 마지막 커밋한 LSN부터 재처리할 수 있다. Consistent Hashing의 목적은 노드 추가와 제거 시 재분배를 줄이는 것이고, Redis Cluster는 이를 16,384개의 해시 슬롯으로 구현한다. 정리하면 Replication은 읽기 부하와 장애를 분산하고, Sharding은 쓰기 부하와 용량을 분산하며, CAP은 그 선택들이 갖는 일관성과 가용성의 제약을 설명한다.
같은 레이어