BE
[Node.js] Prisma DB 모델링 완벽 가이드: 필드 속성, 관계 설정, 실무 팁까지
시작하며
데이터베이스 설계는 프로젝트의 기초 체력을 결정하는 중요한 과정이다.
특히 Prisma를 ORM으로 사용하는 경우, 모델 선언 방식과 관계 설정 방법을 제대로 이해하면 유지보수성과 확장성을 모두 챙길 수 있다.
이번 글에서는 Prisma 모델링 기본 원칙부터 필드 속성, 다양한 관계 설계 패턴(1:1, 1:N, N:M), 그리고 실무에서 자주 쓰이는 팁까지 차근차근 정리해보자.
모델링 기본 원칙
업무 개체(명사) = 모델(model): User, Project, Order, Comment 등.
관계는 명확하게: 1:1, 1:N, N:M을 먼저 종이에 그려보고 필드로 옮기기.
변하지 않는 값 vs 변하는 값 구분: enum(상태, 역할)과 별도 테이블(사전/코드) 적절히 선택.
ID 전략: String @id @default(cuid())(문자열 ID) or Int @id @default(autoincrement())(숫자 ID) 중 하나로 통일.
시간 필드 기본 탑재: createdAt @default(now()), updatedAt @updatedAt.
소프트 삭제 고려: 운영에서 복구·추적 필요하면 deletedAt DateTime? 채택.
Prisma 필드/속성 핵심
1) 자주 쓰는 필드 속성 (field attributes)
@id : 기본키
@default() : 기본값 (예: now(), cuid(), uuid(), 숫자/문자 상수)
@updatedAt : 레코드 갱신 시 자동 시간 업데이트
@unique : 유니크 제약
@relation(fields: [...], references: [...], onDelete: ..., onUpdate: ...) : FK/관계
@map("col_name") : DB 실제 컬럼명 매핑
@db.VarChar(191) : 네이티브 타입 지정(프로바이더별 세부 타입 및 길이/정밀도)
2) 자주 쓰는 모델 속성 (block attributes)
@@id([a, b]) : 복합 기본키
@@unique([a, b]) : 복합 유니크
@@index([a, b]) : 인덱스
@@map("table_name") : 실제 테이블명 매핑
@@fulltext([title, content]) : (지원 DB에서) 전문 검색 인덱스
데이터 타입 빠른 표 (Prisma → DB/JS 매핑 감)
Prisma 타입 | 용도 | JS/TS 타입 | 비고(네이티브 예시) |
String | 문자열/ID | string | @db.VarChar(191), @db.Text 등 |
Int | 32비트 정수 | number | autoincrement 가능 |
BigInt | 큰 정수 | bigint | JS 연산 주의 |
Float | 부동소수 | number | 정확도 이슈 주의(금액엔 비추) |
Decimal | 고정소수 | Decimal(Prisma), 직렬화 필요 | 금액·정밀 수치에 권장. @db.Decimal(18,2) |
Boolean | 불리언 | boolean | |
DateTime | 날짜/시간 | Date | @default(now()) 가능 |
Bytes | 바이너리 | Buffer | 파일 해시 등 |
Json | 반정형 데이터 | Prisma.JsonValue | 필드 확장/메타 |
Unsupported | 지원 불가 타입 | - | 마이그레이션 호환용 |
enum | 상태/역할 등 제한값 | TS 유사 enum | DB엔 ENUM/체크 제약 |
1) Decimal vs Float
금액/정밀 계산 → Decimal + 네이티브 정밀도(@db.Decimal(18,2)).
Float는 오차 허용되는 측정값(예: 온도, 비율)에만.
2) String 길이
인덱싱/유니크 목적이면 MySQL에서 191 길이 관례(걸어둘 값만 짧게).
관계 설계 패턴
0) 기본적인 관계 선언 공식
관계명 관계테이블명 @relation(
fields: [내 FK 컬럼],
references: [상대 PK 또는 Unique 컬럼],
onDelete: Cascade | SetNull | Restrict
)1) 1:N (가장 흔함)
model User {
id String @id @default(cuid())
email String @unique @db.VarChar(320)
posts Post[] // 곤계 필드 (가상) : 역참조
}
model Post {
id String @id @default(cuid())
title String @db.VarChar(200)
authorId String
author User @relation(
fields: [authorId], // 내 FK
references: [id], // 부모 PK
onDelete: Cascade // 삭제 전파 전략
)
}2) 1:1
model User {
id String @id @default(cuid())
profile Profile?
}
model Profile {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}1:1이면 자주 조회되는 쪽에 FK를 두는 편이 실무적(조인 방향 고려).
3) N:M (암시적 관계; 중간 테이블 자동 생성)
: Prisma가 중간 테이블 자동 관리 -> fields, references, onDelete 없이 양쪽에 배열 형태로만 선언
model User {
id String @id @default(cuid())
projects Project[]
}
model Project {
id String @id @default(cuid())
members User[]
}아주 간단하지만 중간 테이블에 추가 필드(role, joinedAt 등)를 못 넣음.
4) N:M (명시적 관계; 중간 테이블 필드 포함형)
: 중간 테이블을 명시 모델로 둔다:
enum Role {
OWNER
MEMBER
}
model User {
id String @id @default(cuid())
memberships Membership[]
}
model Project {
id String @id @default(cuid())
name String @db.VarChar(100)
memberships Membership[]
}
model Membership {
// 복합 PK 또는 개별 PK + 복합 유니크 중 선택
userId String
projectId String
role Role @default(MEMBER)
joinedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@id([userId, projectId]) // 복합 기본키
@@index([projectId])
@@index([userId, role])
}(1) 1단계: 양쪽 엔티티(Model) 준비
model User {
id String @id @default(cuid())
memberships Membership[] // 중간 테이블과의 1:N 관계
}
model Project {
id String @id @default(cuid())
memberships Membership[]
}(2) 2단계: 중간 테이블(Model) 생성
model Membership {
userId String
projectId String
// FK 선언: fields → 내 FK, references → 상대 PK
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
// 복합 PK 또는 복합 Unique로 중복 방지
@@id([userId, projectId])
// 또는 @@unique([userId, projectId])
// 필요하면 인덱스 추가
@@index([projectId])
}(3) 3단계: 추가 필드(페이로드) 선언 (선택)
enum Role {
OWNER
MEMBER
}
model Membership {
userId String
projectId String
role Role @default(MEMBER) // 관계의 역할
joinedAt DateTime @default(now()) // 관계 생성 날짜
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@id([userId, projectId])
@@index([projectId, role])
}Payload는 원래 “전달되는 데이터 덩어리”라는 뜻인데, 여기서는 관계 자체가 전달하는 추가 정보라는 의미로 사용.
User ↔ Project라는 연결에 추가 데이터가 실려 있다는 의미.
인덱스/제약 설계 요령
조회 조건·정렬 컬럼에 인덱스
예) WHERE projectId = ? AND role = ? ORDER BY joinedAt DESC
@@index([projectId, role, joinedAt])유니크 키로 무결성 보장: 이메일, 외부 공급자 ID 등.
긴 문자열 인덱스 주의(MySQL): 필요시 길이 줄이거나 해시 컬럼 도입.
Fulltext(검색): MySQL 8/Innodb에서 가능한 경우만.
인덱스는 쓰기 비용(INSERT/UPDATE)과 스토리지를 늘림 → 최소 필요만.
실무 팁 (정책/도메인 설계)
상태 값은 enum으로 고정(프런트·백 동기화 쉬움). 장기적 확장 필요하면 별도 테이블(코드마스터) 고려.
금액/수량: Decimal(정밀도 지정) + 통화 코드 분리.
메타/옵션: 변동성 높은 스키마는 Json 필드로 완충(필수 질의는 컬럼으로).
감사 로그: 변경 이력 필요하면 Audit 테이블 별도 운영(트리거/앱레벨).
시간대: 서버/DB는 UTC, 프런트에서 KST 변환(일관성 중요).
마무리 하며
ORM의 장점은 코드로 데이터 구조와 관계를 명확하게 표현할 수 있다는 점이다.
하지만 그만큼 설계 초기 단계에서 올바른 모델 구조를 잡아야 이후 개발과 운영이 편해진다.
오늘 정리한 Prisma 모델링 규칙과 관계 설계 패턴을 익혀두면, 작은 프로젝트든 대규모 서비스든 안정적으로 데이터 구조를 관리할 수 있을 것이다.
앞으로 새로운 모델을 선언하거나 관계를 정의할 때, 이 가이드를 떠올려 보세요.