본문 바로가기

Prisma를 이용한 데이터베이스 작업

Prisma ORM 7의 스키마, 마이그레이션, 생성 클라이언트를 Nest provider로 연결하고 PostgreSQL을 조회합니다.

앞 글들에서는 TypeORM과 Mongoose로 데이터베이스를 연결했습니다. 이 글에서는 PostgreSQL을 예로 들어 Prisma ORM 7을 NestJS에 통합합니다. Prisma는 선언한 데이터 모델에서 타입이 있는 쿼리 API를 생성하고, 관계형 데이터베이스의 변경 이력을 SQL 마이그레이션으로 관리합니다. 데이터베이스마다 지원 범위와 설정이 Prisma 버전에 따라 다르므로, 실제 프로젝트에서는 해당 데이터베이스의 문서를 함께 확인합니다.


Prisma의 세 구성 요소

Prisma를 쓸 때는 세 역할을 구분합니다. Prisma 스키마는 schema.prisma에 생성기(generator), 데이터소스의 종류, 모델과 관계를 선언하며, Prisma ORM 7에서는 연결 URL을 schema.prisma가 아니라 prisma.config.ts의 데이터소스 설정에 둡니다. Prisma Client는 모델과 생성기 설정으로부터 만들어지는 타입 안전한 쿼리 빌더로, ORM 7의 prisma-client 생성기는 명시적인 output 경로가 필요하고 실행할 때 데이터베이스별 드라이버 어댑터(driver adapter)를 받습니다. Prisma Migrate는 스키마의 모델 변경으로 SQL 마이그레이션 파일을 만들어 데이터베이스에 적용하며, migrate dev는 개발용이고 배포 환경에는 검토하고 커밋한 이력을 migrate deploy로 적용합니다.

생성된 타입은 필드 이름, 쿼리 인자, 선택한 필드와 관계에 따른 결과의 모양을 컴파일 단계에서 확인하게 해 줍니다. 그러나 HTTP 요청 값의 유효성이나 업무 규칙까지 보장하지는 않으므로, Nest의 DTO와 파이프 검증은 따로 유지합니다.


Prisma 연동하기

이 예제는 이 교재가 1장에서 고른 CommonJS 구성과 PostgreSQL을 씁니다. 연동은 패키지 설치, schema.prisma 선언, CLI용 연결 설정, 마이그레이션과 클라이언트 생성, Nest 프로바이더와 모듈 등록의 순서로 진행합니다. 이 가운데 스키마, 연결 설정, 마이그레이션 명령은 Prisma CLI가 쓰는 것이고, PrismaService와 모듈 등록은 Nest 애플리케이션이 실행 중에 쓰는 코드입니다. 다른 데이터베이스나 ESM 프로젝트라면 드라이버 어댑터 패키지와 생성기의 moduleFormat이 달라집니다.

CLI와 런타임 의존성 설치

새 프로젝트에서 다음 두 줄을 한 번 실행합니다. CLI, 클라이언트, 어댑터의 주 버전을 모두 7로 고정합니다.

npm install --save-dev prisma@7
npm install @prisma/client@7 @prisma/adapter-pg@7 pg dotenv @nestjs/config

버전을 고정하는 이유가 있습니다. 2026년 11월 현재 npm에서 @prisma/client의 latest 태그는 7.10.1이지만, prisma CLI 패키지의 latest 태그는 8.0의 출시 후보(RC) 버전을 가리킵니다. 그래서 버전 없이 npm install --save-dev prisma를 실행하면 CLI와 클라이언트의 주 버전이 어긋날 수 있습니다. 설치한 뒤에는 npm ls prisma @prisma/client @prisma/adapter-pg로 실제 버전을 확인하고 잠금 파일을 보관합니다. ORM 7의 Node.js와 TypeScript 요구 사항은 공식 업그레이드 안내에서 확인합니다.

이어서 생성 코드의 위치를 지정해 프로젝트를 초기화합니다. 이미 초기화를 마친 프로젝트라면 다시 실행하지 않고, 생성된 파일을 다음 단계에 맞춥니다.

npx prisma init --output ../src/generated/prisma

prisma는 스키마, 마이그레이션, 코드 생성을 실행하는 개발 도구이고, 애플리케이션에는 생성 클라이언트의 런타임과 PostgreSQL 드라이버 어댑터가 필요합니다. prisma init은 prisma/schema.prisma, prisma.config.ts, .env의 기본 구조를 만듭니다.

