--- id: db-read-replica-patterns title: Read Replica — Replication Lag / 일관성 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, replication, read-replica, consistency, vibe-coding] tech_stack: { language: "SQL / Postgres / MySQL", applicable_to: ["Backend"] } applied_in: [] aliases: [replication lag, eventual consistency, write-then-read, read-your-writes] --- # Read Replica > Primary 1대 + Replica N대. Read 분산 → Primary 부하↓. **Replication lag (보통 ms~s) 이 함정**. 방금 쓴 데이터 즉시 read 시 미반영 가능 — read-your-writes 패턴 필요. ## 📖 핵심 개념 - Async replication (보통): primary 가 commit 후 replica 로 stream. - Replication lag: primary→replica 도달 시간. - Read-your-writes: 자기가 쓴 건 자기가 읽을 때 보여야. - Strong vs eventual: 모든 쿼리가 강할 필요 없음. ## 💻 코드 패턴 ### 라우팅 — Prisma ```ts import { PrismaClient } from '@prisma/client'; const writer = new PrismaClient({ datasources: { db: { url: process.env.PRIMARY_URL } } }); const reader = new PrismaClient({ datasources: { db: { url: process.env.REPLICA_URL } } }); async function getUserPosts(userId: string) { return reader.post.findMany({ where: { userId } }); // read = replica } async function createPost(input: NewPost) { return writer.post.create({ data: input }); // write = primary } ``` ### Read-your-writes — sticky after write ```ts class DbRouter { private lastWriteAt = 0; reader() { if (Date.now() - this.lastWriteAt < 2000) return writer; // 최근 2초 = primary return reader; } async write(fn: (db) => Promise) { await fn(writer); this.lastWriteAt = Date.now(); } } ``` ### 좀 더 정확 — LSN tracking (Postgres) ```sql -- Primary 에서 commit 후 LSN 받음 SELECT pg_current_wal_lsn(); -- Replica 에서 검사 SELECT pg_last_wal_replay_lsn() >= 'X/Y'::pg_lsn AS caught_up; ``` ```ts async function readAfterWrite(query: () => Promise): Promise { const lsn = await writer.queryRaw('SELECT pg_current_wal_lsn() AS lsn'); for (let i = 0; i < 10; i++) { const caught = await reader.queryRaw( `SELECT pg_last_wal_replay_lsn() >= '${lsn}'::pg_lsn AS ok` ); if (caught.ok) return query.call(reader); await sleep(50); } return query.call(writer); // fallback } ``` ### 라우팅 — request scope ```ts // Express middleware app.use((req, res, next) => { req.db = req.method === 'GET' ? reader : writer; next(); }); ``` ### 트랜잭션 안 — primary 만 ```ts // 트랜잭션 내 read 도 primary — replica 는 다른 view 가능 await writer.$transaction(async (tx) => { const user = await tx.user.findUnique(...); // primary await tx.post.create({ data: { userId: user.id, ...} }); }); ``` ### Replication lag 모니터링 ```sql -- Postgres SELECT now() - pg_last_xact_replay_timestamp() AS lag; -- MySQL SHOW REPLICA STATUS\G -- Seconds_Behind_Source ``` 알람: lag > 5s. ## 🤔 의사결정 기준 | 상황 | 라우팅 | |---|---| | Write | Primary | | 즉시 read after write | Primary (2초 sticky) | | 일반 list / detail | Replica | | 분석 / 리포트 | Replica (분리된 분석용) | | 트랜잭션 내 read | Primary (같은 connection) | | Cache 가능 | Cache 우선, 미스 시 replica | ## ❌ 안티패턴 - **모든 read 를 무조건 replica**: read-after-write 깨짐. - **트랜잭션 안 read 를 replica**: stale. - **Lag 모니터링 없음**: 100s lag 도 모름. - **Replica failover 안 함**: replica 1대 죽으면 모두 실패. health check + 다음 replica. - **Primary write 성공 → 그 자리에서 replica read**: 거의 무조건 stale. - **GROUP BY count 같은 무거운 쿼리 primary**: primary 부하. analytic replica 분리. ## 🤖 LLM 활용 힌트 - 기본 read = replica, write = primary. - Read-your-writes = 2초 sticky 또는 LSN. - Lag 모니터링 + alarm 필수. ## 🔗 관련 문서 - [[DB_Connection_Pooling_Patterns]] - [[DB_Sharding_Strategies]]