icon
3장 : 데이터베이스 통합

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


안녕하세요! NestJS와 함께 TypeORM, Mongoose를 이용한 데이터베이스 연동을 살펴보았습니다. 이번에는 또 다른 강력한 데이터베이스 툴인 Prisma를 NestJS에 통합하는 방법을 알아보겠습니다. Prisma는 스스로를 "Next-generation ORM"이라고 칭하며, 기존 ORM들이 가졌던 여러 단점들을 보완하고 개발자 경험(Developer Experience, DX)을 극대화하는 데 중점을 둡니다.

Prisma는 TypeScript 친화적인 데이터베이스 툴로, 특히 생성된 클라이언트(Generated Client), 마이그레이션(Migration) 도구, 그리고 데이터베이스 스키마 정의 언어를 통해 현대적인 웹 애플리케이션 개발에 필요한 모든 것을 제공합니다. 관계형 데이터베이스와 NoSQL 데이터베이스(MongoDB)를 모두 지원하며, 기존 ORM과는 다른 접근 방식을 제공하여 더욱 안정적이고 효율적인 데이터베이스 작업을 가능하게 합니다.


Prisma란 무엇이며 왜 주목해야 할까요?

Prisma는 단순히 ORM을 넘어선 데이터베이스 툴킷(Database Toolkit) 입니다. 주요 구성 요소는 다음과 같습니다.

  • Prisma Schema: 직관적인 스키마 정의 언어(SDL)를 사용하여 데이터베이스 모델과 관계를 정의합니다.
  • Prisma Client: Prisma Schema를 기반으로 자동 생성되는 타입-세이프(Type-safe)한 쿼리 빌더입니다. 데이터베이스와 상호작용하는 주된 방법이며, TypeScript 환경에서 강력한 자동 완성 기능을 제공합니다.
  • Prisma Migrate: 데이터베이스 스키마 변경 사항을 관리하고 적용하는 마이그레이션 도구입니다.

Prisma 사용의 주요 장점

  • 탁월한 타입 안정성: Prisma Client는 여러분의 데이터베이스 스키마를 기반으로 완벽한 타입 안정성을 제공합니다. 이는 런타임 오류를 줄이고 개발 생산성을 크게 향상시킵니다.
  • 강력한 자동 완성: TypeScript 환경에서 Prisma Client 메서드 사용 시 IDE의 강력한 자동 완성 지원을 받아 오타 및 잘못된 쿼리를 방지할 수 있습니다.
  • 쉬운 마이그레이션 관리: Prisma Migrate는 데이터베이스 스키마 변경 이력을 체계적으로 관리하고 쉽게 적용할 수 있도록 돕습니다.
  • 유연한 관계 관리: 복잡한 관계형 데이터 모델을 직관적으로 정의하고 다룰 수 있습니다.
  • 성능 최적화: Lazy-loading과 같은 N+1 쿼리 문제를 방지하기 위한 기능들을 제공합니다.

기존 ORM들이 객체를 데이터베이스 테이블에 매핑하는 데 중점을 두었다면, Prisma는 스키마 정의부터 클라이언트 생성, 마이그레이션까지 데이터베이스 작업의 전체 라이프사이클을 아우르는 통합된 경험을 제공합니다.


Prisma 연동하기: 핵심 개념 및 설정

NestJS와 Prisma를 연동하는 과정은 크게 다음과 같습니다.

Prisma CLI 설치 및 초기화: 먼저 Prisma CLI를 개발 의존성으로 설치하고 프로젝트를 초기화합니다.

npm install prisma --save-dev
npx prisma init

npx prisma init 명령어를 실행하면 prisma 디렉토리와 그 안에 schema.prisma 파일이 생성됩니다. 또한 .env 파일에 DATABASE_URL 환경 변수 설정 예시가 추가됩니다.

schema.prisma 파일 정의: prisma/schema.prisma 파일에서 데이터베이스 연결 설정, 데이터 모델, 그리고 Prisma Client 설정을 정의합니다.

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  // 사용할 데이터베이스 종류 (postgresql, mysql, sqlite, sqlserver, mongodb)
  provider = "postgresql"
  url      = env("DATABASE_URL") // .env 파일에서 데이터베이스 URL을 가져옵니다.
}

// 사용자 모델 정의
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String? // 선택적 필드
  posts     Post[]   // User는 여러 Post를 가질 수 있습니다 (1:N 관계)
  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]) // Post는 한 User에 속합니다.
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
  • generator client: Prisma Client를 생성할 것임을 나타냅니다.
  • datasource db: 사용할 데이터베이스의 종류와 연결 URL을 정의합니다. env("DATABASE_URL")은 환경 변수에서 URL을 가져오도록 설정합니다.
  • model User, model Post: 각각 users 테이블과 posts 테이블에 매핑될 데이터 모델을 정의합니다. @id, @unique, @default(), @relation() 등의 속성을 사용하여 필드의 특성과 관계를 명시합니다.

환경 변수 설정: 프로젝트 루트에 있는 .env 파일에 데이터베이스 연결 URL을 설정합니다.

