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

Navigation

  • Intro
  • Blog
  • Life

연락하기

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

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

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
노트›data

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

2026-05-06 게시· 2026-05-18 갱신·0회 조회

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

관리자 플랫폼 · 모노레포 백오피스는 한 앱이 여러 도메인 DB 에 직접 접속해야 할 때가 있습니다. 블로그 DB · 마켓 DB · 운영 로그 DB · 외부 호환 SaaS (Supabase) 네 개를 한 프로세스가 CRUD 하는 식. HTTP 경유 대신 풀 직접 접속을 고를 때 고려할 자리들.

1. 왜 풀을 나누는가

한 DB 에 모두 담지 않는 이유.

  • 도메인 격리 — 마켓 데이터 백업이 블로그 글 편집을 막지 않음
  • 권한 · 계정 분리 — DB 단위 role 이 도메인 역할과 1:1
  • 용량 · 백업 주기 분리 — 크롤러 DB 는 매일, 블로그 DB 는 주 1 회 같은 차등 정책
  • 외부 SaaS 와 공존 — Supabase 같은 외부 관리 PG 는 자체 대역 포트 (54332 등) 를 쓰기 때문에 자연히 분리

한 DB 에 스키마로 나누는 길 (schema=marketplace; schema=blog) 도 가능하지만 컨테이너 리소스 · 백업 · 권한 분리가 아쉬울 때 풀 분리가 단순합니다.

2. 싱글톤 풀 — node-postgres 예

// db.ts
import { Pool } from 'pg';

export const dmddkslPool = new Pool({
  host: process.env.DMDDKSL_DB_HOST!,
  port: Number(process.env.DMDDKSL_DB_PORT ?? 5432),
  database: process.env.DMDDKSL_DB_NAME!,
  user: process.env.DMDDKSL_DB_USER!,
  password: process.env.DMDDKSL_DB_PASSWORD!,
  ssl: sslConfig(process.env.DMDDKSL_DB_SSL_MODE),
  max: 10,                     // 풀당 커넥션 상한
});

export const pryzeetPool = new Pool({ /* PRYZEET_DB_* */ });
export const cachePool = new Pool({ /* CACHE_DB_* */ });
export const codingstairsPool = new Pool({ /* CODINGSTAIRS_DB_* */ });
export const da2ariPool = new Pool({ /* DA2ARI_DB_* */ });

환경변수 prefix (DMDDKSL_DB_* · PRYZEET_DB_* 등) 로 도메인을 분리하는 게 .env 읽기에 친절. 각 풀은 프로세스 생명주기 동안 1 개 싱글톤.

3. 얇은 쿼리 헬퍼

풀 직접 호출은 타입 유추가 약합니다. 각 풀마다 얇은 래퍼를 두면 IDE 힌트 · 코드 리뷰 · grep 모두 편해집니다.

// codingstairs-db.ts
export async function queryCodingstairs<T>(
  sql: string, params: unknown[] = []
): Promise<T[]> {
  const { rows } = await codingstairsPool.query<T>(sql, params);
  return rows;
}

export async function queryOneCodingstairs<T>(
  sql: string, params: unknown[] = []
): Promise<T | null> {
  const rows = await queryCodingstairs<T>(sql, params);
  return rows[0] ?? null;
}

pool.query(sql) 직접 사용은 grep 시 의도 파악이 어렵고, 타입 주석을 호출 측마다 달아야 합니다. 헬퍼로 묶으면 호출 측이 await queryCodingstairs<Post>(...) 만.

4. 트랜잭션 — connect() + try / finally

여러 테이블을 원자적으로 쓰려면 풀에서 클라이언트를 체크아웃.

export async function createPostWithTags(post, tags) {
  const client = await codingstairsPool.connect();
  try {
    await client.query('BEGIN');
    const { rows } = await client.query(
      'INSERT INTO posts (...) VALUES (...) RETURNING id',
      [...]
    );
    const postId = rows[0].id;
    for (const tag of tags) {
      await client.query(
        'INSERT INTO post_tags (post_id, tag) VALUES ($1, $2)',
        [postId, tag]
      );
    }
    await client.query('COMMIT');
    return postId;
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();                  // 누락하면 풀 고갈
  }
}

