deep-dive2025-06-15·12 min·23/348

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 init

Problem

핫 리로딩 시 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

  1. globalThis 사용: Next.js의 핫 리로딩과 호환되는 유일한 방법
  2. 연결 풀 제한: connection_limit 파라미터로 동시 연결 수를 제한
  3. 그레이스풀 셧다운: 프로세스 종료 시 연결 해제 필수
  4. 환경별 로깅: 개발 환경에서는 쿼리 로깅, 프로덕션에서는 에러만

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