10편. Prisma 쿼리 실패 후 Pool에서 Connection이 제거되는 이유
📚 목차
1. 문제 맥락: 왜 CREATE 실패 시 Connection 이 바로 제거될까?
2. pg Pool 내부: _release() 로 보는 Connection 생명주기
3. Prisma 7 + PostgreSQL Adapter에서 Pool 사용 구조
4. Prisma · Pool · pg-adapter 로깅 코드 예시
1. 문제 맥락: 왜 CREATE 실패 시 Connection 이 바로 제거될까?
실제 서비스를 구현하고 테스트하는 과정에서 다음과 같은 패턴을 관찰할 수 있습니다.
일반적으로 connection pool을 사용하는 경우, 쿼리 실행이 완료되면 사용하던 connection은 pool로 반환되어 재사용된다고 이해하는 것이 보편적입니다.
그러나 본 사례에서는 쿼리 실행 중 오류가 발생한 경우, 해당 connection이 pool로 반환되지 않고 즉시 제거(remove) 되는 로그가 함께 관찰됩니다.
이는 단순한 구현상의 특이점이 아니라, connection 안정성을 우선시하는 pool의 내부 정책에 따른 동작임을 확인할 수 있습니다.

이 현상은 Prisma 문제가 아니라 pg.Pool의 의도된 설계입니다.
핵심은 pg.Pool 내부의 _release() 로직에 있습니다.
// node_modules/pg-pool/index.js
if (err || this.ending || !client._queryable || client._ending) {
return this._remove(client)
}
여기서 중요한 점은 다음과 같습니다.
▸ Prisma 7에서 PostgreSQL Adapter는 쿼리 실행 결과에 error가 존재하면 해당 error를 그대로 pg Pool에 전달합니다.
▸ pg Pool은 이 error를 “connection이 신뢰할 수 없는 상태가 되었을 가능성”으로 해석합니다.
▸ 그 결과, 해당 connection은 idle queue로 반환되지 않고 즉시 제거(remove) 됩니다.
즉,
쿼리 중 error가 발생한 connection은 “오염(possibly broken)”된 것으로 간주되어 pool에서 제거된다
PostgreSQL은 트랜잭션 오류 발생 시 connection 상태가 비정상(aborted) 이 될 수 있고, 이를 재사용하면 다음 쿼리까지 연쇄 실패가 발생할 수 있기 때문입니다.
2. pg Pool 내부: _release() 로 보는 Connection 생명주기
아래는 pg-pool 코드를 기준으로 실제 생명주기 분기점만 정리한 것입니다.
_release(client, idleListener, err) {
// ─────────────────────────────────────
// 1️⃣ 즉시 제거 조건
// "이 client는 다시 쓰면 안 된다"
// ─────────────────────────────────────
if (
err || // 방금 사용 중 에러가 발생함
this.ending || // pool 자체가 종료 중임
!client._queryable || // client가 정상 동작 상태가 아님
client._ending || // client가 이미 종료 수순에 있음
client._poolUseCount >= maxUses // 사용 횟수 한도를 초과함
) {
// idle로 돌려보내지 않고 바로 제거
return remove(client)
}
// ─────────────────────────────────────
// 2️⃣ 만료된(expired) client
// "정상처럼 보여도 교체 대상"
// ─────────────────────────────────────
if (expired.has(client)) {
expired.delete(client)
return remove(client)
}
// ─────────────────────────────────────
// 3️⃣ idle timeout 설정
// "pool 여유가 있을 때만 정리"
// ─────────────────────────────────────
if (idleTimeoutMillis && aboveMin()) {
setTimeout(() => {
// timeout 시점에도 여전히 여유가 있으면 제거
if (aboveMin()) {
remove(client)
}
})
}
// ─────────────────────────────────────
// 4️⃣ 정상 client
// "다음 요청을 위해 idle 상태로 보관"
// ─────────────────────────────────────
idle.push(new IdleItem(client, listener, timer))
}
3. Prisma 7 + PostgreSQL Adapter에서 Pool 사용 구조
1. 전체 구조 흐름
▸ Prisma는 Connection Pool을 직접 관리하지 않습니다
▸ Prisma는 adapter에게 “쿼리를 실행해 달라”는 요청만 위임합니다
▸ Connection의 생성, 재사용, 제거에 대한 모든 책임은 100% pg.Pool에 위임되어 있습니다
Prisma Client (JS/TS)
└─ Prisma Query Engine (Rust, WASM/Native)
└─ driver-adapter (@prisma/adapter-pg)
└─ pg.Pool
└─ pg.Client
└─ net.Socket → PostgreSQL
이 구조에서 Prisma Query Engine은 SQL을 생성하고 실행 요청을 전달하는 역할만 수행하며,
실제 네트워크 연결 및 connection 생명주기는 전적으로 pg.Pool이 관리합니다.
따라서 CREATE 쿼리 실패 이후 connection 이 제거되는 현상은 Prisma의 동작이라기보다는
pg.Pool의 내부 정책에 따른 정상적인 결과로 이해하는 것이 맞습니다.
2. Prisma 7 PostgreSQL Adapter 예시
다음은 Prisma 7 환경에서 PostgreSQL adapter와 pg.Pool을 함께 구성하는 대표적인 예시입니다.
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import { Pool } from 'pg'
import { env } from './env'
/**
* PostgreSQL Connection Pool
*/
const pool = new Pool({
// PostgreSQL DSN
connectionString: 'postgresql://app:secret@localhost:5432/app',
max: 20, // 동시에 유지할 최대 connection 수
min: 2, // 최소 유지 connection (warm pool)
maxUses: 1000, // connection 1개당 최대 쿼리 수
idleTimeoutMillis: 30_000, // 30초 idle 시 제거
maxLifetimeSeconds: 1800, // 30분 후 강제 재생성
connectionTimeoutMillis: 2_000, // 2초 내 connection 획득 실패 시 에러
})
/**
* Prisma PostgreSQL Adapter
* - Prisma Query Engine이 pg.Pool을 사용하도록 연결
* - Prisma는 직접 DB에 연결하지 않음
*/
const adapter = new PrismaPg(pool)
/**
* Prisma Client
* - 애플리케이션에서 사용하는 ORM 진입점
* - 모든 쿼리는 adapter → pg.Pool → PostgreSQL 경로로 실행됨
*/
export const prisma = new PrismaClient({
adapter,
// 쿼리 / 에러 / 경고 로그를 이벤트로 수신
// connection remove 원인 추적 시 유용
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'event' },
{ level: 'warn', emit: 'event' },
],
})
3. Pool 옵션 설계 기준
| 옵션 | 역할 |
| max | 최대 connection 수 |
| min | 최소 유지 connection |
| maxUses | connection 사용 수 제한 |
| idleTimeoutMillis | idle 제거 |
| maxLifetimeSeconds | 수명 제한 |
| connectionTimeoutMillis | acquire timeout |
4. Prisma · Pool · pg-adapter 로깅 코드 예시
1. pg Pool 이벤트 로깅 - Connection 생명주기 관찰하기
PostgreSQL connection 이 언제 생성되고, 재사용되며, 제거되는지를 이해하려면 pg.Pool 이 제공하는 이벤트 훅(event emitter) 을 활용하면 됩니다.
//새로운 PostgreSQL connection 이 실제로 생성되었을 때 호출된다.
pool.on('connect', (client) => {
console.log('[PG] connect', client.processID)
})
//Pool 에서 connection 하나를 "빌려서" 실제 쿼리를 수행하기 직전에 호출된다.
pool.on('acquire', (client) => {
console.log('[PG] acquire', client.processID)
})
// 쿼리 실행이 끝난 뒤 connection 을 Pool 로 반환할 때 호출된다.
pool.on('release', (err, client) => {
console.log('[PG] release', {
pid: client.processID,
error: !!err,
})
})
// Pool 에서 connection 이 완전히 제거될 때 호출된다.
pool.on('remove', (client) => {
console.log('[PG] remove', client.processID)
})
2. Prisma 쿼리 로그 - Pool 이벤트와 원인 연결하기
Pool 로그만으로는 “왜 에러가 났는지” 알 수 없습니다.
이를 연결해 주는 것이 Prisma Query / Error 이벤트 로그입니다.
const prisma = new PrismaClient({
adapter,
// Prisma Client 에서 발생하는 쿼리를 이벤트 기반으로 수신하도록 설정한다.
log: [
{ level: 'query', emit: 'event' }, // 실제 실행된 SQL
{ level: 'error', emit: 'event' }, // Prisma 레벨 에러
],
})
// Prisma Client 에서
// 실제 DB 로 전달된 SQL 을 그대로 확인할 수 있다.
// - SELECT / INSERT / CREATE / UPDATE 모두 포함
prisma.$on('query', (e) => {
console.log('[PRISMA QUERY]', e.query)
})
// Prisma 레벨에서 인지한 에러
// 이 에러가 발생하면
// → pg.Pool 의 release(err) 로 이어지고
// → connection 이 remove 될 가능성이 생긴다.
prisma.$on('error', (e) => {
console.error('[PRISMA ERROR]', e.message)
})
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Prisma(ORM)' 카테고리의 다른 글
| [Prisma7] 9편. Node.js + Prisma 에러 종류 및 처리 방법 (0) | 2026.02.02 |
|---|---|
| [Prisma7] 8편. Prisma 데이터베이스 동기화(Migration & Sync)실습 : 개발 → 운영 (0) | 2026.01.22 |
| [Prisma7] 7편. Prisma 트랜잭션과 데이터 정합성: 실무 설계와 구현 (0) | 2026.01.21 |
| [Prisma7] 6편. Prisma로 해결되지 않는 쿼리 다루기: Raw SQL 실전 활용 (0) | 2026.01.20 |
| [Prisma7] 5편. Prisma 관계 조회 심화: include / select와 중첩 관계 탐색 (0) | 2026.01.19 |