schema.prisma 정의

prisma/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이라면 프로젝트의 모듈 형식과 import 경로를 함께 맞춥니다.

CLI용 연결 설정

prisma.config.ts
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'),
  },
});
.env
DATABASE_URL="postgresql://username:password@localhost:5432/app?schema=public"

prisma.config.ts는 Prisma CLI가 마이그레이션이나 데이터베이스 구조 읽기(introspection)를 실행할 때 쓸 URL을 제공합니다. .env의 비밀 값은 Git에 커밋하지 말고, 배포 환경에서는 비밀 정보 관리 도구나 플랫폼의 환경 변수로 주입합니다.

개발용 마이그레이션과 클라이언트 생성

npx prisma migrate dev --name init
npx prisma generate

migrate dev는 개발 데이터베이스에서 마이그레이션을 만들고 적용합니다. Prisma ORM 7에서는 이때 클라이언트를 자동으로 생성하지 않으므로, 이어서 prisma generate를 직접 실행해야 합니다. prisma generate는 데이터베이스에 접속하지 않고 생성기와 모델로 클라이언트를 다시 만들며, 스키마를 바꾼 뒤나 운영용 빌드 전에 실행합니다.

운영과 스테이징 환경에서는 마이그레이션 파일을 먼저 검토하고 저장소에 커밋한 뒤, 다음 명령으로 아직 적용되지 않은 마이그레이션만 적용합니다.

npx prisma migrate deploy

Nest 프로바이더로 클라이언트 공유

src/prisma/prisma.service.ts
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는 데이터베이스 드라이버 어댑터가 필요합니다. 기본 싱글턴 범위의 PrismaService를 주입하면, 요청마다 새 클라이언트와 연결 풀을 만들지 않고 한 인스턴스를 다시 씁니다. 클라이언트는 첫 쿼리에서 연결을 맺으므로, 오래 실행되는 일반적인 서버에서는 $connect()를 직접 호출할 필요가 없습니다.

모듈에서 프로바이더 내보내기

src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
src/users/users.module.ts
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 {}
src/app.module.ts
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을 전역 모듈로 만들지 않고 실제로 쓰는 모듈이 가져오게 하면, 프로바이더 의존 관계가 모듈 구조에 드러납니다. prisma.config.ts의 dotenv 로딩은 CLI 프로세스를 위한 것이므로, Nest 런타임도 ConfigModule 같은 방식으로 환경 변수를 따로 불러와야 합니다.

스키마의 변경이 마이그레이션, 생성 코드, Nest 런타임으로 전해지는 경로는 다음과 같습니다.

Prisma 스키마 변경과 Nest 런타임 경계

schema 변경으로 migration과 generated Client라는 독립 산출물을 만든 뒤 Nest DI/runtime 조회에 사용한다.

Nest Prisma 7 · 두 개발 산출물

Schema 변경은 DB migration과 생성 Client로 나뉩니다

migration은 데이터베이스 구조를 바꾸고, prisma generate는 Nest 코드가 import하는 모델 API와 타입을 바꿉니다. 두 산출물의 명령 경로와 런타임의 provider·driver adapter 경계를 구분합니다.

Prisma 개발 산출물과 Nest 런타임 아키텍처 schema.prisma의 모델과 generator 설정은 migrate dev를 통해 migration 파일과 데이터베이스 변경으로 이어지고, prisma generate를 통해 generated Client로 이어진다. Nest 실행 시 UsersService는 generated Client를 상속하고 PostgreSQL driver adapter를 구성한 PrismaService를 주입받아 데이터베이스를 조회한다. DEVELOP · BUILD NEST RUNTIME IMPORT APPLY schema.prisma models · relations DB provider generator · output 선언 기준 migrate dev development only migration SQL commit · deploy prisma generate build artifact Generated Client model API · types UsersService constructor DI 비즈니스 작업 PrismaService singleton provider Client 인스턴스 공유 PrismaPg driver adapter 연결 풀 · 쿼리 전달 PostgreSQL DATABASE_URL schema · records
Prisma의 두 개발 산출물과 런타임 경계 같은 schema 선언의 migration은 DB 구조를 바꾸고 generate는 Client 타입과 API를 만든다. 두 개발 경로는 서로 대체하지 않는다. 런타임은 UsersService가 PrismaService를 주입받고 생성 Client와 PostgreSQL adapter로 DB 레코드에 접근한다. 점선은 generated Client의 import 의존성이다. 개발 산출물 ① 구조 개발 산출물 ② API Nest 실행 경로 schema.prisma 모델 · generator 선언 migration DB 구조 변경 DB 구조 개발 적용 / 배포 적용 schema.prisma 같은 선언에서 API 생성 generate 타입 · 쿼리 API Generated Client Nest 코드가 import UsersService PrismaService 주입 PrismaService Client 공유 · adapter PrismaPg → DB 연결 풀 · 레코드 쿼리

