Prisma를 이용한 데이터베이스 작업
Prisma ORM 7의 스키마, 마이그레이션, 생성 클라이언트를 Nest provider로 연결하고 PostgreSQL을 조회합니다.
앞 절들에서는 NestJS에서 TypeORM과 Mongoose를 이용해 데이터베이스를 연결했습니다.
이 절에서는 PostgreSQL을 예로 들어 Prisma ORM 7을 NestJS에 통합합니다. Prisma는 선언한 데이터 모델에서 타입이 있는 쿼리 API를 생성하고, 관계형 데이터베이스의 변경 이력을 SQL 마이그레이션으로 관리합니다. 데이터베이스별 지원 범위와 설정은 Prisma 버전에 따라 다르므로 실제 프로젝트의 provider 문서를 함께 확인해야 합니다.
Prisma의 세 구성 요소
Prisma를 사용할 때는 다음 세 역할을 구분해야 합니다.
- Prisma Schema:
schema.prisma에 generator, datasource provider, 모델과 관계를 선언합니다. Prisma ORM 7에서 연결 URL은schema.prisma가 아니라prisma.config.ts의 datasource 설정에 둡니다. - Prisma Client: 모델과 generator 설정으로부터 생성되는 타입 안전 쿼리 빌더입니다. ORM 7의
prisma-clientgenerator는 명시적인output경로가 필요하며, 실행 시 데이터베이스별 driver adapter를 받습니다. - Prisma Migrate: Prisma Schema의 모델 변경으로 SQL migration 파일을 만들고 관계형 데이터베이스에 적용합니다.
migrate dev는 개발용이고, 배포 환경에는 검토·커밋한 이력을migrate deploy로 적용합니다.
생성 타입은 필드 이름, 쿼리 인자, 선택한 필드와 관계에 따른 결과 형태를 컴파일 단계에서 확인하게 해 줍니다. 그러나 HTTP 요청 값의 유효성이나 비즈니스 규칙까지 보장하지는 않으므로 Nest DTO와 Pipe 검증은 별도로 유지해야 합니다.
Prisma 연동하기
이 예제는 Nest의 일반적인 CommonJS 설정과 PostgreSQL을 사용합니다.
CLI와 런타임 의존성 설치
npm install --save-dev prisma
npm install @prisma/client @prisma/adapter-pg pg dotenv @nestjs/config
npx prisma init --output ../src/generated/prismaprisma는 스키마·마이그레이션·생성을 실행하는 개발 도구입니다. 애플리케이션에는 생성 클라이언트의 런타임과 PostgreSQL driver adapter가 필요합니다. prisma init은 prisma/schema.prisma, prisma.config.ts, .env의 기본 구조를 만듭니다.
schema.prisma 정의
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}output은 schema.prisma를 기준으로 한 생성 코드의 위치입니다. 기본 Nest 프로젝트가 CommonJS를 사용하므로 여기서는 moduleFormat = "cjs"로 맞춥니다. 애플리케이션이 ESM이라면 프로젝트의 module 형식과 import 경로를 함께 맞춰야 합니다.
CLI용 연결 설정
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});DATABASE_URL="postgresql://username:password@localhost:5432/app?schema=public"prisma.config.ts는 Prisma CLI가 마이그레이션이나 introspection을 실행할 때 사용할 URL을 제공합니다. .env의 비밀값은 Git에 커밋하지 말고, 배포 환경에서는 secret manager나 플랫폼 환경 변수로 주입합니다.
개발 migration과 Client 생성
npx prisma migrate dev --name init
npx prisma generatemigrate dev는 개발 데이터베이스에서 migration을 만들고 적용합니다. Prisma ORM 7에서는 Client 생성을 자동 실행하지 않으므로 이어서 prisma generate를 명시적으로 실행해야 합니다. prisma generate는 데이터베이스에 접속하지 않고 generator와 모델로 Client를 다시 만들며, schema 변경 뒤나 production build 전에 실행합니다.
production과 staging에서는 migration 파일을 먼저 검토하고 저장소에 커밋한 뒤 다음 명령으로 pending migration만 적용합니다.
npx prisma migrate deployNest provider로 Client 인스턴스 공유
import { Injectable } from '@nestjs/common';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error('DATABASE_URL is required');
}
super({
adapter: new PrismaPg({ connectionString }),
});
}
}Prisma ORM 7의 PrismaClient는 database driver adapter가 필요합니다. 기본 singleton scope의 PrismaService를 주입하면 요청마다 새 Client와 연결 풀을 만들지 않고 한 인스턴스를 재사용할 수 있습니다. Client는 첫 쿼리에서 지연 연결하므로 일반적인 장기 실행 서버에서 $connect()를 직접 호출할 필요는 없습니다.
모듈 경계에서 provider 내보내기
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { UsersService } from './users.service';
@Module({
imports: [PrismaModule],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UsersModule } from './users/users.module';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), UsersModule],
})
export class AppModule {}PrismaModule을 전역으로 만들지 않고 실제 소비 모듈이 import하게 하면 provider 의존성이 모듈 경계에 드러납니다. prisma.config.ts의 dotenv 로딩은 CLI 프로세스용이므로 Nest 런타임도 ConfigModule 같은 방식으로 환경 변수를 로드해야 합니다.
스키마 변경이 migration, 생성 코드, Nest 런타임으로 전파되는 경계는 다음과 같습니다.
Nest · Prisma ORM 7
스키마 변경은 두 산출물로 전파된다
migration은 데이터베이스 구조를 바꾸고, generated Client는 Nest 코드가 컴파일하고 실행할 모델 API를 바꿉니다.
개발·빌드 시점
schema.prisma— 모델과 관계, datasource provider, generator와 output을 선언합니다. 연결 URL은prisma.config.ts가 제공합니다.migrate dev— 개발 환경에서 migration SQL을 만들고 적용합니다. Prisma ORM 7에서는 Client를 자동 생성하지 않습니다. 검토·커밋한 migration은 배포 환경에서migrate deploy로 적용합니다.prisma generate— schema의 모델 API와 타입을 명시한 output 경로에 다시 생성합니다. 애플리케이션 build는 이 generated Client를 import합니다.
Nest 실행 시점
PrismaService— generatedPrismaClient를 상속하고PrismaPgdriver adapter를 전달한 singleton provider입니다.도메인 Service — 소비 모듈이
PrismaModule을 import하고PrismaService를 생성자로 주입받아 모델 API를 호출합니다.Driver와 database — adapter가 연결 풀과 PostgreSQL 통신을 담당합니다. migration의 구조 변경과 런타임 레코드 쿼리는 같은 DB 경계에 도달하지만 서로 다른 명령 경로입니다.
migrate dev는 개발용 변경 경로이고 migrate deploy는 검토된 pending migration의 배포 경로입니다. generated Client는 DB 구조 자체가 아니라 Nest 코드가 import하는 타입과 쿼리 API입니다.
Prisma Client 사용하기
UsersService는 PrismaService를 주입받아 생성된 모델 API를 호출합니다.
import { Injectable } from '@nestjs/common';
import { Post, Prisma, User } from '../generated/prisma/client';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({ data });
}
findAllUsers(): Promise<User[]> {
return this.prisma.user.findMany();
}
findUserById(id: number): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
}
updateUser(
id: number,
data: Prisma.UserUpdateInput,
): Promise<User> {
return this.prisma.user.update({
where: { id },
data,
});
}
deleteUser(id: number): Promise<User> {
return this.prisma.user.delete({ where: { id } });
}
getUserPosts(userId: number): Promise<Post[]> {
return this.prisma.post.findMany({
where: { authorId: userId },
});
}
}Prisma.UserCreateInput과 Prisma.UserUpdateInput은 현재 schema에서 생성되므로 모델 필드가 바뀌면 서비스의 컴파일 피드백도 함께 바뀝니다. findUnique()는 행이 없을 때 null을 반환하고, update()와 delete()는 대상 레코드가 없으면 성공값 대신 오류를 반환하므로 애플리케이션의 not-found 처리도 별도로 설계해야 합니다.
관계 데이터를 한 쿼리 결과에 포함하려면 include나 select를 사용하고, 특정 사용자의 게시물 목록만 필요하면 위 예제처럼 post.findMany()에 관계 키를 조건으로 주는 방식이 명확합니다. 쿼리 수와 실행 계획은 작성한 관계 쿼리와 database provider에 따라 달라지므로 “Prisma가 N+1을 자동으로 없앤다”라고 일반화해서는 안 됩니다.
Prisma 통합의 핵심은 세 경계를 섞지 않는 것입니다.
schema.prisma는 모델과 generator, datasource provider의 선언입니다.- migration은 데이터베이스 구조 변경 이력이고, generated Client는 애플리케이션이 컴파일할 타입과 쿼리 API입니다.
- Nest의
PrismaService는 한 Client 인스턴스를 DI로 공유하지만 DTO 검증, 비즈니스 규칙, 오류 변환까지 대신하지는 않습니다.
TypeORM, Mongoose, Prisma를 비교할 때는 데이터 모델, 트랜잭션 요구, migration 운영 방식, provider 지원 범위, 복잡한 쿼리의 표현력과 실행 계획을 함께 확인해야 합니다.