본문으로 바로가기

여러 PostgreSQL 풀 한 앱에서 관리하기

관리 콘솔이나 백오피스는 한 프로세스에서 콘텐츠·카탈로그·운영 로그처럼 서로 다른 DB에 접근해야 할 수 있습니다. HTTP 경유 대신 풀 직접 접속을 선택할 때는 연결 수, 권한, 장애 격리를 함께 설계해야 합니다.

5회 조회약 2분 읽기
X에 공유 새 창에서 열림
목차

관리 콘솔이나 백오피스는 한 프로세스에서 콘텐츠·카탈로그·운영 로그처럼 서로 다른 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

node-postgres · PostgreSQL connection management를 참고합니다.

이 글에서 만나는 용어

data 카테고리의 다른 글

카테고리 전체 보기 →

관련 글

이 글이 도움이 되었나요?