개발·빌드 시점

  1. schema.prisma — 모델과 관계, datasource provider, generator와 output을 선언합니다. 연결 URL은 prisma.config.ts가 제공합니다.

  2. migrate dev — 개발 환경에서 migration SQL을 만들고 적용합니다. Prisma ORM 7에서는 Client를 자동 생성하지 않습니다. 검토·커밋한 migration은 배포 환경에서 migrate deploy로 적용합니다.

  3. prisma generate — schema의 모델 API와 타입을 명시한 output 경로에 다시 생성합니다. 애플리케이션 build는 이 generated Client를 import합니다.

Nest 실행 시점

  1. PrismaService — generated PrismaClient를 상속하고 PrismaPg driver adapter를 전달한 singleton provider입니다.

  2. 도메인 Service — 소비 모듈이 PrismaModule을 import하고 PrismaService를 생성자로 주입받아 모델 API를 호출합니다.

  3. Driver와 database — adapter가 연결 풀과 PostgreSQL 통신을 담당합니다. migration의 구조 변경과 런타임 레코드 쿼리는 같은 DB 경계에 도달하지만 서로 다른 명령 경로입니다.

migrate dev는 개발용 변경 경로이고 migrate deploy는 검토된 pending migration의 배포 경로입니다. generated Client는 DB 구조 자체가 아니라 Nest 코드가 import하는 타입과 쿼리 API입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • schema.prisma의 모델과 generator 설정은 migrate dev를 통해 migration 파일과 데이터베이스 변경으로 이어지고, prisma generate를 통해 generated Client로 이어진다. Nest 실행 시 UsersService는 generated Client를 상속하고 PostgreSQL driver adapter를 구성한 PrismaService를 주입받아 데이터베이스를 조회한다.
  • 같은 schema 선언의 migration은 DB 구조를 바꾸고 generate는 Client 타입과 API를 만든다. 두 개발 경로는 서로 대체하지 않는다. 런타임은 UsersService가 PrismaService를 주입받고 생성 Client와 PostgreSQL adapter로 DB 레코드에 접근한다. 점선은 generated Client의 import 의존성이다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Prisma 스키마 변경과 Nest 런타임 경계
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
schema 산출물 두 경로

schema가 migration·DB 변경과 generated Client를 각각 만듭니다.

명령: migrate·generate · 산출물: 독립 두 경로하나의 schema에서 서로 다른 준비물이 생깁니다.
Nest DI 연결

UsersService가 driver adapter를 구성한 PrismaService를 받습니다.

주입: PrismaService · adapter: PostgreSQL준비한 Client를 runtime graph에 연결합니다.
runtime 조회

generated Client 기반 서비스가 DB를 조회합니다.

시점: runtime · 대상: DB · CLI: 요청마다 실행 아님빌드 준비물을 실제 업무가 사용합니다.

Prisma 스키마 변경과 Nest 런타임 경계

schema 변경으로 migration과 generated Client라는 독립 산출물을 만든 뒤 Nest DI/runtime 조회에 사용한다.

Nest Prisma 7 · 두 개발 산출물

Schema 변경은 DB migration과 생성 Client로 나뉩니다

migration은 데이터베이스 구조를 바꾸고, prisma generate는 Nest 코드가 import하는 모델 API와 타입을 바꿉니다. 두 산출물의 명령 경로와 런타임의 provider·driver adapter 경계를 구분합니다.

