본문으로 건너뛰기
Previous
Next
Prisma 완벽 가이드: 차세대 TypeScript ORM

Prisma 완벽 가이드: 차세대 TypeScript ORM

Prisma 완벽 가이드: 차세대 TypeScript ORM

이 글의 핵심

Prisma는 타입 안전하고 직관적인 차세대 ORM으로 Prisma Schema로 DB를 정의하고 자동 생성된 타입 안전 클라이언트로 쿼리합니다. 마이그레이션, Prisma Studio GUI, 강력한 관계 쿼리로 생산성이 높습니다.

Prisma란?

Prisma는 차세대 TypeScript ORM(Object-Relational Mapping)으로, 타입 안전성과 개발자 경험을 극대화한 데이터베이스 툴킷입니다.

핵심 구성 요소

  1. Prisma Schema

    • 선언적 데이터 모델
    • 단일 진실 공급원
    • 마이그레이션 생성
  2. Prisma Client

    • 자동 생성 쿼리 빌더
    • 완벽한 타입 안전성
    • 자동완성 지원
  3. Prisma Migrate

    • 스키마 마이그레이션
    • 버전 관리
    • 롤백 지원
  4. Prisma Studio

    • GUI 데이터 브라우저
    • 데이터 편집
    • 시각적 탐색

TypeORM vs Sequelize vs Prisma 비교

항목TypeORMSequelizePrisma
언어TypeScriptJavaScript/TSTypeScript
스키마 정의데코레이터 클래스모델 클래스Schema 파일
타입 안전성좋음보통매우 좋음
마이그레이션CLICLIMigrate CLI
Raw SQL지원지원지원
관계 쿼리Eager/LazyIncludeInclude
N+1 해결수동수동자동
GUIPrisma Studio
학습 곡선중간중간낮음

프로젝트 설정

Prisma를 시작하려면 먼저 프로젝트에 필요한 패키지를 설치해야 합니다. Prisma는 개발 의존성(devDependencies)으로 설치하고, Prisma Client는 런타임에 필요하므로 일반 의존성(dependencies)으로 설치합니다.

설치 후 npx prisma init 명령어를 실행하면 prisma 폴더와 schema.prisma 파일이 자동으로 생성됩니다. 이 파일이 데이터베이스 스키마를 정의하는 중심 파일입니다. 데이터베이스 종류에 따라 적절한 provider를 선택할 수 있습니다.

# Prisma 설치
npm install -D prisma
npm install @prisma/client

# Prisma 초기화
npx prisma init

# PostgreSQL 사용 시
npx prisma init --datasource-provider postgresql

# MySQL 사용 시
npx prisma init --datasource-provider mysql

초기화 후 .env 파일에 DATABASE_URL을 설정해야 합니다. 이 URL은 데이터베이스 연결 정보를 담고 있으며, 환경에 따라 다른 값을 사용할 수 있습니다.

디렉터리 구조

my-project/
├── prisma/
│   ├── schema.prisma     # 스키마 정의
│   └── migrations/       # 마이그레이션 파일
├── src/
│   └── index.ts
├── .env                  # DATABASE_URL
└── package.json

Prisma Schema

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

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
  @@map("users")  // 테이블명 지정
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  userId Int     @unique
  user   User    @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  tags      Tag[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
  @@index([published])
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

마이그레이션

# 마이그레이션 생성 및 적용
npx prisma migrate dev --name init

# 프로덕션 적용
npx prisma migrate deploy

# 마이그레이션 상태 확인
npx prisma migrate status

# 마이그레이션 리셋 (개발 전용)
npx prisma migrate reset

# Prisma Client 재생성
npx prisma generate

Prisma Client 사용

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Create
const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: '홍길동',
    posts: {
      create: [
        { title: '첫 번째 글' },
        { title: '두 번째 글' }
      ]
    }
  }
});

// Read
const users = await prisma.user.findMany();
const user = await prisma.user.findUnique({
  where: { email: '[email protected]' }
});

// Include 관계
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true,
    profile: true
  }
});

// Update
const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: '김철수' }
});

// Delete
await prisma.user.delete({
  where: { id: 1 }
});

고급 쿼리

필터링

// AND 조건
const users = await prisma.user.findMany({
  where: {
    AND: [
      { age: { gte: 20 } },
      { role: 'USER' }
    ]
  }
});

// OR 조건
const users = await prisma.user.findMany({
  where: {
    OR: [
      { email: { contains: 'gmail.com' } },
      { email: { contains: 'naver.com' } }
    ]
  }
});

// NOT 조건
const users = await prisma.user.findMany({
  where: {
    NOT: { role: 'ADMIN' }
  }
});

관계 필터링

// 포스트가 있는 사용자만
const users = await prisma.user.findMany({
  where: {
    posts: {
      some: {
        published: true
      }
    }
  }
});

// 특정 태그가 있는 포스트
const posts = await prisma.post.findMany({
  where: {
    tags: {
      some: {
        name: 'typescript'
      }
    }
  },
  include: {
    author: true,
    tags: true
  }
});

집계

// 카운트
const userCount = await prisma.user.count();
const activeUsers = await prisma.user.count({
  where: { isActive: true }
});

// 그룹화 집계
const stats = await prisma.user.groupBy({
  by: ['role'],
  _count: { id: true },
  _avg: { age: true }
});

트랜잭션

// 연속 트랜잭션
const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: '[email protected]' } }),
  prisma.post.create({ data: { title: 'Hello' } })
]);

// 대화형 트랜잭션
await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { email: '[email protected]' }
  });
  
  await tx.post.create({
    data: {
      title: 'Hello',
      authorId: user.id
    }
  });
  
  // 에러 발생 시 자동 롤백
});

