DB Replication & Sharding
분류: Layer 8 - 데이터베이스 심화
DB Replication & Sharding
섹션 제목: “DB Replication & Sharding”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”데이터베이스를 여러 서버에 복제(Replication) 하거나 여러 조각으로 분할(Sharding) 하여 단일 DB의 한계를 극복하는 수평 확장 전략이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”서비스가 성장하면 단일 DB 서버는 반드시 병목이 된다. 트래픽이 폭발적으로 증가할 때 단순히 서버 사양을 올리는 것(수직 확장)은 비용이 기하급수적으로 증가하고, 결국 물리적 한계에 부딪힌다. 이 문제를 해결하는 방법이 수평 확장이다.
실제로 발생하는 문제들:
- 사용자가 늘어날수록 조회 쿼리가 DB CPU를 포화시킨다
- 쓰기 요청이 많아지면 락(Lock) 경합으로 응답 시간이 늘어난다
- 장애 시 데이터 손실과 서비스 중단이 동시에 발생한다
Replication이 해결하는 것:
- 읽기 트래픽을 여러 서버로 분산 (읽기 성능 향상)
- Primary 장애 시 Replica가 대신 서비스 (가용성 향상)
- 데이터 복사본 유지 (재해 복구)
Sharding이 해결하는 것:
- 데이터 자체가 너무 커서 한 서버에 담을 수 없을 때 (수평 확장)
- 쓰기 트래픽 자체를 여러 서버로 분산
3. 핵심 개념
섹션 제목: “3. 핵심 개념”3-1. Replication: 데이터 복제
섹션 제목: “3-1. Replication: 데이터 복제”비유로 이해하기
섹션 제목: “비유로 이해하기”회사에 중요한 서류가 있다고 상상해보자. 원본(Primary)은 팀장 책상에 있고, 복사본(Replica)은 다른 직원들 책상에 놓여있다. 내용을 읽을 때는 어느 책상에서든 복사본을 꺼내볼 수 있다. 하지만 내용을 수정할 때는 반드시 팀장 책상의 원본에만 해야 하고, 수정된 내용이 자동으로 복사본들에 전파된다.
원리: Primary-Replica (Master-Slave) 구조
섹션 제목: “원리: Primary-Replica (Master-Slave) 구조”┌─────────────────────────────────────────────┐│ 애플리케이션 │└──────────┬───────────────────┬───────────────┘ │ 쓰기 (INSERT/UPDATE/DELETE) │ 읽기 (SELECT) ▼ ▼ ┌─────────────┐ ┌──────────────────────┐ │ Primary │ ──복제──► │ Replica 1, 2, 3... │ │ (Master) │ │ (Read Only) │ └─────────────┘ └──────────────────────┘동작 순서:
- 클라이언트가 Primary에 쓰기 요청 (INSERT, UPDATE, DELETE)
- Primary가 변경 내용을 Binary Log (MySQL) 또는 WAL (PostgreSQL)에 기록
- Replica가 로그를 읽어서 동일한 쿼리를 자신에게 적용
- 클라이언트의 읽기 요청은 Replica로 라우팅
왜 이렇게 동작하는가 — 로그 기반 복제의 원리
섹션 제목: “왜 이렇게 동작하는가 — 로그 기반 복제의 원리”왜 Replica는 Primary의 데이터 파일을 직접 복사하지 않고 로그를 재생하는 방식을 쓸까? 데이터 파일 전체를 복사하면 수 GB~TB 규모의 I/O가 매번 발생한다. 반면 WAL(Write-Ahead Log)이나 Binary Log는 변경분만 기록하므로 크기가 작고, 네트워크를 통해 스트리밍하기에 적합하다. 이것은 git이 전체 소스를 복사하지 않고 diff(변경분)만 전송하는 것과 같은 원리다.
PostgreSQL의 물리적 복제 (Streaming Replication) 내부 흐름:
Primary 서버 내부:1. 트랜잭션 실행 → WAL 버퍼에 변경 기록2. COMMIT 시 WAL 버퍼 → pg_wal/ 디렉토리에 flush (디스크 기록)3. WAL Sender 프로세스가 네트워크로 WAL 레코드 전송
Replica 서버 내부:4. WAL Receiver 프로세스가 WAL 레코드 수신5. 수신한 WAL을 pg_wal/에 기록 (write_lsn)6. flush 완료 (flush_lsn)7. Startup 프로세스가 WAL을 순서대로 재생(replay) → 데이터 파일에 적용 (replay_lsn)이 과정에서 sent_lsn, write_lsn, flush_lsn, replay_lsn 네 가지 LSN(Log Sequence Number)이 추적된다. 각 단계 사이의 차이가 곧 Replication Lag의 원인이다. 네트워크 지연은 sent_lsn - write_lsn 차이로 나타나고, Replica의 디스크 I/O 병목은 write_lsn - flush_lsn 차이로 나타나며, CPU 병목은 flush_lsn - replay_lsn 차이로 나타난다.
📖 더 보기: Understanding and Reducing PostgreSQL Replication Lag — LSN 기반 Lag 분석과 원인별 해결 전략 (pgEdge, 중급)
MySQL의 논리적 복제 (Binary Log Replication) 내부 흐름:
Primary 서버:1. 트랜잭션 실행 → Binary Log에 SQL 또는 행 변경(ROW) 기록2. Binlog Dump Thread가 Replica로 이벤트 전송
Replica 서버:3. I/O Thread가 이벤트 수신 → Relay Log에 기록4. SQL Thread가 Relay Log를 읽어 쿼리 실행 (재생)MySQL은 기본적으로 논리적 복제 (SQL 재실행 또는 행 변경 재적용)를 사용하는 반면, PostgreSQL은 물리적 복제 (WAL 바이트 스트림 그대로 재생)를 사용한다. 물리적 복제가 더 빠르고 일관성이 높지만 Major 버전이 다른 서버 간에는 사용할 수 없는 제약이 있다.
동기 복제 vs 비동기 복제
섹션 제목: “동기 복제 vs 비동기 복제”| 구분 | 동기 복제 | 비동기 복제 |
|---|---|---|
| 동작 방식 | Replica 확인 후 커밋 완료 | Primary 커밋 후 나중에 Replica 전파 |
| 일관성 | 강함 (항상 최신 데이터) | 약함 (Replica Lag 발생 가능) |
| 성능 | 느림 (네트워크 왕복 대기) | 빠름 (대기 없음) |
| 장애 시 | 데이터 손실 없음 | 최근 변경 일부 손실 가능 |
| 사용 예 | 금융 거래, 결제 시스템 | 일반 웹 서비스, 소셜 미디어 |
AWS RDS는 기본적으로 비동기 복제를 사용한다. 단, Multi-AZ 배포는 동기 복제를 사용해 고가용성을 보장한다. (Multi-AZ는 Replica가 아니라 Standby이므로 읽기 분산에는 별도로 Read Replica를 만들어야 한다.)
반동기 복제 (Semi-Synchronous Replication):
완전 동기와 완전 비동기의 중간 지점으로, MySQL에서 지원하는 방식이다. Primary는 최소 1개의 Replica가 WAL을 수신(write)했음을 확인한 후 커밋을 완료한다. Replica가 replay까지 완료할 필요는 없으므로 완전 동기보다 빠르면서도 데이터 손실 가능성을 크게 줄인다.
완전 동기: Primary COMMIT → Replica replay 완료 확인 → 응답 (가장 느림, 데이터 손실 0)반동기: Primary COMMIT → Replica write 확인 → 응답 (중간)완전 비동기: Primary COMMIT → 즉시 응답 → Replica에 나중에 전파 (가장 빠름, 데이터 손실 가능)📖 더 보기: Troubleshoot replication lags in RDS for PostgreSQL — AWS 공식 Replication Lag 트러블슈팅 가이드 (AWS re:Post, 입문~중급)
Multi-Master 복제
섹션 제목: “Multi-Master 복제”모든 노드가 쓰기를 허용하는 구조다. 여러 곳에서 동시에 같은 데이터를 수정하면 쓰기 충돌(Write Conflict) 이 발생한다. 해결이 복잡하기 때문에 잘 사용하지 않는다.
노드 A: user_id=1 의 name을 "Alice"로 변경노드 B: user_id=1 의 name을 "Bob"으로 변경 (동시에)→ 최종 값은 "Alice"? "Bob"? 누가 결정하는가?충돌 해결 방법에는 타임스탬프 기반 Last-Write-Wins, 애플리케이션 레벨 병합 등이 있으나 모두 복잡성을 수반한다.
AWS RDS Read Replica 설정
섹션 제목: “AWS RDS Read Replica 설정”# AWS CLI로 Read Replica 생성aws rds create-db-instance-read-replica \ --db-instance-identifier mydb-replica \ --source-db-instance-identifier mydb-primary \ --db-instance-class db.t3.medium \ --availability-zone ap-northeast-2b
# 예상 출력{ "DBInstance": { "DBInstanceIdentifier": "mydb-replica", "DBInstanceStatus": "creating", "ReadReplicaSourceDBInstanceIdentifier": "mydb-primary", "Endpoint": { "Address": "mydb-replica.xxxx.ap-northeast-2.rds.amazonaws.com", "Port": 5432 } }}TypeORM으로 읽기/쓰기 분리 구현 (NestJS)
섹션 제목: “TypeORM으로 읽기/쓰기 분리 구현 (NestJS)”// app.module.ts - TypeORM Replication 설정import { TypeOrmModule } from "@nestjs/typeorm";
@Module({ imports: [ TypeOrmModule.forRoot({ type: "postgres", replication: { master: { host: process.env.DB_MASTER_HOST, // Primary 엔드포인트 port: 5432, username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, }, slaves: [ { host: process.env.DB_REPLICA1_HOST, // Replica 1 엔드포인트 port: 5432, username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, }, { host: process.env.DB_REPLICA2_HOST, // Replica 2 엔드포인트 port: 5432, username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, }, ], }, entities: [__dirname + "/**/*.entity{.ts,.js}"], synchronize: false, }), ],})export class AppModule {}// user.service.ts - 읽기/쓰기 자동 분리import { InjectRepository } from "@nestjs/typeorm";import { Repository } from "typeorm";import { User } from "./user.entity";
@Injectable()export class UserService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {}
// SELECT → 자동으로 Replica로 라우팅 async findAll(): Promise<User[]> { return this.userRepository.find(); // TypeORM이 내부적으로 slave(Replica) 연결을 사용 }
// INSERT → 자동으로 Primary로 라우팅 async create(userData: CreateUserDto): Promise<User> { const user = this.userRepository.create(userData); return this.userRepository.save(user); // TypeORM이 내부적으로 master(Primary) 연결을 사용 }
// 명시적으로 Master에서 읽기 (최신 데이터가 필요한 경우) async findOneConsistent(id: number): Promise<User> { return this.userRepository .createQueryBuilder("user") .setQueryRunner( await this.userRepository.manager.connection.createQueryRunner( "master", ), // 명시적으로 master 지정 ) .where("user.id = :id", { id }) .getOne(); }}TypeORM Replication 동작 원리:
find(),findOne(),createQueryBuilder()→ 기본적으로 slave(Replica) 사용save(),insert(),update(),delete()→ 항상 master(Primary) 사용- slave가 여러 개면 라운드 로빈 방식으로 부하를 분산한다
3-2. Sharding: 수평 분할
섹션 제목: “3-2. Sharding: 수평 분할”비유로 이해하기
섹션 제목: “비유로 이해하기”학교에서 학생을 반으로 나눠 배정한다고 생각해보자.
이름 ㄱ~ㅁ → 1반 (Shard 1)이름 ㅂ~ㅈ → 2반 (Shard 2)이름 ㅊ~ㅎ → 3반 (Shard 3)이름의 첫 글자(이것이 Shard Key)에 따라 어느 반(Shard)에 들어가는지 결정된다. 선생님(애플리케이션)은 특정 학생을 찾을 때 이름의 첫 글자를 보고 어느 반으로 가면 되는지 바로 알 수 있다.
원리: Sharding 구조
섹션 제목: “원리: Sharding 구조”┌─────────────────────────────────────────┐│ 애플리케이션 ││ (Shard Key로 어느 DB인지 결정) │└────────────┬──────────┬─────────────────┘ │ │ │ user_id │ user_id│ user_id │ 1~100만 │ 100~200만 200~300만 ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ │Shard 1 │ │Shard 2 │ │Shard 3 │ │DB서버 │ │DB서버 │ │DB서버 │ └────────┘ └────────┘ └────────┘Range Sharding vs Hash Sharding
섹션 제목: “Range Sharding vs Hash Sharding”Range Sharding (범위 기반 분할)
-- user_id 기준으로 범위 분할-- Shard 1: user_id 1 ~ 1,000,000-- Shard 2: user_id 1,000,001 ~ 2,000,000-- Shard 3: user_id 2,000,001 ~ 3,000,000
-- 애플리케이션 로직function getShardId(userId: number): number { if (userId <= 1_000_000) return 1; if (userId <= 2_000_000) return 2; return 3;}장점: 범위 쿼리가 효율적 (2024년 1월 데이터 모두 조회 등) 단점: 새로운 데이터가 특정 Shard에 집중 (최신 데이터가 항상 마지막 Shard에만 쌓임)
Hash Sharding (해시 기반 분할)
// user_id의 해시값으로 Shard 결정function getShardId(userId: number, totalShards: number): number { return userId % totalShards;}
// user_id=1005 → 1005 % 3 = 0 → Shard 0// user_id=1006 → 1006 % 3 = 1 → Shard 1// user_id=1007 → 1007 % 3 = 2 → Shard 2장점: 데이터가 고르게 분산됨 단점: 범위 쿼리 불가 (1~100번 사용자 조회 시 모든 Shard 탐색), Shard 추가 시 재분배 필요
Shard Key 선택이 중요한 이유 — Hot Shard 문제
섹션 제목: “Shard Key 선택이 중요한 이유 — Hot Shard 문제”Shard Key를 잘못 선택하면 특정 Shard에 요청이 집중되는 Hot Shard 문제가 발생한다.
예시: 소셜 미디어 서비스에서 user_id로 Sharding→ 팔로워 1000만 명인 유명인의 게시물 요청이 모두 같은 Shard로 집중→ 해당 Shard만 CPU 100%, 나머지 Shard는 여유 있음좋은 Shard Key의 조건:
- 높은 카디널리티: 다양한 값이 존재해야 함 (boolean은 최악)
- 균등 분포: 특정 값에 트래픽이 몰리지 않아야 함
- 쿼리 정렬: 대부분의 쿼리에서 Shard Key가 WHERE 조건에 포함되어야 함
// 나쁜 Shard Key 예시const badShardKey = "is_premium_user"; // true/false 2개 값만 존재
// 좋은 Shard Key 예시const goodShardKey = "user_id"; // 수백만 개의 다양한 값const betterShardKey = "hash(user_id)"; // 더욱 균등한 분포왜 Shard Key 설계가 모든 것을 결정하는가 — DynamoDB의 파티션 한계
섹션 제목: “왜 Shard Key 설계가 모든 것을 결정하는가 — DynamoDB의 파티션 한계”각 파티션(Shard)에는 물리적 처리량 한계가 있다. DynamoDB의 경우 단일 파티션당 최대 3,000 RCU(Read Capacity Units)와 1,000 WCU(Write Capacity Units) 라는 하드 리밋이 존재한다. 테이블 전체의 프로비저닝 용량이 아무리 커도 하나의 파티션이 이 한계를 넘으면 쓰로틀링(Throttling) 이 발생한다. 이것이 Hot Partition 문제가 단순히 “느려지는” 수준이 아니라 요청 자체가 거부되는 심각한 문제인 이유다.
Write Sharding 기법 — Hot Partition 해결:
하나의 파티션 키로 쓰기가 집중되는 경우, 키 뒤에 랜덤 접미사를 붙여 여러 파티션으로 분산하는 기법이다.
// Write Sharding: 파티션 키에 랜덤 접미사 추가const SHARD_COUNT = 10;
function writeWithSharding(item: { eventDate: string; data: any }) { const shardSuffix = Math.floor(Math.random() * SHARD_COUNT); const shardedKey = `${item.eventDate}#${shardSuffix}`; // eventDate="2026-04-07" → "2026-04-07#3", "2026-04-07#7" 등으로 분산
return dynamoDB.putItem({ TableName: "DailyEvents", Item: { pk: { S: shardedKey }, // 10개 파티션에 분산 data: { S: JSON.stringify(item.data) }, }, });}
// 읽기 시에는 모든 샤드를 조회 후 합산async function readAllShards(eventDate: string): Promise<any[]> { const promises = Array.from({ length: SHARD_COUNT }, (_, i) => dynamoDB.query({ TableName: "DailyEvents", KeyConditionExpression: "pk = :pk", ExpressionAttributeValues: { ":pk": { S: `${eventDate}#${i}` } }, }), ); const results = await Promise.all(promises); return results.flatMap((r) => r.Items ?? []);}📖 더 보기: Using write sharding to distribute workloads evenly — AWS 공식 문서 — DynamoDB Write Sharding 공식 가이드 (입문)
📖 더 보기: How to Handle DynamoDB Hot Partitions — Hot Partition 진단 및 해결 전략 실전 가이드 (2026, 중급)
Consistent Hashing — Shard 추가 시 데이터 이동 최소화
섹션 제목: “Consistent Hashing — Shard 추가 시 데이터 이동 최소화”일반 Hash Sharding의 치명적 단점은 Shard 수가 바뀌면 거의 모든 데이터를 재분배해야 한다는 것이다. user_id % 3으로 분산하다가 Shard를 4개로 늘리면 user_id % 4 계산으로 대부분의 데이터가 다른 Shard로 이동해야 한다.
Consistent Hashing(일관적 해싱) 은 이 문제를 해결한다.
비유: 시계 모양의 원형 링(해시 링)을 상상해보자. 0~360도 사이에 각 Shard 서버를 배치하고, 데이터는 자신의 해시값에 해당하는 위치에서 시계 방향으로 가장 가까운 서버에 저장된다.
원리:
해시 링 (0 ~ 2^32 범위를 원으로 표현)
Shard A (90도) |Shard D ─────┼───── Shard B (180도)(270도) | | Shard C (270도 아래)
data_1 → hash = 120도 → 시계방향 첫 Shard = Shard Bdata_2 → hash = 300도 → 시계방향 첫 Shard = Shard A (360→0→90)Shard 추가 시 영향:
기존 3개 Shard로 운영 중 → Shard E를 120~180도 사이에 추가→ 120~180도 범위의 데이터만 Shard B → Shard E로 이동→ 전체 데이터의 약 1/4만 이동 (일반 Hash는 3/4 이동)DynamoDB와 Cassandra가 Consistent Hashing을 내부적으로 사용하는 이유가 바로 이것이다. Shard(파티션) 추가 시 데이터 재분배를 최소화할 수 있다.
📖 더 보기: Consistent Hashing for System Design Interviews — 해시 링의 동작 원리를 시각적으로 설명 (Hello Interview, 입문)
AWS RDS Read Replica 모니터링 — OldestReplicationSlotLag
섹션 제목: “AWS RDS Read Replica 모니터링 — OldestReplicationSlotLag”AWS RDS에서 Logical Replication(예: DMS, Debezium)을 사용할 때, 슬롯이 WAL을 계속 보존하면 OldestReplicationSlotLag 메트릭이 증가한다. 이 값이 급증하면 디스크가 가득 찰 위험이 있다.
# AWS CLI로 OldestReplicationSlotLag 모니터링aws cloudwatch get-metric-statistics \ --namespace AWS/RDS \ --metric-name OldestReplicationSlotLag \ --dimensions Name=DBInstanceIdentifier,Value=mydb-primary \ --start-time 2026-04-07T00:00:00Z \ --end-time 2026-04-08T00:00:00Z \ --period 300 \ --statistics Maximum
# 예상 출력 (슬롯 lag이 증가 중인 경우):# {# "Datapoints": [# {"Timestamp": "...", "Maximum": 524288000.0}, ← 500MB# {"Timestamp": "...", "Maximum": 1073741824.0}, ← 1GB (빠르게 증가)# ]# }# → 이 값이 수 GB 이상이면 즉시 슬롯 상태 확인 필요AWS 콘솔에서 알람 설정 권장:
CloudWatch → 경보 → 경보 생성메트릭: AWS/RDS > DBInstance > OldestReplicationSlotLag조건: > 5,368,709,120 (5GB)작업: SNS → 이메일/Slack 알림→ 슬롯 WAL이 5GB 초과 시 즉시 알림받아 조치📖 더 보기: Best practices for Amazon RDS PostgreSQL replication — AWS Blog — RDS 복제 설정 모범 사례와 OldestReplicationSlotLag 모니터링 전략 (AWS 공식, 입문~중급)
PostgreSQL 16의 Logical Replication 개선 — 제로 다운타임 샤딩
섹션 제목: “PostgreSQL 16의 Logical Replication 개선 — 제로 다운타임 샤딩”PostgreSQL 16부터 스트리밍 Replica 위에서도 Logical Replication Slot 생성이 가능해졌다. 이 기능 덕분에 대규모 데이터 마이그레이션이나 샤딩 작업 시 기존에 수 주씩 걸리던 작업을 수 분~수 시간으로 단축할 수 있게 됐다.
비유: 기존에는 공장(Primary)을 멈춰야만 새 창고(Shard)로 짐을 옮길 수 있었다. 이제는 창고의 직원(Replica Logical Slot)이 새 창고로 짐을 옮기는 동안 공장은 계속 돌아간다.
제로 다운타임 Sharding 마이그레이션 흐름:
단계 1: 새 Shard DB 준비 (기존 서비스 계속 운영)단계 2: Logical Replication으로 데이터 실시간 복제 시작 Primary → Shard 1 (user_id 1~100만) Primary → Shard 2 (user_id 100~200만)단계 3: 복제 lag이 0에 가까워지면 (수십ms 이내)단계 4: 애플리케이션 Shard 라우팅 코드 배포 (수초 중단)단계 5: 기존 Primary에서 이전된 데이터 삭제
→ 전체 서비스 중단 없이 샤딩 완료# PostgreSQL 16 Logical Replication으로 Sharding 마이그레이션 예시# Primary에서 Publication 생성psql -h primary -c "CREATE PUBLICATION shard1_pubFOR TABLE usersWHERE (user_id <= 1000000); -- Shard 1 범위만 복제"
# Shard 1 DB에서 Subscription 생성psql -h shard1 -c "CREATE SUBSCRIPTION shard1_subCONNECTION 'host=primary dbname=mydb user=replicator'PUBLICATION shard1_pub;"
# 복제 상태 확인psql -h shard1 -c "SELECT * FROM pg_stat_subscription;"# subname | received_lsn | last_msg_receipt_time | latest_end_lsn | latest_end_time# shard1_sub | 0/5A3F210 | 2026-04-07 10:23:45.1 | 0/5A3F210 | ...# received_lsn ≈ latest_end_lsn 이면 거의 동기화 완료실무 연결: DynamoDB 파티션 키
섹션 제목: “실무 연결: DynamoDB 파티션 키”AWS DynamoDB는 내부적으로 Sharding을 자동으로 처리한다. 개발자가 지정하는 파티션 키(Partition Key) 가 바로 Shard Key의 역할을 한다.
// DynamoDB 테이블 생성 - AWS SDK v3 (NestJS 환경)import { DynamoDB } from "@aws-sdk/client-dynamodb";
const client = new DynamoDB({ region: "ap-northeast-2" });
await client.createTable({ TableName: "UserActivity", KeySchema: [ { AttributeName: "userId", KeyType: "HASH" }, // 파티션 키 = Shard Key { AttributeName: "timestamp", KeyType: "RANGE" }, // 정렬 키 ], AttributeDefinitions: [ { AttributeName: "userId", AttributeType: "S" }, { AttributeName: "timestamp", AttributeType: "S" }, ], BillingMode: "PAY_PER_REQUEST",});주의: DynamoDB에서도 Hot Partition 문제가 발생할 수 있다. 예를 들어 timestamp를 파티션 키로 설정하면 모든 쓰기가 현재 시간대 파티션에 집중된다. 반드시 userId처럼 분산이 잘 되는 키를 파티션 키로 사용해야 한다.
3-3. Vertical Partitioning: 수직 분할
섹션 제목: “3-3. Vertical Partitioning: 수직 분할”비유로 이해하기
섹션 제목: “비유로 이해하기”도서관에서 책의 기본 정보(제목, 저자, 출판연도)와 상세 정보(내용 요약, 리뷰, 전체 목차)를 서로 다른 선반에 보관한다. 대부분의 조회는 기본 정보만 필요하므로 빠르게 접근할 수 있다. 상세 정보가 필요한 경우에만 다른 선반으로 간다.
변경 전: users 테이블 (모든 컬럼이 한 테이블)┌──────────────────────────────────────────────────────────┐│ id │ email │ name │ created_at │ bio │ avatar_url │ sns_links │ preferences_json │└──────────────────────────────────────────────────────────┘
변경 후: 수직 분할users 테이블 (자주 쓰는 핵심 정보)┌─────────────────────────────────────┐│ id │ email │ name │ created_at │└─────────────────────────────────────┘
user_profiles 테이블 (덜 쓰는 상세 정보)┌──────────────────────────────────────────────────────────┐│ user_id │ bio │ avatar_url │ sns_links │ preferences_json │└──────────────────────────────────────────────────────────┘실무 예시: NestJS + TypeORM 엔티티 분리
섹션 제목: “실무 예시: NestJS + TypeORM 엔티티 분리”// user.entity.ts - 자주 조회되는 기본 정보@Entity("users")export class User { @PrimaryGeneratedColumn() id: number;
@Column({ unique: true }) email: string;
@Column() name: string;
@CreateDateColumn() createdAt: Date;
// 관계: 프로필은 필요할 때만 로드 @OneToOne(() => UserProfile, (profile) => profile.user, { lazy: true }) profile: Promise<UserProfile>;}
// user-profile.entity.ts - 가끔 조회되는 상세 정보@Entity("user_profiles")export class UserProfile { @PrimaryColumn() userId: number;
@Column({ nullable: true, type: "text" }) bio: string;
@Column({ nullable: true }) avatarUrl: string;
@Column({ nullable: true, type: "jsonb" }) snsLinks: Record<string, string>;
@Column({ nullable: true, type: "jsonb" }) preferences: Record<string, unknown>;
@OneToOne(() => User, (user) => user.profile) @JoinColumn({ name: "userId" }) user: User;}// user.service.ts - 분리된 테이블 활용@Injectable()export class UserService { // 목록 조회: 기본 정보만 로드 (빠름) async findAll(): Promise<User[]> { return this.userRepository.find(); // SELECT id, email, name, created_at FROM users; }
// 프로필 페이지: 상세 정보까지 로드 (필요할 때만) async findWithProfile(id: number): Promise<User> { return this.userRepository.findOne({ where: { id }, relations: ["profile"], }); // SELECT * FROM users JOIN user_profiles ON ... }}수직 분할의 장점:
- 자주 쓰는 컬럼만 있는 테이블은 행 크기가 작아 캐시 효율이 높아진다
- 큰 텍스트/JSON 컬럼이 분리되어 기본 조회 I/O가 감소한다
- 민감한 정보(프로필, 개인정보)를 별도 테이블로 분리해 접근 제어가 쉬워진다
3-4. CAP Theorem
섹션 제목: “3-4. CAP Theorem”CAP Theorem의 정의, CP/AP 시스템 선택 기준, 실무 판단 프레임워크에 대한 상세 내용은 db-modeling.md 를 참고하세요. 이 문서에서는 Replication/Sharding 맥락에서의 적용에 집중합니다.
비유로 이해하기
섹션 제목: “비유로 이해하기”분산된 창고 시스템을 운영한다고 상상해보자. 서울 창고와 부산 창고가 있는데, 두 창고 사이의 통신이 끊겼다(네트워크 파티션). 이 상황에서 어떻게 할 것인가?
- 일관성(C) 선택: “두 창고 재고가 일치할 때만 주문 처리” → 부산 창고는 서울 연결될 때까지 모든 주문 거절 (가용성 포기)
- 가용성(A) 선택: “일단 부산 재고 기준으로 주문 처리” → 나중에 서울이랑 맞춰볼게 (일관성 포기, 재고 불일치 발생)
원리: 3가지 보장
섹션 제목: “원리: 3가지 보장” C (Consistency) 일관성 ╱ ╲ ╱ 선택 불가 ╲ ╱ (이론상) ╲ ╱ ╲A (Availability)────P (Partition Tolerance)가용성 파티션 내성| 속성 | 의미 |
|---|---|
| Consistency (일관성) | 모든 노드가 동시에 동일한 데이터를 반환한다 |
| Availability (가용성) | 일부 노드에 장애가 발생해도 모든 요청이 응답을 받는다 |
| Partition Tolerance (파티션 내성) | 네트워크가 분리되어도 시스템이 계속 동작한다 |
CAP 정리의 핵심: 분산 시스템에서 네트워크 파티션은 언제나 발생할 수 있다. 따라서 Partition Tolerance는 포기할 수 없고, 실질적으로 C와 A 중 하나를 선택해야 한다.
CP vs AP 선택
섹션 제목: “CP vs AP 선택”CP 시스템 (일관성 우선)
MongoDB, Redis Cluster, HBase, ZooKeeper
장애 시 동작:네트워크 분리 → 일관성 없는 노드에서 요청 거절 → 에러 반환→ "지금 당장 데이터가 맞는지 확신할 수 없으면 차라리 에러를 낸다"// Redis Cluster CP 예시 - 파티션 발생 시 에러 발생import { Redis } from "ioredis";
const redis = new Redis.Cluster([ { host: "redis-1", port: 6379 }, { host: "redis-2", port: 6379 }, { host: "redis-3", port: 6379 },]);
// 네트워크 파티션 시// CLUSTERDOWN Hash slot not served 에러 발생// → 데이터 불일치보다 에러가 낫다고 판단const value = await redis.get("key");AP 시스템 (가용성 우선)
DynamoDB, Cassandra, CouchDB
장애 시 동작:네트워크 분리 → 각 노드가 독립적으로 요청 처리 → 오래된 데이터 반환 가능→ "잘못된 데이터라도 일단 응답한다, 나중에 동기화한다"// DynamoDB AP 예시 - Eventually Consistent Readconst result = await dynamoDB .getItem({ TableName: "Products", Key: { productId: { S: "prod-123" } }, ConsistentRead: false, // Eventually Consistent (기본값, AP 방식) // ConsistentRead: true 이면 Strong Consistent (CP 방식, 비용 2배) }) .promise();실무 선택 기준
섹션 제목: “실무 선택 기준”우리 서비스에 맞는 DB를 어떻게 고르는가?
Q1: 데이터 불일치가 치명적인가?→ 결제, 재고, 금융 거래 → CP (PostgreSQL, MySQL with Replication)
Q2: 잠깐 오래된 데이터를 보여줘도 되는가?→ SNS 피드, 상품 카탈로그, 검색 → AP (DynamoDB, Cassandra)
Q3: 서비스 중단 vs 데이터 불일치 중 뭐가 더 나쁜가?→ 서비스 중단이 더 나쁨 → AP→ 데이터 불일치가 더 나쁨 → CP4. 실무에서 어떻게 쓰이나
섹션 제목: “4. 실무에서 어떻게 쓰이나”일반적인 아키텍처 패턴
섹션 제목: “일반적인 아키텍처 패턴”소규모 서비스 (MAU 1만 이하)└── 단일 RDS (PostgreSQL/MySQL)
중규모 서비스 (MAU 10만~100만)├── RDS Primary (쓰기)└── RDS Read Replica × 2 (읽기)
대규모 서비스 (MAU 100만 이상)├── Aurora Cluster (자동 복제, 최대 15개 Read Replica)├── ElastiCache Redis (캐시, Hot Data)└── DynamoDB (무한 확장 필요한 이벤트 로그 등)AWS Aurora vs RDS Read Replica 차이
섹션 제목: “AWS Aurora vs RDS Read Replica 차이”| 항목 | RDS Read Replica | Aurora Read Replica |
|---|---|---|
| 복제 방식 | 비동기 (Binary Log) | 공유 스토리지 (즉시 반영) |
| Replica Lag | 수초~수분 | 수십ms 이하 |
| Replica 최대 수 | 5개 | 15개 |
| Failover | 수동 또는 수분 | 자동, 30초 이내 |
왜 Aurora가 Lag이 훨씬 적은가: RDS Read Replica는 Primary가 Binary Log를 전송하면 Replica가 SQL을 재실행(논리적 복제)한다. Aurora는 Primary와 Replica가 동일한 분산 스토리지(6개 복사본, 3 AZ) 를 공유한다. Primary가 스토리지에 쓰면 Replica는 이미 같은 스토리지를 보고 있으므로 “복제” 지연이 구조적으로 최소화된다.
Aurora Global Database — 멀티 리전 복제
섹션 제목: “Aurora Global Database — 멀티 리전 복제”Aurora Global Database는 단일 클러스터를 여러 AWS 리전에 걸쳐 확장하는 기능이다. Primary 리전에서 쓰기가 발생하면 보조 리전에 1초 이내(일반적으로 ~100ms)로 복제된다.
Primary Region (ap-northeast-2, 서울)└── Aurora Cluster (쓰기 + 읽기) │ │ Cross-Region 복제 (~100ms, 스토리지 레벨) │Secondary Region (us-east-1, 버지니아)└── Aurora Cluster (읽기 전용) └── 장애 시 Write 승격 가능 (RPO < 1초, RTO < 1분)사용 시나리오:
- 재해 복구(DR): 서울 리전 전체 장애 시 버지니아 리전의 클러스터를 Primary로 승격
- 글로벌 읽기 성능: 미국 사용자는 미국 리전 Aurora에서 읽기 → 레이턴시 감소
- 규제 준수: 특정 리전의 데이터를 백업 리전에 실시간 복제
# AWS CLI로 Aurora Global Database 생성aws rds create-global-cluster \ --global-cluster-identifier my-global-cluster \ --source-db-cluster-identifier arn:aws:rds:ap-northeast-2:123456789:cluster:my-aurora
# 보조 리전에 클러스터 추가aws rds create-db-cluster \ --db-cluster-identifier my-aurora-us \ --global-cluster-identifier my-global-cluster \ --engine aurora-postgresql \ --region us-east-1
# 재해 복구 시 보조 클러스터를 Primary로 승격 (Managed Failover)aws rds failover-global-cluster \ --global-cluster-identifier my-global-cluster \ --target-db-cluster-identifier arn:aws:rds:us-east-1:123456789:cluster:my-aurora-us# → 약 1분 내에 us-east-1 클러스터가 쓰기를 받기 시작📖 더 보기: Amazon Aurora Global Database — AWS 공식 문서 — Cross-Region 복제 설정, Failover 절차, 성능 지표 (입문~중급)
실세계 사례: OpenAI가 PostgreSQL 하나로 8억 명을 처리하는 방법 (2026)
섹션 제목: “실세계 사례: OpenAI가 PostgreSQL 하나로 8억 명을 처리하는 방법 (2026)”OpenAI는 2026년 1월, 단일 PostgreSQL Primary + 약 50개의 Read Replica 구성으로 ChatGPT 8억 명의 트래픽을 처리하고 있음을 공개했다. 트래픽의 95%가 읽기 요청이라는 점이 이 아키텍처의 핵심 전제다.
핵심 설계 원칙:
- 쓰기 최소화: 읽기 95% / 쓰기 5% 비율 — 읽기 모델을 최대한 캐시에 가깝게 설계
- Replica Fan-out: Primary 1대가 50개 Replica에 동시 WAL 스트리밍 → Replica 수가 늘수록 Primary I/O 부담 증가
- Cascading Replication (개발 중): Primary → 중간 Replica → 하위 Replica 계층 구조로 Primary의 WAL 전송 부담을 분산. 100개 이상 Replica로 확장하기 위한 핵심 기술.
기존 구조 (Fan-out):Primary → Replica 1 → Replica 2 → ... → Replica 50 (Primary가 50개에 동시 스트리밍)
Cascading Replication (개발 중):Primary → 중간 Replica A → Replica 1, 2, 3 → 중간 Replica B → Replica 4, 5, 6→ Primary 부하를 중간 Replica가 분담실무 적용 포인트:
읽기 트래픽이 압도적으로 많은 서비스라면, Sharding보다 Read Replica 확장이 먼저다. 95% 읽기라면 Primary를 10배 키우는 것보다 Replica 10대를 추가하는 것이 비용 대비 효과가 크다.
📖 더 보기: Scaling PostgreSQL to power 800 million ChatGPT users — OpenAI — 단일 PostgreSQL Primary + 50 Read Replica로 8억 명을 처리한 OpenAI의 실전 아키텍처 공개 (2026, 중급)
Aurora PostgreSQL 논리 복제 Write-Through Cache
섹션 제목: “Aurora PostgreSQL 논리 복제 Write-Through Cache”Debezium/DMS 등 논리 복제 사용 시 Aurora PostgreSQL의 Write-Through Cache 기능(rds.logical_wal_cache 파라미터, 기본 64MB)을 활성화하면 WAL 디코딩 중 반복적인 스토리지 I/O를 캐시로 대체해 복제 Lag을 최대 17배 개선할 수 있다. Debezium + MSK(Kafka) 연동 시 CDC Lag이 문제가 된다면 이 파라미터를 최대 2GB까지 늘리는 것이 첫 번째 튜닝 포인트다. (Aurora PostgreSQL 14.5 / 13.8 / 12.12 이상 지원)
📖 더 보기: Achieve up to 17x lower replication lag with write-through cache for Aurora PostgreSQL — AWS — Aurora 논리 복제 Write-Through Cache 상세 설명 및 벤치마크 (입문~중급)
5. 내 업무와의 연결고리
섹션 제목: “5. 내 업무와의 연결고리”BackOps 엔지니어로서 다음 시나리오에서 직접 관련이 있다:
- API 응답 느릴 때: 읽기 쿼리가 Primary에 집중되고 있지 않은가? TypeORM Replication 설정으로 읽기 분산
- 배치 작업 설계: 대용량 데이터 처리 시 Read Replica에서 읽어 Primary 부하 차단
- DynamoDB 파티션 설계: 이벤트 로그, 세션 데이터 저장 시 Shard Key(파티션 키) 선택이 성능 좌우
- 재해 복구 계획: Multi-AZ(동기 복제 Standby) + Read Replica(비동기, 읽기 분산)의 차이 이해
- 일관성 요구사항 파악: 결제 성공 직후 바로 최신 잔액을 보여줘야 한다면
ConsistentRead: true또는 Master에서 직접 읽기
AWS 모니터링 실전: RDS Performance Insights
섹션 제목: “AWS 모니터링 실전: RDS Performance Insights”Sharding과 Replication 문제는 CloudWatch 외에 RDS Performance Insights로 더 정밀하게 진단할 수 있다.
AWS 콘솔 경로:RDS → 데이터베이스 선택 → 모니터링 탭 → Performance Insights
주요 지표:- DBLoad: 동시에 실행 중인 쿼리 수 (CPU 코어 수보다 높으면 병목)- Top SQL: CPU 또는 I/O를 가장 많이 소비하는 쿼리 Top 10- Wait Events: 쿼리가 무엇을 기다리는지 (Lock wait, I/O wait 등)# AWS CLI로 Replication Lag 알람 설정aws cloudwatch put-metric-alarm \ --alarm-name "RDS-ReplicaLag-High" \ --alarm-description "Replica Lag이 30초 초과 시 알람" \ --metric-name ReplicaLag \ --namespace AWS/RDS \ --statistic Average \ --period 60 \ --threshold 30 \ --comparison-operator GreaterThanThreshold \ --dimensions Name=DBInstanceIdentifier,Value=mydb-replica \ --evaluation-periods 2 \ --alarm-actions arn:aws:sns:ap-northeast-2:123456789:my-alert-topic
# 예상 동작: ReplicaLag 30초 초과가 2분 연속되면 SNS → 이메일/Slack 알림📖 더 보기: Sharding with Amazon Relational Database Service — AWS RDS 환경에서 애플리케이션 레벨 Sharding 구현 전략 공식 가이드 (입문~중급)
6. 비슷한 개념과 비교
섹션 제목: “6. 비슷한 개념과 비교”| 개념 | 목적 | 분할 기준 | 서버 수 |
|---|---|---|---|
| Replication | 가용성, 읽기 성능 | 데이터 전체 복사 | Primary 1 + N Replicas |
| Horizontal Sharding | 쓰기 확장, 데이터 크기 | 행(Row) 단위 분산 | N개 (동등) |
| Vertical Partitioning | 쿼리 효율 | 열(Column) 단위 분리 | 동일 DB 내 |
| Table Partitioning | 조회 성능 | 행 단위 (같은 서버 내) | 동일 DB 내 |
Sharding vs Table Partitioning 차이:
- Table Partitioning: 한 서버 안에서 테이블을 논리적으로 나눔 (MySQL PARTITION BY)
- Sharding: 완전히 다른 DB 서버로 물리적으로 분산
-- MySQL Table Partitioning (같은 서버, 다른 개념!)CREATE TABLE orders ( id INT, created_at DATE, amount DECIMAL(10,2))PARTITION BY RANGE (YEAR(created_at)) ( PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025), PARTITION p2025 VALUES LESS THAN (2026));6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”문제 1: Replica Lag — 읽기 데이터가 최신이 아님
섹션 제목: “문제 1: Replica Lag — 읽기 데이터가 최신이 아님”증상:
사용자가 게시물 작성 후 목록 페이지로 이동 → 방금 작성한 게시물이 안 보임또는: 결제 완료 후 내역 페이지에서 거래 내역이 없음원인: 비동기 복제로 인해 Primary에 쓰기가 완료되었지만 Replica에 아직 반영되지 않은 상태. Replica Lag은 수백ms에서 수 초까지 발생할 수 있다. 주요 원인별로 구분하면:
- 네트워크 지연: Primary → Replica 간 네트워크 대역폭 부족 또는 지연 시간 증가
- Replica I/O 병목: Replica 디스크가 WAL 쓰기를 따라가지 못함 (HDD 사용 시 빈번)
- Replica CPU 부족: WAL replay 프로세스가 CPU를 확보하지 못해 지연
- Primary의 대량 쓰기 작업: 배치 INSERT, 대규모 DDL(ALTER TABLE) 실행 시 WAL이 급증
- Primary의 Access Exclusive Lock:
ALTER TABLE,DROP INDEX등 DDL이 장시간 락을 잡으면 Replica replay도 블록됨
확인 방법 — LSN 단계별 병목 진단:
-- PostgreSQL: LSN 단계별 Lag 확인 (어디서 병목인지 진단)SELECT client_addr, state, pg_size_pretty(pg_wal_lsn_diff(sent_lsn, write_lsn)) AS network_lag, -- 네트워크 지연 pg_size_pretty(pg_wal_lsn_diff(write_lsn, flush_lsn)) AS disk_lag, -- 디스크 I/O 지연 pg_size_pretty(pg_wal_lsn_diff(flush_lsn, replay_lsn)) AS replay_lag, -- CPU/replay 지연 pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS total_lag -- 전체 LagFROM pg_stat_replication;
-- 예상 출력 (network_lag이 크면 네트워크 문제, replay_lag이 크면 Replica CPU 문제)-- client_addr | state | network_lag | disk_lag | replay_lag | total_lag-- 10.0.1.100 | streaming | 128 kB | 0 bytes | 8 MB | 8 MB-- → replay_lag가 크다 = Replica의 CPU 또는 디스크 I/O가 replay를 따라가지 못함
-- MySQL: Replica Lag 확인SHOW REPLICA STATUS\G-- Seconds_Behind_Source: 0 이면 정상, 클수록 지연AWS CloudWatch로 Replica Lag 모니터링:
메트릭: AWS/RDS > ReplicaLag경보 설정: ReplicaLag > 30초 → SNS 알림
추가 확인 지표:- CPUUtilization (Replica): 80% 초과 시 replay 병목 가능성- ReadIOPS / WriteIOPS (Replica): I/O 포화 여부 확인- NetworkReceiveThroughput (Replica): 네트워크 대역폭 병목 확인해결 방법:
// 방법 1: 쓰기 직후 읽기는 Master에서 명시적으로 읽기async createPost(userId: number, content: string): Promise<Post> { const post = await this.postRepository.save({ userId, content });
// 방금 생성한 데이터를 즉시 반환하기 위해 객체 그대로 반환 // Replica에서 읽지 않고 save()가 반환한 객체 사용 return post;}
// 방법 2: 중요한 읽기는 TypeORM QueryRunner로 master 지정const queryRunner = dataSource.createQueryRunner('master');const freshData = await queryRunner.manager.findOne(Post, { where: { id } });await queryRunner.release();문제 2: Hot Shard — 특정 DB 서버만 과부하
섹션 제목: “문제 2: Hot Shard — 특정 DB 서버만 과부하”증상:
DB 서버 3대 중 1대만 CPU 90% 이상특정 사용자(유명인, 인플루언서)의 API 요청 시 타임아웃 발생다른 사용자는 정상이지만 특정 계정 관련 작업만 느림원인: Shard Key 설계 문제. 트래픽이 집중되는 특정 값이 하나의 Shard에 집중되고 있다. 예를 들어 팔로워 수백만 명의 계정 데이터가 단일 Shard에 있는 경우.
확인 방법:
# 각 Shard DB의 쿼리 수 비교# Shard 1: 초당 쿼리 10,000건# Shard 2: 초당 쿼리 200건# Shard 3: 초당 쿼리 150건# → Shard 1이 Hot Shard해결 방법:
// 방법 1: 핫 키를 별도 전용 Shard로 이전// "유명인" 계정은 별도 dedicated shard에서 서비스
// 방법 2: 복합 Shard Key 사용 (user_id + 날짜)function getShardId(userId: string, date: string): number { const combinedKey = `${userId}:${date.slice(0, 7)}`; // userId + YYYY-MM return hash(combinedKey) % TOTAL_SHARDS;}// → 같은 userId라도 월별로 다른 Shard에 분산
// 방법 3: 읽기 집중 문제라면 캐시로 해결// 핫 키의 데이터를 Redis에 캐싱하여 DB 직접 조회 최소화문제 3: Shard 추가 시 데이터 재분배 필요 (Re-sharding)
섹션 제목: “문제 3: Shard 추가 시 데이터 재분배 필요 (Re-sharding)”증상:
Shard를 3개에서 4개로 늘렸더니 기존 쿼리가 잘못된 Shard를 조회"user_id=1001의 데이터를 찾을 수 없음" 에러 다수 발생원인:
Hash Sharding에서 1001 % 3 = 2 (기존 Shard 2)이던 데이터가 1001 % 4 = 1 (새 Shard 1)이 되어버림. Shard 수가 바뀌면 모든 데이터의 매핑이 달라진다.
해결 방법:
방법 1: Consistent Hashing 사용→ Shard를 추가해도 전체 데이터의 일부(1/N)만 이동→ 처음부터 Consistent Hashing 알고리즘 도입
방법 2: 단계적 마이그레이션Step 1: 새 Shard 추가, 새 데이터는 새 Shard에만 저장Step 2: 이중 쓰기(구 Shard + 새 Shard 동시에)Step 3: 백그라운드로 구 Shard 데이터를 새 Shard로 이전Step 4: 읽기를 새 Shard로 전환Step 5: 구 Shard 데이터 삭제
방법 3: DynamoDB처럼 자동 관리되는 서비스 사용→ AWS가 파티션 재분배를 자동으로 처리문제 4: Split-Brain — Multi-Master에서 쓰기 충돌
섹션 제목: “문제 4: Split-Brain — Multi-Master에서 쓰기 충돌”증상:
두 노드에서 동시에 같은 레코드를 수정네트워크 복구 후 충돌 감지: ERROR: Conflict detected on row id=42원인: 네트워크 파티션 상황에서 두 Master가 독립적으로 같은 데이터를 수정했다. CAP에서 AP를 선택한 결과로 발생하는 전형적인 문제다.
해결 방법:
단기: Last-Write-Wins 정책→ 타임스탬프가 더 최신인 쪽의 데이터 채택→ 단점: 일부 변경사항이 자동으로 사라짐
중기: 애플리케이션 레벨 충돌 해결 로직→ 충돌 발생 시 사람이 검토할 수 있도록 큐에 적재
장기: Multi-Master 아키텍처 재고→ 쓰기 충돌이 자주 발생한다면 Single-Master 구조가 더 안전문제 5: Replication Slot이 WAL을 무한 보존 — 디스크 가득 참
섹션 제목: “문제 5: Replication Slot이 WAL을 무한 보존 — 디스크 가득 참”증상:
FATAL: could not write to file "pg_wal/000000010000000000000042": No space left on devicepg_wal 디렉토리가 수십~수백 GB로 증가Primary DB 쓰기 불가 → 서비스 전체 장애원인: PostgreSQL의 Replication Slot은 Replica가 아직 읽지 못한 WAL을 삭제하지 않도록 보존한다. Replica가 장시간 연결이 끊기거나, 사용하지 않는 슬롯이 남아있으면 WAL이 계속 쌓여 디스크를 가득 채운다. 특히 CDC(Debezium) 슬롯이 장애로 멈추면 빠르게 디스크가 차오른다.
확인 방법:
-- 비활성 슬롯 확인 (active = false인 슬롯이 WAL을 막고 있음)SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_walFROM pg_replication_slotsORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;
-- 예상 출력:-- slot_name | active | retained_wal-- debezium_slot | f | 45 GB ← 원인!-- replica1_slot | t | 128 kB ← 정상
-- WAL 디렉토리 전체 크기 확인SELECT pg_size_pretty(sum(size)) AS total_wal_size FROM pg_ls_waldir();해결 방법:
-- 1. 사용하지 않는 슬롯 즉시 삭제 (Replica/CDC 연결 상태 확인 후!)SELECT pg_drop_replication_slot('debezium_slot');
-- 2. PostgreSQL 13+ : max_slot_wal_keep_size로 슬롯의 WAL 보존 한도 설정-- postgresql.conf-- max_slot_wal_keep_size = 10GB-- → 이 값을 초과하면 슬롯이 invalidated 되어 WAL을 자동 해제
-- 3. 예방: 슬롯 모니터링 알람 설정 (retained_wal > 5GB 시 알림)📖 더 보기: Mastering Postgres Replication Slots: Preventing WAL Bloat — Replication Slot의 WAL 보존 메커니즘과 디스크 풀 예방 전략 (Gunnar Morling, 중급)
7. 체크리스트
섹션 제목: “7. 체크리스트”- Replication과 Sharding의 차이를 설명할 수 있다 (목적, 분할 기준, 서버 구성)
- 동기 복제와 비동기 복제의 트레이드오프를 설명할 수 있다
- TypeORM에서 읽기/쓰기 분리를 설정할 수 있다
- AWS RDS Read Replica와 Multi-AZ의 차이를 설명할 수 있다
- Range Sharding과 Hash Sharding의 장단점을 비교할 수 있다
- Hot Shard 문제를 인식하고 예방 방법을 알고 있다
- DynamoDB 파티션 키 설계 시 주의사항을 설명할 수 있다
- CAP Theorem의 3가지 속성을 설명하고, CP/AP 예시를 들 수 있다
- Replica Lag 발생 시 CloudWatch 메트릭으로 확인할 수 있다
- 서비스 특성에 따라 CP/AP 중 적합한 DB를 선택하는 판단 기준을 말할 수 있다
8. 핵심 키워드
섹션 제목: “8. 핵심 키워드”| 키워드 | 설명 |
|---|---|
| Primary / Master | 쓰기를 담당하는 메인 DB 서버 |
| Replica / Slave | 읽기를 담당하는 복제 DB 서버 |
| Binary Log / WAL | 복제를 위한 변경 내역 로그 파일 |
| Replica Lag | Primary 변경이 Replica에 반영되기까지의 지연 시간 |
| Shard Key | 데이터가 어느 Shard로 갈지 결정하는 기준 컬럼 |
| Hot Shard | 특정 Shard에 트래픽이 집중되는 현상 |
| Hash Sharding | Shard Key의 해시값으로 균등 분배 |
| Range Sharding | 값의 범위로 Shard 결정 |
| Consistent Hashing | Shard 추가 시 데이터 이동을 최소화하는 알고리즘 |
| Vertical Partitioning | 컬럼을 기준으로 테이블을 분리 |
| CAP Theorem | 분산 시스템에서 C, A, P 중 동시에 2개만 보장 가능 |
| Eventually Consistent | 즉각 일관성 대신 결국에는 일치하는 AP 방식 |
| Multi-AZ | AWS의 동기 복제 고가용성 구성 (읽기 분산 불가) |
| Split-Brain | 네트워크 파티션으로 두 Master가 독립 동작하는 상태 |
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 AWS RDS Read Replicas 공식 문서 — Read Replica 생성·모니터링·승격 방법을 AWS 공식으로 안내 (입문)
- 📖 AWS What is Database Sharding? — Sharding 개념과 AWS 환경에서의 적용 전략 공식 설명 (입문)
- 📖 Consistent Hashing — Hello Interview — 해시 링의 동작 원리를 시각적으로 설명. 시스템 디자인 면접 준비용 (입문~중급)
- 📖 PostgreSQL Replication Lag 완전 분석 — Percona — Lag 원인 진단부터 해결까지 단계별 실전 가이드 (중급)
- 📖 Mastering Postgres Replication Slots — Gunnar Morling — Replication Slot이 WAL을 보존하는 메커니즘과 디스크 풀 예방 전략 (중급)
9. 직접 확인해보기
섹션 제목: “9. 직접 확인해보기”9-1. PostgreSQL Replica Lag 확인
섹션 제목: “9-1. PostgreSQL Replica Lag 확인”# Primary에서 Replica 연결 및 Lag 상태 확인psql -h <primary-host> -U postgres -c "SELECT application_name, client_addr, state, sync_state, pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS replication_lagFROM pg_stat_replication;"
# 예상 출력 application_name | client_addr | state | sync_state | replication_lag--------------------+--------------+-------+--------------+----------------- replica1 | 10.0.1.100 | streaming | async | 512 kB replica2 | 10.0.1.101 | streaming | async | 0 bytes9-2. MySQL Replica 상태 확인
섹션 제목: “9-2. MySQL Replica 상태 확인”# Replica DB 서버에서 실행mysql -u root -p -e "SHOW REPLICA STATUS\G" | grep -E "Seconds_Behind|Running|Error"
# 예상 출력 Replica_IO_Running: Yes Replica_SQL_Running: YesSeconds_Behind_Source: 0 ← 0이면 정상, 클수록 지연9-3. DynamoDB 파티션 키 분산 확인 (CloudWatch)
섹션 제목: “9-3. DynamoDB 파티션 키 분산 확인 (CloudWatch)”# AWS CLI로 DynamoDB 테이블의 ConsumedWriteCapacityUnits 확인aws cloudwatch get-metric-statistics \ --namespace AWS/DynamoDB \ --metric-name ConsumedWriteCapacityUnits \ --dimensions Name=TableName,Value=UserActivity \ --start-time 2026-04-01T00:00:00Z \ --end-time 2026-04-02T00:00:00Z \ --period 300 \ --statistics Sum
# 예상 출력 (ThrottledRequests가 많으면 Hot Partition 의심){ "Datapoints": [ {"Timestamp": "2026-04-01T12:00:00Z", "Sum": 245.0, "Unit": "Count"}, {"Timestamp": "2026-04-01T12:05:00Z", "Sum": 12500.0, "Unit": "Count"}, ← 급증 = Hot Partition! ... ]}9-4. TypeORM Replication 동작 확인 (로그)
섹션 제목: “9-4. TypeORM Replication 동작 확인 (로그)”// ormconfig의 logging 옵션 활성화TypeOrmModule.forRoot({ logging: ["query"], // 모든 쿼리 로그 출력 // ...});
// 실행 후 로그에서 [master] / [slave] 표시 확인// [slave] SELECT "User"."id", "User"."name" FROM "users" "User"// [master] INSERT INTO "users"("name") VALUES ($1) RETURNING "id"10. 한 줄 요약
섹션 제목: “10. 한 줄 요약”Replication은 데이터를 복사해서 읽기 부하와 장애를 분산하고, Sharding은 데이터를 쪼개서 쓰기 부하와 용량을 분산하며, CAP 정리는 이 모든 분산 시스템 설계의 근본 제약을 설명한다.