Prisma 개발 산출물과 Nest 런타임 아키텍처 schema.prisma의 모델과 generator 설정은 migrate dev를 통해 migration 파일과 데이터베이스 변경으로 이어지고, prisma generate를 통해 generated Client로 이어진다. Nest 실행 시 UsersService는 generated Client를 상속하고 PostgreSQL driver adapter를 구성한 PrismaService를 주입받아 데이터베이스를 조회한다. DEVELOP · BUILD NEST RUNTIME IMPORT APPLY schema.prisma models · relations DB provider generator · output 선언 기준 migrate dev development only migration SQL commit · deploy prisma generate build artifact Generated Client model API · types UsersService constructor DI 비즈니스 작업 PrismaService singleton provider Client 인스턴스 공유 PrismaPg driver adapter 연결 풀 · 쿼리 전달 PostgreSQL DATABASE_URL schema · records
Prisma의 두 개발 산출물과 런타임 경계 같은 schema 선언의 migration은 DB 구조를 바꾸고 generate는 Client 타입과 API를 만든다. 두 개발 경로는 서로 대체하지 않는다. 런타임은 UsersService가 PrismaService를 주입받고 생성 Client와 PostgreSQL adapter로 DB 레코드에 접근한다. 점선은 generated Client의 import 의존성이다. 개발 산출물 ① 구조 개발 산출물 ② API Nest 실행 경로 schema.prisma 모델 · generator 선언 migration DB 구조 변경 DB 구조 개발 적용 / 배포 적용 schema.prisma 같은 선언에서 API 생성 generate 타입 · 쿼리 API Generated Client Nest 코드가 import UsersService PrismaService 주입 PrismaService Client 공유 · adapter PrismaPg → DB 연결 풀 · 레코드 쿼리

개발·빌드 시점

  1. schema.prisma — 모델과 관계, datasource provider, generator와 output을 선언합니다. 연결 URL은 prisma.config.ts가 제공합니다.

  2. migrate dev — 개발 환경에서 migration SQL을 만들고 적용합니다. Prisma ORM 7에서는 Client를 자동 생성하지 않습니다. 검토·커밋한 migration은 배포 환경에서 migrate deploy로 적용합니다.

  3. prisma generate — schema의 모델 API와 타입을 명시한 output 경로에 다시 생성합니다. 애플리케이션 build는 이 generated Client를 import합니다.

Nest 실행 시점

  1. PrismaService — generated PrismaClient를 상속하고 PrismaPg driver adapter를 전달한 singleton provider입니다.

  2. 도메인 Service — 소비 모듈이 PrismaModule을 import하고 PrismaService를 생성자로 주입받아 모델 API를 호출합니다.

  3. Driver와 database — adapter가 연결 풀과 PostgreSQL 통신을 담당합니다. migration의 구조 변경과 런타임 레코드 쿼리는 같은 DB 경계에 도달하지만 서로 다른 명령 경로입니다.

migrate dev는 개발용 변경 경로이고 migrate deploy는 검토된 pending migration의 배포 경로입니다. generated Client는 DB 구조 자체가 아니라 Nest 코드가 import하는 타입과 쿼리 API입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • schema.prisma의 모델과 generator 설정은 migrate dev를 통해 migration 파일과 데이터베이스 변경으로 이어지고, prisma generate를 통해 generated Client로 이어진다. Nest 실행 시 UsersService는 generated Client를 상속하고 PostgreSQL driver adapter를 구성한 PrismaService를 주입받아 데이터베이스를 조회한다.
  • 같은 schema 선언의 migration은 DB 구조를 바꾸고 generate는 Client 타입과 API를 만든다. 두 개발 경로는 서로 대체하지 않는다. 런타임은 UsersService가 PrismaService를 주입받고 생성 Client와 PostgreSQL adapter로 DB 레코드에 접근한다. 점선은 generated Client의 import 의존성이다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Prisma 스키마 변경과 Nest 런타임 경계
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
schema 산출물 두 경로

schema가 migration·DB 변경과 generated Client를 각각 만듭니다.

명령: migrate·generate · 산출물: 독립 두 경로하나의 schema에서 서로 다른 준비물이 생깁니다.
Nest DI 연결

UsersService가 driver adapter를 구성한 PrismaService를 받습니다.

주입: PrismaService · adapter: PostgreSQL준비한 Client를 runtime graph에 연결합니다.
runtime 조회

generated Client 기반 서비스가 DB를 조회합니다.

시점: runtime · 대상: DB · CLI: 요청마다 실행 아님빌드 준비물을 실제 업무가 사용합니다.

