codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›PostgreSQL 깊게 다루기 + Redis · Kafka›2단계

2단계

여러 풀 오케스트레이션

0회 조회

여러 풀 오케스트레이션

도메인별 DB 분리. 한 앱이 여러 PG 풀을 다루는 실전.

1. 왜 풀을 나누는가

  • 도메인 격리 — 백업 · 유지보수 · 장애 영향 분리
  • 권한 분리 — 크론 전용 사용자 vs 웹 앱 사용자
  • 용량 · 백업 주기 차등 — 크롤러 DB 매일, 블로그 DB 주 1회
  • 외부 SaaS 공존 — Supabase CLI 등 자체 대역 포트

2. pg Pool 싱글톤

import { Pool } from "pg";

function requireEnv(name: string) {
  const v = process.env[name];
  if (!v) throw new Error(`env ${name} missing`);
  return v;
}

export const blogPool = new Pool({
  host: requireEnv("BLOG_DB_HOST"),
  port: Number(process.env.BLOG_DB_PORT ?? 5432),
  database: requireEnv("BLOG_DB_NAME"),
  user: requireEnv("BLOG_DB_USER"),
  password: requireEnv("BLOG_DB_PASSWORD"),
  max: 10,
});

export const marketPool = new Pool({ /* MARKET_DB_* */ });

requireEnv 는 환경변수 오타를 런타임 첫 호출에서 잡음.

3. 얇은 쿼리 헬퍼

export async function queryBlog<T>(
  sql: string, params: unknown[] = []
): Promise<T[]> {
  const { rows } = await blogPool.query<T>(sql, params);
  return rows;
}

호출 측 await queryBlog<Post>("SELECT ...") 만. 타입 힌트 · grep 명확.

4. 트랜잭션 래퍼

export async function withPoolClient<T>(pool: Pool, fn: (c: any) => Promise<T>): Promise<T> {
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    const r = await fn(client);
    await client.query("COMMIT");
    return r;
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  } finally {
    client.release();
  }
}

// 사용
await withPoolClient(blogPool, async (c) => {
  await c.query("INSERT INTO posts ...");
  await c.query("INSERT INTO post_tags ...");
});

release() 누락을 구조적으로 방지.

5. SSL 분기

function sslConfig(mode?: string, connString?: string) {
  if (connString?.includes(".supabase.")) return { rejectUnauthorized: false };
  if (mode === "require") return { rejectUnauthorized: true };
  return false;
}
  • 로컬 · docker-to-docker — SSL 없음
  • 클라우드 (RDS) — rejectUnauthorized: true
  • Supabase pooler — rejectUnauthorized: false (알려진 특수성)

6. graceful shutdown

const pools = [blogPool, marketPool, cachePool];

process.once("SIGTERM", async () => {
  await Promise.all(pools.map(p => p.end()));
  process.exit(0);
});

Docker stop 시 진행 중 쿼리 완료 후 종료.

7. 라우팅 규칙

/api/blog/*    → blogPool
/api/market/*  → marketPool
/api/search    → (cross-domain) → blog + market 둘 다

cross-domain 쿼리는 application level join 또는 별도 search service.

8. cross-cutting 풀

감사로그 · FCM 토큰 · 세션 같이 모든 도메인에서 쓰는 데이터는 한 풀 (coreDbPool) 에.

"각 도메인마다 audit_logs 따로" 는 초반 1~2년 과도한 분리. 통합 조회가 오히려 가치.

9. 연결 수 튜닝

pg_stat_activity 로 관찰.

SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
  • 대부분 idle — pool max 줄여도 됨
  • active 많음 — slow query 있나 확인
  • 자주 idle in transaction — 트랜잭션 미종결 누수

10. 자주 걸리는 자리

  • 환경변수 오타 — requireEnv 로 즉시 실패
  • max 너무 큼 — PG max_connections 초과. 앱 인스턴스 × max 총합 계산
  • 긴 트랜잭션 — 다른 쿼리 대기. 별도 admin 풀 분리 고려
  • 스크립트에서 pool.end() 누락 — 프로세스가 안 죽음

하고픈 말

풀 N 개는 "도메인 N 개 = 풀 N 개 + cross-cutting 1 ~ 2 개" 가 장기적으로 편합니다. 더 세분화는 실제 병목이 생긴 뒤.

Next

  • 03-pgvector-hnsw

← 1단계

PostgreSQL 심화 — EXPLAIN · 인덱스

3단계 →

pgvector + HNSW