Next.js에서 Prisma 클라이언트 singleton 패턴 구현하기
Next.js 앱에서 Prisma 클라이언트의 singleton 패턴을 구현하여 개발 및 프로덕션 환경의 연결 풀 문제를 해결하는 방법을 알아봅니다.
Next.js에서 Prisma 클라이언트 singleton 패턴 구현하기
Introduction
Next.js 개발 환경에서 Prisma 클라이언트는 핫 리로딩으로 인해 여러 인스턴스가 생성될 수 있습니다. 이를 "Prisma client singleton 문제"라고 하며, 데이터베이스 연결 풀 고갈과 메모리 누수를 유발합니다. 이 글에서는 프로덕션 환경에서도 안정적으로 작동하는 singleton 패턴을 구현합니다.
Environment
# 프로젝트 구조
my-app/
├── lib/
│ └── prisma.js # singleton 인스턴스
├── prisma/
│ └── schema.prisma # 스키마 정의
├── app/
│ └── api/
│ └── users/
│ └── route.js
└── package.json
# 기술 스택
Next.js: 14.2.5
Prisma: 5.19.1
PostgreSQL: 15.4# Prisma 설치 및 초기화
npm install prisma @prisma/client
npx prisma initProblem
핫 리로딩 시 Prisma 클라이언트가 여러 번 인스턴스화됩니다:
// ❌ 문제점: 매번 새 인스턴스 생성
// lib/prisma.js
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
// 개발 환경에서 핫 리로딩마다 새 연결 생성
// "Too many database connections" 에러 발생# 에러 로그
Error: Too many database connections
at NodeEngine.connect (/node_modules/.prisma/client/engine.js:345:15)
at PrismaClient.connect (/node_modules/.prisma/client/runtime/index.js:2154:23)Analysis
Next.js의 개발 서버는 모듈 캐시를 사용합니다. 핫 리로딩 시 모듈이 다시 로드되지만, 글로벌 변수는 유지됩니다. 이를 이용하여 singleton을 구현합니다.
// singleton 패턴의 동작 원리
// 1. 개발 환경: globalThis에 인스턴스 저장
// 2. 프로덕션 환경: 일반 모듈 캐시 사용
// 3. 연결 풀: 단일 연결 공유Solution
1. 기본 singleton 구현
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export default prisma;2. 타입 안전한 singleton
// lib/prisma.ts
import { PrismaClient, Prisma } from '@prisma/client';
// Prisma 클라이언트 타입 확장
type ExtendedPrismaClient = PrismaClient<
Prisma.ClientOptions,
never,
never,
{
log: Array<{
emit: 'event';
level: 'query';
}>;
}
>;
const globalForPrisma = globalThis as unknown as {
prisma: ExtendedPrismaClient | undefined;
};
function createPrismaClient(): ExtendedPrismaClient {
return new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
}
export const prisma: ExtendedPrismaClient =
globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// Graceful shutdown
process.on('beforeExit', async () => {
await prisma.$disconnect();
});
export default prisma;3. 연결 풀 설정
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
// 연결 풀 설정
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
// 로깅 설정
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'stdout',
level: 'error',
},
{
emit: 'stdout',
level: 'warn',
},
],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
// Prisma 이벤트 리스너
prisma.$on('query', (e) => {
console.log('Query: ' + e.query);
console.log('Duration: ' + e.duration + 'ms');
});
export default prisma;4. 환경 변수 설정
# .env.local
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=5&pool_timeout=30"// lib/prisma.ts에 환경 변수 검증 추가
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']),
});
const env = envSchema.parse(process.env);
export const prisma = new PrismaClient({
datasources: {
db: {
url: env.DATABASE_URL,
},
},
});Lessons Learned
- globalThis 사용: Next.js의 핫 리로딩과 호환되는 유일한 방법
- 연결 풀 제한:
connection_limit파라미터로 동시 연결 수를 제한 - 그레이스풀 셧다운: 프로세스 종료 시 연결 해제 필수
- 환경별 로깅: 개발 환경에서는 쿼리 로깅, 프로덕션에서는 에러만
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.