deep-dive2025-05-06·8·87/348

Node.js에서 Prisma 클라이언트 연결 관리

Prisma ORM을 사용한 데이터베이스 연결을 효과적으로 관리하는 방법과 최적화 기법을 다룹니다.

Introduction

Prisma는 현대적인 Node.js/TypeScript ORM으로, 타입 안전한 데이터베이스 접근을 제공합니다. 이번 포스트에서는 Prisma 클라이언트의 연결을 효과적으로 관리하는 방법을 살펴보겠습니다.

Environment

npm list prisma @prisma/client
# prisma@5.8.1
# @prisma/client@5.8.1

node --version
# v20.11.0

# Prisma 스키마
cat prisma/schema.prisma
# datasource db {
#   provider = "postgresql"
#   url      = env("DATABASE_URL")
# }

Problem

Prisma 클라이언트 사용 시 연결 문제가 발생했습니다:

const { PrismaClient } = require('@prisma/client');

// 매번 새 인스턴스 생성 시도
async function getUser(id) {
    const prisma = new PrismaClient(); // 문제!
    const user = await prisma.user.findUnique({ where: { id } });
    return user;
}

// 결과: 연결 풀 고갈 에러
// Error: Too many database connections open

Analysis

연결 풀 동작 방식

Prisma Client
    │
    ├─ Connection 1 ──→ DB
    ├─ Connection 2 ──→ DB
    ├─ Connection 3 ──→ DB
    └─ Connection 4 ──→ DB (풀 크기)

일반적인 문제점

문제원인해결책
연결 고갈인스턴스 다중 생성싱글톤 패턴
연결 누수연결 미해제graceful shutdown
성능 저하설정 미최적화연결 풀 튜닝

Solution

1. 싱글톤 Prisma 클라이언트

// lib/prisma.js
const { PrismaClient } = require('@prisma/client');

let prisma;

if (process.env.NODE_ENV === 'production') {
    prisma = new PrismaClient();
} else {
    // 개발 환경에서는 전역에 저장하여 핫 리로드 시 연결 유지
    if (!global.prisma) {
        global.prisma = new PrismaClient();
    }
    prisma = global.prisma;
}

module.exports = prisma;

2. 연결 풀 설정

// prisma/schema.prisma
datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

generator client {
    provider = "prisma-client-js"
    binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

// lib/prisma.js
const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient({
    datasources: {
        db: {
            url: process.env.DATABASE_URL + '?connection_limit=5&pool_timeout=10',
        },
    },
    log: [
        { emit: 'event', level: 'query' },
        { emit: 'stdout', level: 'error' },
        { emit: 'stdout', level: 'warn' },
    ],
});

// 쿼리 로깅
prisma.$on('query', (e) => {
    console.log('Query: ' + e.query);
    console.log('Duration: ' + e.duration + 'ms');
});

3. 연결 관리 유틸리티

class PrismaManager {
    constructor() {
        this.client = null;
    }

    async connect() {
        if (!this.client) {
            this.client = new PrismaClient({
                datasources: {
                    db: {
                        url: process.env.DATABASE_URL,
                    },
                },
            });

            // 연결 확인
            await this.client.$connect();
            console.log('Prisma connected successfully');
        }
        return this.client;
    }

    async disconnect() {
        if (this.client) {
            await this.client.$disconnect();
            this.client = null;
            console.log('Prisma disconnected');
        }
    }

    getClient() {
        if (!this.client) {
            throw new Error('Prisma client not initialized');
        }
        return this.client;
    }
}

// 싱글톤 인스턴스
const prismaManager = new PrismaManager();

module.exports = prismaManager;

4. 트랜잭션 처리

const prisma = require('./lib/prisma');

// 기본 트랜잭션
async function transferMoney(fromId, toId, amount) {
    const result = await prisma.$transaction(async (tx) => {
        const from = await tx.account.update({
            where: { id: fromId },
            data: { balance: { decrement: amount } },
        });

        const to = await tx.account.update({
            where: { id: toId },
            data: { balance: { increment: amount } },
        });

        return { from, to };
    });

    return result;
}

// 인터랙티브 트랜잭션
async function createUserWithProfile(userData, profileData) {
    return await prisma.$transaction(async (tx) => {
        const user = await tx.user.create({ data: userData });

        const profile = await tx.profile.create({
            data: {
                ...profileData,
                userId: user.id,
            },
        });

        return { user, profile };
    });
}

5. 연결 상태 모니터링

const prisma = require('./lib/prisma');

class ConnectionMonitor {
    constructor(prismaClient) {
        this.prisma = prismaClient;
        this.metrics = {
            queries: 0,
            errors: 0,
            slowQueries: 0,
        };
    }

    startMonitoring() {
        // 쿼리 이벤트 리스닝
        this.prisma.$on('query', (event) => {
            this.metrics.queries++;

            if (event.duration > 1000) {
                this.metrics.slowQueries++;
                console.warn('Slow query detected:', {
                    query: event.query,
                    duration: event.duration,
                });
            }
        });

        // 에러 이벤트 리스닝
        this.prisma.$on('error', (event) => {
            this.metrics.errors++;
            console.error('Prisma error:', event);
        });
    }

    getMetrics() {
        return this.metrics;
    }

    resetMetrics() {
        this.metrics = {
            queries: 0,
            errors: 0,
            slowQueries: 0,
        };
    }
}

// 사용 예시
const monitor = new ConnectionMonitor(prisma);
monitor.startMonitoring();

6. Graceful Shutdown

const express = require('express');
const prisma = require('./lib/prisma');

const app = express();

// 기존 라우트...

const server = app.listen(3000);

// Graceful shutdown
const gracefulShutdown = async () => {
    console.log('Starting graceful shutdown...');

    server.close(async () => {
        console.log('HTTP server closed');

        await prisma.$disconnect();
        console.log('Prisma disconnected');

        process.exit(0);
    });

    // 강제 종료 타임아웃
    setTimeout(() => {
        console.error('Forced shutdown');
        process.exit(1);
    }, 10000);
};

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

Lessons Learned

  1. 싱글톤 패턴: Prisma 클라이언트는 싱글톤으로 관리하여 연결 고갈을 방지하세요
  2. 연결 풀 설정: 애플리케이션 규모에 맞게 연결 풀 크기를 조정하세요
  3. 트랜잭션 활용: 여러 작업을 원자적으로 처리하기 위해 트랜잭션을 사용하세요
  4. 모니터링: 쿼리 성능과 에러를 지속적으로 모니터링하세요
  5. Graceful Shutdown: 애플리케이션 종료 시 연결을 올바르게 해제하세요

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.