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 openAnalysis
연결 풀 동작 방식
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
- 싱글톤 패턴: Prisma 클라이언트는 싱글톤으로 관리하여 연결 고갈을 방지하세요
- 연결 풀 설정: 애플리케이션 규모에 맞게 연결 풀 크기를 조정하세요
- 트랜잭션 활용: 여러 작업을 원자적으로 처리하기 위해 트랜잭션을 사용하세요
- 모니터링: 쿼리 성능과 에러를 지속적으로 모니터링하세요
- Graceful Shutdown: 애플리케이션 종료 시 연결을 올바르게 해제하세요
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.