SQL을 직접 작성할까, ORM을 쓸까 — Node.js 생태계에서 Prisma와 Sequelize는 어떻게 다를까요?

Express 프로젝트를 시작할 때 데이터베이스 접근 방식을 결정해야 합니다. Node.js 생태계의 두 대표 ORM인 Prisma와 Sequelize는 설계 철학부터 다릅니다. 잘못 선택하면 프로젝트 중반에 교체 비용이 큽니다.

개념 정의

ORM(Object-Relational Mapping) 은 데이터베이스 테이블을 프로그래밍 언어의 객체에 매핑하는 기술입니다. SQL을 직접 작성하는 대신 메서드 호출로 데이터를 다루며, 스키마 정의 → 마이그레이션 → CRUD 의 사이클로 동작합니다.

Prisma vs Sequelize 비교

기준PrismaSequelize
스키마 정의.prisma 파일 (DSL)JavaScript/TypeScript 코드
타입 안전성자동 생성 타입 (TypeScript 우선)수동 타입 정의 필요
쿼리 빌더자체 클라이언트 (Prisma Client)메서드 체이닝
마이그레이션prisma migrate (SQL 자동 생성)sequelize-cli
러닝 커브새 DSL 학습 필요JavaScript 패턴과 유사
로우 쿼리$queryRawsequelize.query()
관계 정의스키마 파일에 선언적associate() 메서드로 정의
커뮤니티급성장 중 (2019~)오래된 안정적 생태계 (2011~)

Prisma — 스키마 우선 설계

Prisma는 .prisma 파일에 스키마를 선언하고, 여기서 모든 것을 생성합니다.

스키마 정의

PRISMA
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String
  posts Post[]  // ← 관계 정의
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int
}

이 파일 하나에 데이터베이스 연결, 모델, 관계가 모두 선언되어 있습니다. npx prisma generate를 실행하면 이 스키마에 맞는 TypeScript 타입이 자동 생성됩니다.

마이그레이션과 CRUD

마이그레이션은 스키마 변경 사항을 SQL로 변환합니다.

BASH
npx prisma migrate dev --name add-user-model

이 명령이 실행되면 prisma/migrations/ 디렉토리에 SQL 파일이 생성되고, 데이터베이스에 적용됩니다.

Express에서 사용하는 CRUD 코드입니다.

JS
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

// 생성
app.post('/users', async (req, res) => {
  const user = await prisma.user.create({
    data: { email: req.body.email, name: req.body.name },
  });
  res.status(201).json(user);
});

// 조회 (관계 포함)
app.get('/users/:id', async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: Number(req.params.id) },
    include: { posts: true }, // ← 관계 데이터 함께 조회
  });
  res.json(user);
});

include: { posts: true }를 지정하면 JOIN 쿼리가 생성되어 연관된 Post 데이터가 함께 조회됩니다.

Sequelize — 코드 우선 설계

Sequelize는 JavaScript 코드로 모델을 정의합니다.

JS
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize(process.env.DATABASE_URL);

const User = sequelize.define('User', {
  email: { type: DataTypes.STRING, unique: true, allowNull: false },
  name: { type: DataTypes.STRING, allowNull: false },
});

const Post = sequelize.define('Post', {
  title: { type: DataTypes.STRING, allowNull: false },
});

User.hasMany(Post);        // 1:N 관계
Post.belongsTo(User);
JS
// Express에서 사용
app.post('/users', async (req, res) => {
  const user = await User.create({
    email: req.body.email,
    name: req.body.name,
  });
  res.status(201).json(user);
});

app.get('/users/:id', async (req, res) => {
  const user = await User.findByPk(req.params.id, {
    include: Post,  // ← Eager loading
  });
  res.json(user);
});

어떤 걸 선택할까

PLAINTEXT
TypeScript 프로젝트 + 타입 안전성 중시   →  Prisma
JavaScript 프로젝트 + 빠른 프로토타이핑  →  Sequelize
복잡한 로우 쿼리가 많은 경우              →  둘 다 한계, knex 또는 직접 SQL 고려
기존 Sequelize 프로젝트                  →  무리하게 마이그레이션하지 않기

주의할 점

N+1 문제는 ORM에서 항상 따라온다

관계 데이터를 조회할 때 각 레코드마다 추가 쿼리가 발생하는 N+1 문제는 ORM의 고질적 함정입니다.

JS
// N+1 발생 — 유저마다 별도 쿼리
const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

// 해결 — include로 한 번에 조회
const users = await prisma.user.findMany({
  include: { posts: true },
});

Prisma는 include를, Sequelize는 include 옵션이나 eager loading을 사용해야 합니다.

Prisma Client를 매번 생성하지 않기

JS
// 잘못된 방식 — 요청마다 새 인스턴스
app.get('/users', async (req, res) => {
  const prisma = new PrismaClient(); // ← 커넥션 풀 낭비
  const users = await prisma.user.findMany();
  res.json(users);
});

// 올바른 방식 — 싱글턴
const prisma = new PrismaClient();
app.get('/users', async (req, res) => {
  const users = await prisma.user.findMany();
  res.json(users);
});

PrismaClient는 내부에 커넥션 풀을 관리합니다. 매 요청마다 새로 생성하면 커넥션이 고갈됩니다.

마이그레이션을 건너뛰고 DB를 직접 수정하지 않기

프로덕션 DB를 직접 ALTER TABLE로 수정하면, 마이그레이션 히스토리와 실제 스키마가 어긋납니다. 이후 prisma migrate deploy가 실패하거나 예상과 다른 마이그레이션이 생성됩니다.

스키마 변경은 반드시 마이그레이션을 통해서만 진행합니다. 마이그레이션 파일은 "데이터베이스의 git log"와 같습니다.

정리

항목설명
Prisma스키마 DSL 기반, 자동 타입 생성, TypeScript 최적화
SequelizeJavaScript 코드 기반, 오래된 생태계, 빠른 시작
N+1 해결include 옵션으로 Eager Loading
커넥션 관리Client를 싱글턴으로 생성
마이그레이션스키마 변경의 유일한 경로, 직접 DB 수정 금지
선택 기준TypeScript → Prisma, 빠른 프로토타입 → Sequelize
댓글 로딩 중...