4.Node.js/Prisma(ORM)

[Prisma7] 10편. 쿼리 실패 후 Pool에서 Connection이 제거되는 이유

쿼드큐브 2026. 2. 4. 08:44
반응형
반응형

 

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 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형