release() 누락이 풀 고갈의 대표 원인. withPoolClient<T>() 같은 헬퍼로 감싸 두면 실수를 줄일 수 있습니다.

5. SSL 설정

Supabase · RDS · 클라우드 PG 에 붙을 때 SSL 필요.

export function sslConfig(
  mode?: string,
  connectionString?: string
): Pool['options']['ssl'] {
  const isSupabase = connectionString?.includes('.supabase.');
  if (isSupabase) return { rejectUnauthorized: false };
  if (mode === 'require') return { rejectUnauthorized: true };
  return false;                        // 로컬 / Docker 간 SSL 미사용
}

클라우드는 rejectUnauthorized: true 가 안전. Supabase 는 연결 풀러가 자체 인증서를 쓰는 특수성 때문에 false 가 관례. 로컬 컨테이너 간은 SSL 없음.

6. 풀 해지 — graceful shutdown

const pools = [dmddkslPool, pryzeetPool, cachePool, codingstairsPool, da2ariPool];

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

Next.js 서버 재시작 · Docker stop 시 요청을 끝까지 처리한 후 풀을 닫아 커넥션 누수를 방지.

7. 도메인 라우팅 규칙

"이 API 가 어느 풀을 써야 하는가" 가 명확하지 않으면 혼선. 몇 가지 규칙으로 묶어 두면 리뷰가 쉬워집니다.

  • /api/pryzeet/** → pryzeetPool
  • /api/codingstairs/** → codingstairsPool
  • /api/da2ari/** → da2ariPool
  • 감사로그 · 세션 · FCM 토큰 등 전역 → dmddkslPool · cachePool 같은 "cross-cutting" 풀

audit_logs 같은 전역 테이블은 한 풀에 고정하는 편이 단순. "도메인 마다 audit_logs 를 따로 두자" 는 확장은 첫 1 ~ 2 년은 오히려 과도한 경우가 많음.

8. 자주 걸리는 자리

환경변수 오타 — DMDDKSL_DB_HOST 를 DMDDSKL_DB_HOST 로 철자 실수하면 빈 문자열 → pg 가 localhost 로 연결 시도 → 이해 안 되는 에러. requireEnv() 헬퍼로 부재 시 즉시 throw.

풀 max 너무 낮음 / 높음 — 기본 10 이 많으나 동시 요청이 많은 관리자 페이지는 금방 대기. 반대로 50 + 이면 PG 쪽이 과부하. 운영 로그의 pg_stat_activity 로 실제 사용량 측정 후 조정.

같은 풀에 너무 긴 트랜잭션 — 마이그레이션 + 대량 INSERT 가 10 초 넘게 잡히면 다른 요청이 커넥션 대기. 별도 admin 풀 (max:2) 을 따로 두거나, 장시간 작업은 별도 프로세스 · 백그라운드 잡으로 분리.

풀을 import 만 하고 종료 안 함 — 로컬 스크립트 실행 시 await pool.end() 를 부르지 않으면 Node 프로세스가 커넥션 대기로 안 죽음. 스크립트 끝에 명시적 종료.

하고픈 말

여러 풀을 들고 가는 패턴은 단일 DB 보다 운영 복잡도가 확실히 높습니다. 하지만 도메인이 셋 이상이고 각기 다른 라이프사이클 (크롤링 DB 백업 · 블로그 DB revalidate · Supabase 외부 관리) 을 가지면 분리가 오히려 단순합니다. 시작은 "도메인 수 = 풀 수" 로, 필요 시 전역 cross-cutting 풀 1 ~ 2 개를 추가하는 순서가 자연스럽습니다.

Next

  • postgres-first
  • postgres-deep
  • backend/09-audit-log-pattern

node-postgres · Supabase Connection Pooling · AWS RDS Best Practices 를 참고합니다.

data 카테고리의 다른 글

카테고리 전체 보기 →
  • DB 시드 소스를 코드 트리 안에 두지 않는다
  • Supabase Storage — 파일 업로드와 권한
  • Kafka 실무 — 토픽 설계와 메시지 흐름
  • 백업과 복구
  • 이미지 파이프라인
  • 푸시 알림 — FCM 과 Web Push