3단계
여러 PostgreSQL 풀 연결
30 분
여러 PostgreSQL 풀 연결
관리자 앱이 3 개 도메인 DB 를 직접 다루려면 풀 3 개. pg 드라이버만으로 충분합니다.
1. 풀 싱글톤
// src/shared/lib/db.ts
import { Pool } from 'pg';
function sslConfig(mode?: string): any {
if (mode === 'require') return { rejectUnauthorized: true };
return false;
}
function requireEnv(name: string): 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'),
ssl: sslConfig(process.env.BLOG_DB_SSL_MODE),
max: 10,
});
export const marketPool = new Pool({
/* MARKET_DB_* */
});
requireEnv가 부재 시 즉시 throw → 오타 · 환경설정 누락을 런타임 첫 호출에서 드러남max: 10이 기본. 동시 요청이 많으면pg_stat_activity관찰 후 조정
2. 얇은 쿼리 헬퍼
도메인별 헬퍼를 두면 호출 측 코드가 짧아지고 grep 이 명확해집니다.
// src/shared/lib/blog-db.ts
import { blogPool } from './db';
export async function queryBlog<T>(
sql: string,
params: unknown[] = []
): Promise<T[]> {
const { rows } = await blogPool.query<T>(sql, params);
return rows;
}
export async function queryOneBlog<T>(
sql: string,
params: unknown[] = []
): Promise<T | null> {
const rows = await queryBlog<T>(sql, params);
return rows[0] ?? null;
}
호출 측:
const posts = await queryBlog<Post>(
`SELECT id, title FROM posts WHERE published = true ORDER BY created_at DESC LIMIT $1`,
[20]
);
3. 트랜잭션
풀에서 클라이언트를 체크아웃해 BEGIN / COMMIT.
export async function createPostWithTags(post, tags) {
const client = await blogPool.connect();
try {
await client.query('BEGIN');
const { rows } = await client.query(
`INSERT INTO posts (title, body) VALUES ($1, $2) RETURNING id`,
[post.title, post.body]
);
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();
}
}
finally { client.release() } 누락이 풀 고갈의 대표 원인. withPoolClient<T>() 래퍼를 직접 만들어도 좋습니다.
4. graceful shutdown
// src/app/api/health/route.ts 에 가까운 곳에 1 회 등록
const pools = [blogPool, marketPool];
if (typeof process !== 'undefined') {
process.once('SIGTERM', async () => {
await Promise.all(pools.map((p) => p.end()));
});
}
Docker stop · Next 재시작 시 커넥션을 깨끗이 닫음.
5. 페이지 예 — 블로그 포스트 목록
// src/app/admin/blog/posts/page.tsx
import { queryBlog } from '@/shared/lib/blog-db';
export default async function Page() {
const posts = await queryBlog<Post>(
`SELECT id, title, published, created_at FROM posts ORDER BY created_at DESC LIMIT 50`
);
return <PostsView initialRows={posts} />;
}
type Post = {
id: number;
title: string;
published: boolean;
created_at: string;
};
Server Component 에서 직접 풀 호출 → PostsView 에 props 로 전달. 'use client' 는 PostsView 내부에만.
6. 자주 걸리는 자리
- 환경변수 오타 →
requireEnv로 즉시 실패 max너무 큼 → PG 쪽max_connections초과- 트랜잭션 미종결 →
client.release()누락 추적 - 로컬 · Docker · 클라우드 간 SSL 설정 혼동
하고픈 말
풀 싱글톤 + 얇은 헬퍼 조합은 ORM 없이도 충분히 버팁니다. Drizzle · TypeORM 을 당기는 건 join 이 복잡해지거나 마이그레이션 자동화가 필요해지는 중반부 — 3 ~ 5 개 테이블 수준에서는 raw SQL 이 더 빠르고 읽기 쉽습니다.
Next
- 04-resource-table-ssot