여러 PostgreSQL 풀 한 앱에서 관리하기
관리 콘솔이나 백오피스는 한 프로세스에서 콘텐츠·카탈로그·운영 로그처럼 서로 다른 DB에 접근해야 할 수 있습니다. HTTP 경유 대신 풀 직접 접속을 선택할 때는 연결 수, 권한, 장애 격리를 함께 설계해야 합니다.
목차
관리 콘솔이나 백오피스는 한 프로세스에서 콘텐츠·카탈로그·운영 로그처럼 서로 다른 DB에 접근해야 할 수 있습니다. HTTP 경유 대신 풀 직접 접속을 선택할 때는 연결 수, 권한, 장애 격리를 함께 설계해야 합니다.
1. 왜 풀을 나누는가
- 도메인 격리 — 카탈로그 백업이 콘텐츠 편집을 막지 않음
- 권한·계정 분리 — DB role이 도메인 역할과 대응
- 용량·백업 주기 분리 — 수집 DB는 매일, 콘텐츠 DB는 주 1회처럼 운영
- 외부 관리형 DB 공존 — 외부 풀러와 로컬 컨테이너의 수명주기를 분리
스키마(content, catalog)만 나누는 방법도 있지만 컨테이너·백업·권한까지 분리해야 한다면 풀 분리가 더 명확합니다.
2. 싱글톤 풀 — node-postgres 예
import { Pool } from 'pg';
export const contentPool = new Pool({
host: process.env.CONTENT_DB_HOST!,
port: Number(process.env.CONTENT_DB_PORT ?? 5432),
database: process.env.CONTENT_DB_NAME!,
user: process.env.CONTENT_DB_USER!,
password: process.env.CONTENT_DB_PASSWORD!,
ssl: sslConfig(process.env.CONTENT_DB_SSL_MODE),
max: 10,
});
export const catalogPool = new Pool({ /* CATALOG_DB_* */ });
export const operationsPool = new Pool({ /* OPERATIONS_DB_* */ });
도메인 prefix를 환경변수에 넣으면 .env를 읽기 쉽습니다. 각 풀은 프로세스 생명주기에서 하나만 만들고, 총 max 합계가 PostgreSQL의 연결 한도를 넘지 않는지 계산합니다.
3. 얇은 쿼리 헬퍼
export async function queryContent<T>(
sql: string,
params: unknown[] = [],
): Promise<T[]> {
const { rows } = await contentPool.query<T>(sql, params);
return rows;
}
export async function queryOneContent<T>(
sql: string,
params: unknown[] = [],
): Promise<T | null> {
return (await queryContent<T>(sql, params))[0] ?? null;
}
호출자가 어느 DB를 쓰는지 함수 이름으로 드러나고, SQL 파라미터·반환 타입을 한 경계에서 검증할 수 있습니다.
4. 트랜잭션 — connect() + try/finally
const client = await contentPool.connect();
try {
await client.query('BEGIN');
const { rows } = await client.query(
'INSERT INTO posts (...) VALUES (...) RETURNING id',
[...],
);
await client.query('COMMIT');
return rows[0].id;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
release() 누락은 풀 고갈의 대표 원인입니다. withPoolClient() 헬퍼와 트랜잭션 timeout을 함께 두면 누락과 장기 점유를 줄일 수 있습니다.
5. SSL과 종료
- 로컬·Docker 내부 통신은 환경에 맞춰 SSL을 끌 수 있음
- 클라우드는 CA 검증과
rejectUnauthorized: true를 기본으로 함 - 외부 풀러의 인증서 정책은 공급자 문서에 맞춰 별도 설정
process.on('SIGTERM', async () => {
await Promise.all([contentPool, catalogPool, operationsPool].map((pool) => pool.end()));
process.exit(0);
});
6. 도메인 라우팅 규칙
/api/content/**→contentPool/api/catalog/**→catalogPool/api/operations/**→operationsPool- 감사 로그·세션처럼 횡단하는 데이터 → 고정된 operations pool
경로와 풀의 매핑을 표로 고정하면 리뷰에서 잘못된 DB 접근을 빨리 찾을 수 있습니다. 다른 풀을 가로지르는 업무는 분산 트랜잭션으로 착각하지 말고 outbox·보상 작업·재처리 상태를 설계합니다.
7. 자주 걸리는 자리
환경변수 오타 — 누락된 host가 localhost로 조용히 대체되지 않도록 requireEnv()로 즉시 실패시킵니다.
풀 합계 초과 — 풀마다 max: 10을 주면 replica 수만큼 연결이 곱해집니다. replicas × Σmax + 관리 여유를 DB 한도와 비교합니다.
긴 트랜잭션 — 대량 작업은 별도 worker 또는 작은 전용 풀로 격리하고, 목록 API가 같은 연결을 기다리지 않게 합니다.
스크립트 종료 누락 — 일회성 명령은 마지막에 await pool.end()를 호출합니다.
하고픈 말
여러 풀은 단일 DB보다 운영 복잡도가 높습니다. 도메인별 백업·권한·장애 격리가 실제 요구일 때만 나누고, 먼저 연결 수 예산과 복구 시나리오를 문서화하세요.
Next
- postgres-first
- postgres-deep
- backend/09-audit-log-pattern
이 글에서 만나는 용어
data 카테고리의 다른 글
카테고리 전체 보기 →관련 글
감사로그 — logAdminAction 패턴
관리자 기능을 갖춘 백엔드에서 "누가 · 언제 · 무엇을 · 왜" 의 네 축을 기록하는 감사로그 (audit log) 는 단순한 관행 이상입니다. 개인정보보호법 · GDPR 같은 규정 준수, 사고 조사, 권한 오남용 방지의 실질 수단.
SQL 을 단일 진실 출처로
스키마는 어디에서 진실을 가질 것인가. ORM 모델·마이그레이션 파일·DDL SQL — 후보가 여럿입니다. 누적 마이그레이션 모델과 단일 SQL 파일 + CREATE IF NOT EXISTS 모델을 비교합니다.
PostgreSQL 부터
데이터를 저장할 도구를 고르는 일은 자주 부담입니다. NoSQL · 분산 SQL · 그래프 DB · 시계열 DB 가 줄지어 있습니다. 그러나 많은 프로젝트가 처음 한참 동안 PostgreSQL 한 가지로 충분합니다.
검색을 ILIKE·pg_trgm·마이그레이션으로 최적화하기
콘텐츠 검색은 제목·설명·slug·lesson 본문에서 ILIKE '%검색어%'를 사용한다. 검색어를 escape하고 길이를 제한하는 것은 안전성 계약이고, wildcard 앞뒤 때문에 일반 B-tree 인덱스는 이 쿼리를 충분히 돕지 못한다.