그림은 같은 스키마 선언에서 두 개발 산출물이 나온다는 점을 보여 줍니다. 마이그레이션은 DB 구조를 바꾸고, generate는 Nest 코드가 가져다 쓸 클라이언트의 타입과 쿼리 API를 만들며, 두 경로는 서로를 대신하지 않습니다. 실행 시점에는 UsersService가 PrismaService를 주입받고, 생성된 클라이언트가 PrismaPg 어댑터를 거쳐 PostgreSQL의 레코드에 접근합니다. 점선은 생성된 클라이언트를 import하는 의존 관계입니다.

더 깊이: 드라이버 어댑터가 바꾼 연결 풀의 기본값

ORM 7의 prisma-client 생성기는 Rust로 만든 쿼리 엔진 없이 동작하는 클라이언트를 만들고, 그 대신 모든 데이터베이스에서 드라이버 어댑터를 요구합니다. 그래서 연결 풀도 Prisma가 아니라 어댑터에 넘긴 Node.js 드라이버가 관리하며, 연결 풀 문서에 따르면 그 기본값은 ORM 6과 크게 다릅니다. 이 글의 PrismaPg가 쓰는 pg 드라이버를 기준으로 비교하면 다음과 같습니다.

항목ORM 6의 기본값ORM 7과 pg의 기본값
풀 크기connection_limit, 물리 CPU 수 × 2 + 1max, 10
풀에서 연결을 얻기까지 기다리는 시간pool_timeout, 10초connectionTimeoutMillis, 0(제한 없음)
새 연결을 맺는 시간 제한connect_timeout, 5초connectionTimeoutMillis, 0(제한 없음)
쉬는 연결을 닫기까지의 시간max_idle_connection_lifetime, 300초idleTimeoutMillis, 10초

시간 제한이 0이라는 것은 DB가 응답하지 않거나 풀이 가득 찼을 때 요청이 오류 없이 오래 기다릴 수 있다는 뜻입니다. 풀 크기도 CPU 수와 상관없이 10으로 정해지므로, ORM 6에서 올린 서버는 같은 부하에서 동시 연결 수가 달라질 수 있습니다. 이런 값은 new PrismaPg({ connectionString, max: 20 })처럼 어댑터를 만들 때 넘기는 옵션으로 정하며, 문서는 ORM 6과 같은 시간 제한을 원하면 connectionTimeoutMillis를 5000, idleTimeoutMillis를 300000으로 두라고 안내합니다.


Prisma Client 사용하기

UsersService는 PrismaService를 주입받아 생성된 모델 API를 호출합니다. PrismaService가 PrismaClient를 상속했으므로 this.prisma.user와 this.prisma.post를 바로 쓸 수 있으며, 이 속성 이름은 스키마의 model User와 model Post에서 첫 글자를 소문자로 바꾼 것입니다. 각 메서드는 Prisma 쿼리가 돌려주는 Promise를 그대로 반환하므로 async를 붙이지 않았습니다.

src/users/users.service.ts
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은 지금의 스키마에서 생성되므로, 모델의 필드가 바뀌면 서비스의 컴파일 결과도 함께 바뀝니다. findUnique()는 행이 없으면 null을 돌려주지만, update()와 delete()는 대상 레코드가 없으면 성공 값 대신 오류를 던지므로, 애플리케이션의 '찾을 수 없음' 처리를 따로 설계해야 합니다.

PostgreSQL 서버, 데이터베이스, 계정은 연결 URL에 맞게 먼저 준비되어 있어야 합니다. prisma init이나 npm 설치가 DB 서버를 띄우지는 않습니다. 마이그레이션과 생성이 끝나고 UsersService를 주입받았다면, createUser({ email: '[email protected]', name: 'Alice' })가 돌려준 ID로 findUserById(id)를 호출해 저장과 조회가 이어지는지 확인합니다. 이 글에는 UsersController가 없으므로, 아무 HTTP 경로로 요청해서는 확인할 수 없습니다.

관계 데이터를 한 쿼리 결과에 함께 담으려면 include나 select를 쓰고, 특정 사용자의 게시물 목록만 필요하다면 위 예제처럼 post.findMany()에 관계 키를 조건으로 주는 방식이 분명합니다. 쿼리 수와 실행 계획은 작성한 관계 쿼리와 데이터베이스에 따라 달라지므로, Prisma가 N+1 문제를 저절로 없애 준다고 일반화하지 않습니다.

