PostgreSQL Read-Only 레플리카에서 데이터가 바로 안 보이는 이유 + 직접 실험해보기
🐘 PostgreSQL Read-Only 레플리카에서 데이터가 바로 안 보이는 이유 + 직접 실험해보기
서비스를 운영하다 보면 한 번쯤 이런 경험이 있습니다:
✅ 어떤 데이터를 업데이트했는데,
❌ 바로 직후 조회해보니 반영이 안 되어 있음?! 😱
이번 글에서는 PostgreSQL의 Read-Only Replica 환경에서 발생할 수 있는 데이터 복제 지연 문제를 다룹니다. 실제로 겪었던 디버깅 과정과 함께, 이를 재현할 수 있는 Node.js + Docker 실험 환경까지 공유합니다.
🧩 현상 요약
pass key 삭제 REST API 호출 → 200 OK
→ 직후 pass key 조회 시, 삭제되지 않은 값이 반환됨
⚙️ 시스템 구성
pgUtil: Primary 연결 (읽기/쓰기 가능)roPgUtil: Read-Only Replica 연결 (읽기 전용)
🔍 재현 시나리오
-
Primary 트랜잭션에서 row 업데이트
BEGIN; UPDATE pass_key SET deleted = true WHERE id = 'abc123'; COMMIT; -
직후 Read-Only 커넥션으로 동일 row 조회
SELECT * FROM pass_key WHERE id = 'abc123';
- 기대 결과:
deleted = true - 실제 결과:
deleted = false
🧪 디버깅 과정 요약
- Slonik 트랜잭션 구현 문제인가? → ❌ 문제 없음
- 쿼리 순서 꼬임인가? → ❌ 순서 보장 확인됨
- Primary에서만 조회하면 문제 없음 → ✅ Replica 복제 지연으로 판단
💡 결론: Read-Only Replica의 복제 지연 (Replication Lag)
PostgreSQL은 Streaming Replication 구조를 사용하며, 일반적으로 비동기(async) 방식입니다. 이는 Primary에서 COMMIT이 완료되어도, Replica 반영에는 수 밀리초 ~ 수십 밀리초의 지연이 생길 수 있다는 뜻입니다.
🐳 로컬에서 직접 실험해보기 (Docker + Node.js)
복제 지연 현상을 테스트해볼 수 있도록 Primary / Replica 구성과 테스트 코드를 함께 제공합니다.
📦 docker-compose.yml
version: '3.8'
services:
postgres-primary:
image: postgres:15
container_name: postgres-primary
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- ./primary-data:/var/lib/postgresql/data
networks:
- pgnet
postgres-replica:
image: postgres:15
container_name: postgres-replica
depends_on:
- postgres-primary
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports:
- "5433:5432"
command:
- bash
- -c
- |
sleep 5
echo "host replication all all trust" >> /var/lib/postgresql/data/pg_hba.conf
echo "standby_mode = 'on'" > /var/lib/postgresql/data/recovery.conf
echo "primary_conninfo = 'host=postgres-primary port=5432 user=postgres password=pass'" >> /var/lib/postgresql/data/recovery.conf
echo "recovery_target_timeline = 'latest'" >> /var/lib/postgresql/data/recovery.conf
postgres -c wal_level=replica -c hot_standby=on -c max_wal_senders=10 -c wal_keep_size=64 -c hot_standby_feedback=on -c max_standby_streaming_delay=10000
volumes:
- ./replica-data:/var/lib/postgresql/data
networks:
- pgnet
networks:
pgnet:
❗ max_standby_streaming_delay=10000 → 최대 10초의 지연 허용 (테스트용)
🧪 테스트 코드 (Node.js + node-postgres)
import { Client } from 'pg';
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
const main = async () => {
const primary = new Client({ connectionString: 'postgres://user:pass@localhost:5432/app' });
const replica = new Client({ connectionString: 'postgres://user:pass@localhost:5433/app' });
await primary.connect();
await replica.connect();
const id = 'test-id';
console.log('🚀 Updating row in primary...');
await primary.query('UPDATE pass_key SET deleted = true WHERE id = $1', [id]);
console.log('🕵️ Immediately reading from replica...');
const before = await replica.query('SELECT deleted FROM pass_key WHERE id = $1', [id]);
console.log('Replica BEFORE delay:', before.rows[0]);
await delay(200);
const after = await replica.query('SELECT deleted FROM pass_key WHERE id = $1', [id]);
console.log('Replica AFTER 200ms delay:', after.rows[0]);
await primary.end();
await replica.end();
};
main();
✅ 실행 결과 예시
🚀 Updating row in primary...
🕵️ Immediately reading from replica...
Replica BEFORE delay: { deleted: false }
Replica AFTER 200ms delay: { deleted: true }
🛡 실전에서 주의할 점
| 상황 | 해결 방안 |
|---|---|
| 실시간성 높은 조회 | Primary에서 조회 강제 (pgUtil 등) |
| 중요 트랜잭션 직후 조회 | 캐시 또는 지연 후 조회 권장 |
| 복제 지연 상태 확인 | pg_stat_replication, pg_last_xact_replay_timestamp() 활용 |
📌 마무리
PostgreSQL의 레플리카는 매우 유용하지만, eventual consistency 환경이라는 사실을 잊지 말아야 합니다. 실시간 조회나 중요한 트랜잭션 직후에는 반드시 일관성을 고려한 설계가 필요합니다.
"쓰기 직후 즉시 읽는 것은 항상 Primary에서 하자."