성능 최적화

Prisma는 기본적으로 성능이 우수하지만, 대규모 애플리케이션에서는 추가 최적화가 필요합니다. 가장 중요한 것은 필요한 데이터만 조회하는 것입니다.

Select를 사용하면 필요한 필드만 조회해 네트워크 전송량과 메모리 사용량을 줄일 수 있습니다. 특히 BLOB 필드나 긴 텍스트 필드가 있는 경우 큰 차이가 납니다.

// Select로 필요한 필드만
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    email: true
  }
});

// 페이지네이션
const users = await prisma.user.findMany({
  skip: 10,
  take: 10
});

// 커서 기반 페이지네이션
const users = await prisma.user.findMany({
  take: 10,
  cursor: {
    id: lastUserId
  },
  skip: 1
});

페이지네이션은 두 가지 방식이 있습니다. Offset 기반(skip/take)은 구현이 간단하지만 데이터가 많으면 느려집니다. 커서 기반은 일관된 성능을 제공하지만 이전 페이지로 이동하기 어렵습니다.

실전 사례: 블로그 시스템 구축

실무에서 Prisma를 사용해 블로그 시스템을 구축하는 과정을 살펴보겠습니다. 사용자, 포스트, 댓글, 태그의 관계를 효율적으로 모델링하고 쿼리하는 방법을 다룹니다.

N+1 문제 자동 해결

전통적인 ORM에서는 N+1 문제가 자주 발생합니다. 예를 들어 10개의 포스트를 조회하고 각 포스트의 작성자를 조회하면 총 11번의 쿼리가 실행됩니다(1번: 포스트 목록, 10번: 각 작성자). Prisma는 include를 사용하면 자동으로 JOIN이나 IN 쿼리로 최적화합니다.

// Prisma는 자동으로 최적화된 쿼리 생성
const posts = await prisma.post.findMany({
  include: {
    author: {
      select: {
        name: true,
        email: true
      }
    },
    tags: true
  }
});

이 쿼리는 내부적으로 효율적인 JOIN 또는 2번의 쿼리(포스트, 태그)로 실행됩니다. 개발자가 직접 최적화를 신경 쓸 필요가 없습니다.

복잡한 필터링 구현

실무에서는 여러 조건을 조합한 복잡한 검색 기능이 자주 필요합니다. Prisma의 조합 연산자(AND, OR, NOT)를 사용하면 복잡한 쿼리도 타입 안전하게 작성할 수 있습니다.

// 게시된 포스트 중 특정 태그가 있거나 제목에 키워드가 포함된 포스트
const posts = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      {
        OR: [
          {
            tags: {
              some: {
                name: { in: ['typescript', 'prisma'] }
              }
            }
          },
          {
            title: {
              contains: '튜토리얼',
              mode: 'insensitive'  // 대소문자 무시
            }
          }
        ]
      }
    ]
  },
  include: {
    author: true,
    tags: true,
    _count: {
      select: { comments: true }
    }
  },
  orderBy: {
    createdAt: 'desc'
  }
});

이런 복잡한 쿼리도 타입 안전성이 보장되어 오타나 잘못된 필드명 사용을 컴파일 타임에 잡을 수 있습니다.

트랜잭션으로 데이터 일관성 보장

포스트를 삭제할 때 관련된 댓글과 태그 관계도 함께 삭제해야 합니다. 트랜잭션을 사용하면 모든 작업이 성공하거나 모두 실패하도록 보장할 수 있습니다.

async function deletePostWithRelations(postId: number) {
  return await prisma.$transaction(async (tx) => {
    // 댓글 삭제
    await tx.comment.deleteMany({
      where: { postId }
    });
    
    // 포스트 삭제 (태그 관계는 onDelete: Cascade로 자동 삭제)
    await tx.post.delete({
      where: { id: postId }
    });
    
    // 사용되지 않는 태그 정리
    await tx.tag.deleteMany({
      where: {
        posts: {
          none: {}
        }
      }
    });
  });
}

트랜잭션 중 에러가 발생하면 모든 변경사항이 자동으로 롤백됩니다.

주의사항과 팁

1. 연결 풀 관리

서버리스 환경(Lambda, Vercel)에서는 연결 풀이 금방 고갈될 수 있습니다. Prisma Data Proxy나 PgBouncer 같은 연결 풀러를 사용하세요.

2. 마이그레이션 전략

개발 환경에서는 prisma migrate dev를 사용하고, 프로덕션에서는 prisma migrate deploy를 사용하세요. 절대 프로덕션에서 migrate reset을 실행하면 안 됩니다.

3. 타입 생성 자동화

스키마를 변경한 후 반드시 npx prisma generate를 실행해 타입을 재생성해야 합니다. CI/CD 파이프라인에 이 단계를 포함하세요.

Prisma는 현대적이고 타입 안전한 ORM입니다. 직관적인 API와 강력한 타입 시스템으로 데이터베이스 작업을 즐겁게 만듭니다. N+1 문제 자동 해결, 트랜잭션 지원, Prisma Studio GUI 등 생산성을 높이는 기능이 풍부합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Prisma를 활용한 타입 안전 데이터베이스 접근 완벽 가이드. TypeORM/Sequelize 대비 장점, 설치부터 스키마 정의, 마이그레이션, Prisma Client, 관계 쿼리, 트랜잭션, 성능 최적화까지 실… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

Prisma, ORM, TypeScript, Database, PostgreSQL, MySQL, Node.js 등으로 검색하시면 이 글이 도움이 됩니다.