# .env
DATABASE_URL="postgresql://username:password@localhost:5432/your_database_name?schema=public"
# 또는 MySQL: DATABASE_URL="mysql://username:password@localhost:3306/your_database_name"
# 또는 MongoDB: DATABASE_URL="mongodb://username:password@localhost:27017/your_database_name"

참고: DATABASE_URL은 여러분의 실제 데이터베이스 연결 정보로 대체해야 합니다. 보안을 위해 이 파일은 Git에 포함되지 않도록 .gitignore에 추가하는 것이 좋습니다.

Prisma Client 생성: 스키마를 정의했으면, 다음 명령어를 실행하여 Prisma Client를 생성합니다.

npx prisma generate

이 명령어를 실행하면 node_modules/@prisma/client 경로에 여러분의 스키마에 맞는 타입-세이프한 클라이언트 코드가 생성됩니다.

마이그레이션 적용 (선택 사항): 데이터베이스 스키마를 데이터베이스에 반영하려면 마이그레이션을 실행합니다.

npx prisma migrate dev --name init # 'init'은 마이그레이션 이름입니다.

이 명령은 prisma/migrations 디렉토리에 마이그레이션 파일을 생성하고, 데이터베이스에 스키마를 적용합니다.

NestJS와 Prisma Client 통합: Prisma Client를 NestJS 애플리케이션에서 사용하려면, 이를 NestJS의 의존성 주입 시스템에 등록해야 합니다. 일반적으로 전용 PrismaService를 생성하여 관리합니다.

src/prisma/prisma.service.ts
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; // 생성된 Prisma Client 임포트

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    // 애플리케이션 시작 시 데이터베이스 연결
    await this.$connect();
  }

  async onModuleDestroy() {
    // 애플리케이션 종료 시 데이터베이스 연결 해제
    await this.$disconnect();
  }

  // 선택 사항: Graceful Shutdown을 위한 메서드 추가 (NestJS 공식 문서 권장)
  async enableShutdownHooks(app: INestApplication) {
    process.on('beforeExit', async () => {
      await app.close();
    });
  }
}

그리고 PrismaModule을 만들어 PrismaService를 관리하고 다른 모듈에서 사용할 수 있도록 exports합니다.

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

@Global() // 이 모듈을 전역 모듈로 선언하여 모든 곳에서 PrismaService를 사용할 수 있게 합니다.
@Module({
  providers: [PrismaService],
  exports: [PrismaService], // PrismaService를 외부에 노출
})
export class PrismaModule {}

루트 모듈(AppModule)에서 PrismaModuleimports합니다.

src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module'; // PrismaModule 임포트
import { UsersModule } from './users/users.module'; // 사용자 모듈 (PrismaService를 사용할 모듈)

@Module({
  imports: [PrismaModule, UsersModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

Prisma Client 사용하기

PrismaService가 준비되었으니, 이제 서비스에서 PrismaService를 주입받아 데이터베이스 작업을 수행할 수 있습니다.

src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; // PrismaService 임포트
import { User, Post } from '@prisma/client'; // Prisma Client에서 생성된 타입 임포트

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {} // PrismaService 주입

  async createUser(email: string, name?: string): Promise<User> {
    return this.prisma.user.create({
      data: {
        email,
        name,
      },
    });
  }

  async findAllUsers(): Promise<User[]> {
    return this.prisma.user.findMany();
  }

  async findUserById(id: number): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

  async updateUser(id: number, name: string): Promise<User> {
    return this.prisma.user.update({
      where: { id },
      data: { name },
    });
  }

  async deleteUser(id: number): Promise<User> {
    return this.prisma.user.delete({
      where: { id },
    });
  }

  async getUserPosts(userId: number): Promise<Post[]> {
    return this.prisma.user.findUnique({
      where: { id: userId },
      include: { posts: true }, // 관계된 posts도 함께 가져옵니다.
    }).posts(); // Prisma 4.0 부터 findUnique의 결과에서 직접 관계된 데이터를 가져올 수 있습니다.
  }
}

위 코드에서 보듯이, this.prisma.user와 같이 모델 이름을 통해 접근하며, create, findMany, findUnique, update, delete 등 직관적인 메서드를 사용하여 데이터베이스 작업을 수행합니다. 특히 TypeScript 환경에서는 각 메서드의 인자와 반환 타입에 대한 강력한 타입 체크와 자동 완성 기능이 제공되어 개발 편의성이 매우 높습니다.


Prisma는 데이터베이스와의 상호작용 방식에 혁신을 가져오며, 특히 TypeScript 프로젝트에서 그 진가를 발휘합니다. 기존 ORM의 복잡성 없이 간결하고 타입-세이프한 방식으로 데이터베이스를 다루고 싶다면, Prisma는 훌륭한 선택이 될 것입니다.

이것으로 3장 "데이터베이스 통합"을 마무리하겠습니다. 이제 여러분은 NestJS 애플리케이션에 관계형 데이터베이스(TypeORM), NoSQL 데이터베이스(MongoDB), 그리고 차세대 ORM인 Prisma를 통합하는 방법을 모두 익히셨습니다. 여러분의 프로젝트 요구사항에 가장 적합한 데이터베이스 솔루션을 선택하고 성공적으로 연동하시길 바랍니다.