TypeORM, Mongoose, Prisma를 비교할 때는 데이터 모델, 트랜잭션 요구, 마이그레이션 운영 방식, 지원하는 데이터베이스, 복잡한 쿼리의 표현력과 실행 계획을 함께 봅니다.


정리

  • schema.prisma는 모델, 생성기, 데이터소스 종류의 선언이고, ORM 7의 연결 URL은 prisma.config.ts에 둡니다.
  • 마이그레이션은 DB 구조의 변경 이력이고 생성된 클라이언트는 애플리케이션이 컴파일할 타입과 쿼리 API이며, ORM 7에서는 migrate dev 뒤에 generate를 따로 실행합니다.
  • PrismaService는 드라이버 어댑터를 받은 클라이언트 하나를 DI로 공유하지만, DTO 검증, 업무 규칙, 오류 변환은 대신하지 않습니다.
  • prisma CLI의 latest 태그가 클라이언트와 다를 수 있으므로 주 버전을 고정해 설치하고 실제 버전을 확인합니다.

연습 문제

1. 두 산출물 가운데 하나만 만들면

User에 새 필드를 추가한 뒤 generate만 하고 DB 마이그레이션을 적용하지 않았습니다. 반대로 마이그레이션만 하고 클라이언트를 다시 생성하지 않은 경우도 있습니다. 두 경우에 새 필드를 쓰는 코드는 각각 어느 단계에서 막히나요?

해설 보기

generate만 하면 새 필드를 아는 클라이언트가 생기지만 DB 구조는 바뀌지 않으므로, 새 열을 쓰는 쿼리는 실행할 때 DB에서 실패합니다.

마이그레이션만 하면 DB는 바뀌어도 기존 클라이언트의 타입과 API가 새 필드를 모르므로, 그 필드를 쓰는 코드가 컴파일 단계에서 막힙니다. 생성 API를 다시 만들고 애플리케이션을 컴파일해야 합니다. 두 명령은 서로의 산출물을 대신하지 않으며, HTTP DTO에서 새 필드를 받을지는 또 따로 정합니다.

2. 없는 레코드에 대한 세 호출

ID 99인 사용자가 없을 때 findUserById(99), updateUser(99, { name: 'B' }), deleteUser(99)를 차례로 호출합니다. 각각 어떤 결과가 나오나요? 컨트롤러가 이 결과를 그대로 반환하거나 던지면 HTTP 응답은 어떻게 되나요?

해설 보기

findUserById(99)는 null을 돌려줍니다. 그대로 반환하면 Express 어댑터가 빈 본문으로 200을 보내므로, 없는 사용자가 성공처럼 보입니다.

updateUser와 deleteUser는 대상 레코드가 없어 Prisma가 PrismaClientKnownRequestError를 던지며, 오류 참조 문서의 코드는 P2025입니다. 이것은 HttpException이 아니므로 그대로 던지면 기본 예외 처리가 500으로 응답합니다. 404로 응답하려면 null을 확인하거나 P2025를 잡아 NotFoundException으로 바꾸는 처리가 필요합니다.

3. 버전 없이 설치하면

2026년 11월에 새 프로젝트에서 npm install @prisma/client @prisma/adapter-pg와 npm install --save-dev prisma를 버전 없이 실행했습니다. 이 글의 설정과 무엇이 어긋날 수 있으며, 어떻게 확인하고 바로잡나요?

해설 보기

npm은 버전이 없으면 각 패키지의 latest 태그를 설치합니다. 이때 @prisma/client는 7.10.1이지만 prisma CLI는 8.0의 출시 후보 버전이 설치되어, CLI와 클라이언트의 주 버전이 달라질 수 있습니다. 주 버전이 다르면 생성 코드와 런타임이 서로 기대하는 형식이 어긋날 수 있습니다.

npm ls prisma @prisma/client @prisma/adapter-pg로 실제 버전을 확인하고, npm install --save-dev prisma@7처럼 주 버전을 고정해 다시 설치합니다. 패키지마다 latest 태그가 언제나 안정 버전을 가리킨다고 가정하지 않고, 잠금 파일을 함께 보관합니다.

다음 글 트랜잭션 관리와 데이터 정합성에서는 여러 쓰기를 하나의 단위로 묶어, 일부만 성공하는 상태를 막는 방법을 다룹니다.