2편. schema.prisma 설계 규칙: 필드,제약,인덱스,Relation
📚 목차
1. schema.prisma 구성과 모델 선언 규칙
2. 필드 속성(Attribute)과 제약 설계
3. Prisma 스칼라 타입과 PostgreSQL 매핑
4. 관계(Relation) 모델링 패턴: 1:1, 1:N, N:M

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /Prisma
1. schema.prisma 구성과 모델 선언 규칙
🔷 schema.prisma의 역할과 구성 요소
| 구성요소 | 역할 | 주요 항목 |
| Generator | Prisma Client 등 코드 생성 도구 설정 | provider, output |
| Datasource | 데이터베이스 연결 정보 설정 | provider(postgresql, mysql 등), url |
| Model | 실제 DB 테이블과 매핑될 비즈니스 엔티티 정의 | 필드 타입, 제약조건, 관계(1:1/1:N/N:M), @id/@default/@unique/@updatedAt, @@index 등 |
// 1. Generator: Prisma Client 생성 설정
generator client {
provider = "prisma-client" // Prisma 7부터는 -js를 뺍니다.
output = "../src/generated" // output 설정이 필수가 되었습니다.
}
// 2. Datasource: 데이터베이스 연결 정보
datasource db {
provider = "postgresql"
}
// 3. Model: 실제 데이터 구조 정의
model User { // PascalCase (단수형 권장)
id Int @id
displayName String? @map("display_name") // camelCase
@@map("users") // DB 테이블명: snake_case 복수형
}
Prisma 7 Client 구조 변화 비교: prisma-client-js와 prisma-client
| 구분 | 기존 (prisma-client-js) | 신규 (prisma-client) |
| 패키지 명 | @prisma/client (legacy 생성 방식) | prisma-client (신규 Client 패키지) |
| Client 생성 위치 | node_modules/.prisma/client 내부에 자동 생성 | 사용자 지정 경로 필수 (generator output) |
| Runtime 엔진 | Rust 기반 Query Engine (Binary) | Wasm 기반 엔진 / Driver Adapter 중심 |
| Driver Adapter | 제한적 / 실험적 | 공식 1st-class 지원 (pg, mysql 등) |
| Node.js 호환성 | Node 16+ (ESM 시 제약 존재) | Node 18+ / 20+ 최적화 |
| 환경 변수 로딩 | .env 자동 로드 | .env 자동 로드 없음 |
🔷 모델 선언 문법과 네이밍 컨벤션
model User { // ✅ PascalCase (단수형 권장)
id Int @id
displayName String? @map("display_name") // ✅ camelCase
@@map("users") // ✅ DB 테이블명: snake_case 복수형
}
▸ Model 이름: PascalCase 단수형 권장 (예: User, UserOrder)
▸ 필드(Column) 이름: camelCase 권장 (예: createdAt, firstName)
▸ DB 매핑: 실제 DB 테이블이나 컬럼명이 프로젝트 컨벤션(주로 snake_case)과 다를 경우 @map, @@map을 사용합니다.
🔷 필드 정의 및 데이터 타입
model User {
// 스칼라 필드 (실제 DB 컬럼)
id Int @id @default(autoincrement())
email String @unique // Required
displayName String? @map("display_name") // Optional (?)
createdAt DateTime @default(now())
deletedAt DateTime? // Nullable
// 관계 필드 (DB 컬럼 아님, Prisma만 사용)
profile Profile? // 1:1 관계
posts Post[] // 1:N 관계
}
✔️ 스칼라 vs 관계 필드
| 구분 | 스칼라 필드 | 관계 필드 |
| DB 컬럼 | O | X (가상 필드) |
| 타입 | String, Int, Boolean, DateTime, Json, Decimal 등 | 다른 model 타입 (User, Post 등) |
| 예시 | email, createdAt, published | posts, profile, author |
✔️ Nullable(?)의 의미
▸ ?가 붙으면 NULL 허용
▸ Prisma에서 Optional = DB Nullable
▸ String ≠ String? (명확히 구분)
displayName String? // 1. Optional: 생성 시 생략 가능
deletedAt DateTime? // 2. Nullable: DB에 NULL 허용
2. 필드 속성(Attribute)과 제약 설계
🔷 기본 속성: @id, @default, @updatedAt
model User {
// @id: 기본 키 지정
id Int @id @default(autoincrement())
// @default: 기본값 설정
createdAt DateTime @default(now()) @map("created_at")
// @updatedAt: 자동 업데이트 타임스탬프
updatedAt DateTime @updatedAt @map("updated_at")
// @unique: 단일 컬럼 유니크 제약
email String @unique
// @@unique: 복합 유니크 제약 (아래 예시)
@@map("users")
}
@id : Primary Key 선언
▸ @id는 해당 필드를 모델의 기본 키(Primary Key)로 지정합니다.
▸ Prisma 모델은 반드시 하나의 @id 필드를 가져야 하며, 데이터베이스에서는 PRIMARY KEY 제약 조건으로 생성됩니다.
▸ Primary Key는 각 레코드를 고유하게 식별하는 역할을 하며, 인덱스가 자동으로 생성됩니다.
@default() : 기본값 설정
▸ @default()는 레코드가 생성될 때 자동으로 채워질 기본값을 정의합니다.
▸ 또한 now()를 사용하면 레코드 INSERT 시점의 현재 시간이 저장되며, 이는 Prisma Client가 아닌 데이터베이스 함수 기준으로 동작합니다.
@updatedAt : 수정 시 자동 갱신
▸ @updatedAt는 Prisma Client를 통해 레코드가 UPDATE될 때마다 해당 필드 값을 현재 시각으로 자동 갱신하도록 지정하는 속성입니다.
▸ Prisma ORM 레벨의 기능이므로, Prisma를 통하지 않고 직접 SQL로 UPDATE를 실행할 경우에는 자동 갱신되지 않습니다.
🔷 Unique 제약 : @unique vs @@unique
@unique : 단일 컬럼 제약
▸ email 값 중복 불가
▸ DB에서는 UNIQUE INDEX 생성
model User {
id Int @id @default(autoincrement())
email String @unique
}
// CREATE UNIQUE INDEX users_email_key ON users(email);
@@unique : 복합 컬럼
▸ 한 사용자는 하나의 게시글에 한 번만 좋아요 가능
▸ 단일 필드가 아닌 조합의 유일성 보장
▸ 복합 Unique는 실무에서 “중복 방지 로직”의 핵심입니다.
model PostLike {
userId Int
postId Int
@@unique([userId, postId])
}
🔷 컬럼 / 테이블 매핑: @map, @@map
@map과 @@map을 사용하는 이유는 "코딩 스타일(TypeScript)과 데이터베이스 관례(SQL) 사이의 간극을 메우기 위해서" 입니다.
▸ TypeScript/Prisma: PascalCase(모델명)나 camelCase(필드명)를 주로 사용합니다.
▸ SQL/Database: snake_case를 표준으로 사용하는 경우가 많습니다.
▸ 결과: 코드에서는 user.displayName으로 접근하고, DB 저장 시에는 display_name으로 저장
@map : 필드 매핑
▸ Prisma 필드명: displayName
▸ 실제 DB 컬럼명: display_name
model User {
id Int @id @default(autoincrement())
displayName String? @map("display_name")
}
@@map : 테이블 매핑
▸ Prisma 모델명: User
▸ 실제 테이블명: users
model User {
id Int @id @default(autoincrement())
@@map("users")
}
🔷 인덱스 선언: @@index
model Post {
id Int @id @default(autoincrement())
authorId Int
createdAt DateTime @default(now())
// 복합 인덱스 (조회 쿼리 패턴에 최적화)
@@index([authorId, createdAt])
}
▸ 특정 유저의 최신 게시글 조회에 최적화
▸ WHERE authorId = ? ORDER BY createdAt DESC 패턴 대응
단일 칼럼 인덱스: Soft Delete 최적화 인덱스
model Post {
deletedAt DateTime?
// 단일 컬럼 인덱스 (소프트 삭제 조회 최적화)
@@index([deletedAt])
}
// 사용예시
prisma.post.findMany({
where: { deletedAt: null }
})
인덱스(Index) 설계 전략
▸ 필터링 조건: WHERE 절에 자주 쓰이는 deletedAt, published 등
▸ 외래 키 (FK): JOIN 성능을 위한 authorId, postId 등
▸ 정렬 기준: ORDER BY에 사용되는 createdAt 등
복합 인덱스 작성 시, 데이터의 중복도가 낮은(선택도가 높은) 컬럼을 앞에 배치하는 것이 성능상 유리합니다.
예: @@index([authorId, createdAt]) (작성자별 정렬 조회 최적화)
🔷 참조 무결성(Referential Action) : OnDelete, onUpdate
Prisma에서 @relation(...)을 선언할 때 onDelete, onUpdate 옵션을 함께 지정할 수 있습니다.
이 옵션들은 부모 테이블의 데이터가 삭제되거나(DELETE), 기본키가 변경될 때(UPDATE) 자식 테이블의 참조 데이터(외래키)가 어떤 방식으로 처리되어야 하는지를 정의합니다.
| 옵션 | 의미 |
| Cascade | 부모가 삭제되면, 해당 부모를 참조하는 자식도 함께 삭제되도록 처리합니다. |
| Restrict | 자식이 부모를 참조하고 있는 동안에는, 부모를 삭제할 수 없도록 제한합니다. |
| SetNull | 부모가 삭제되면, 자식의 외래키를 NULL로 변경하여 참조를 끊습니다. (단, FK가 Nullable이어야 합니다.) |
| SetDefault | FK를 default 값으로 변경합니다. 거의 안씀 |
예제 : Post – User 관계에서 onDelete: Restrict
어떤 사용자가 작성한 게시글(Post)이 하나라도 존재하는 상태라면 그 사용자(User) 레코드는 삭제할 수 없습니다
삭제를 시도하면 DB가 “참조 중인 데이터가 있으니 삭제 불가”로 막습니다
model Post {
// 게시글의 고유 식별자 (Primary Key)
id Int
// User 모델의 id 컬럼을 참조하는 외래키(Foreign Key)
authorId Int
// Post ↔ User 관계를 정의하는 관계 필드
author User @relation(
// 이 모델(Post)에서 외래키로 사용할 필드
fields: [authorId],
// 참조 대상이 되는 User 모델의 기본키
references: [id],
// 게시글이 존재하는 사용자는 삭제할 수 없다
onDelete: Restrict
)
}
예제 : Comment – Post 관계에서 onDelete: Cascade
게시글(Post)을 삭제하면, 그 게시글을 참조하는 댓글(Comment)들도 DB가 자동으로 함께 삭제합니다.
model Comment {
postId Int
// Post 모델과의 관계(Relation) 필드
post Post @relation(
//모델에서 외래키로 사용할 필드
fields: [postId],
//참조 대상이 되는 Post 모델의 기본키(PK) 필드
references: [id],
// 연결된 Post가 삭제되면, 이 Post를 참조하는 Comment 레코드도 삭제
onDelete: Cascade
)
}
예제: onUpdate
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
// FK: User.email을 참조 (email은 @unique이므로 references로 사용 가능)
authorEmail String @map("author_email")
author User @relation(
fields: [authorEmail],
references: [email],
onUpdate: Cascade, // 부모(User.email) 변경 시 자식(Post.authorEmail)도 자동 변경
onDelete: Restrict
)
@@index([authorEmail])
@@map("posts")
}
▸ onUpdate: Cascade
- 부모의 참조 값이 바뀌면, 자식 FK도 같이 바뀜 (관계 유지)
▸ onUpdate: Restrict
- 참조 중이면 부모 값 변경 자체를 막음 (안전하게 차단)
▸ onUpdate: SetNull
- 부모 값이 바뀌면 자식 FK를 NULL로 만들어 관계를 끊음 (FK가 Nullable이어야 함)
3. Prisma 스칼라 타입과 PostgreSQL 매핑
Prisma는 기본적으로 데이터베이스 추상화 계층을 제공하지만, @db 속성을 사용하면 특정 DB에 최적화된 타입을 직접 지정할 수 있습니다.
🔷 기본 타입 매핑 요약
| Prisma 타입 | PostgreSQL 타입 | 설명 | 예시 |
| String | text | 가변 길이 문자열 (길이 제한 없음) | 이메일, 본문, 이름 |
| Int | integer | 32비트 정수 | 일반적인 ID, 카운트 |
| BigInt | bigint | 64비트 정수 | 대규모 숫자 데이터, 로그 ID |
| Boolean | boolean | 참 / 거짓 값 | 활성화 여부, 삭제 여부 |
| DateTime | timestamp(3) | 날짜 및 시간 (밀리초 정밀도) | 생성일, 수정일 |
| Decimal | numeric | 고정 소수점 실수 (정밀도 유지) | 금액, 이자율 |
| Json | jsonb | 바이너리 JSON (인덱싱·성능 최적화) | 설정값, 메타데이터 |
| Bytes | bytea | 이진 데이터 | 이미지, 파일 데이터 |
▸ Float 사용: 금액 계산 시 Float를 쓰면 $0.1 + 0.2 = 0.30000000000000004$ 같은 오차가 발생합니다.
▸ 남용되는 String: 태그 목록이나 상태 값들을 단순히 String 하나에 콤마로 구분해 넣지 마세요.
(배열 타입이나 Enum, 별도 테이블 권장)
🔷 세밀한 타입 제어 (@db 속성)
기본 매핑만으로 부족할 때, @db 어노테이션을 사용하여 DB의 특정 기능을 활용합니다.
model Product {
id Int @id @default(autoincrement())
// 가변 길이 제한: text 대신 varchar(255) 사용
name String @db.VarChar(255)
// 명시적 텍스트: 긴 설명글
description String @db.Text
// 정밀도 지정: 총 10자리, 소수점 아래 2자리 (금액에 필수)
price Decimal @db.Decimal(10, 2)
// 성능 최적화: 검색과 인덱싱에 유리한 JsonB
metadata Json @db.JsonB
// 정밀한 시간: 밀리초(3)를 넘어 마이크로초(6) 단위 저장
createdAt DateTime @default(now()) @db.Timestamp(6)
@@map("products") // DB 테이블명은 복수형 권장
}
4. 관계(Relation) 모델링 패턴: 1:1, 1:N, N:M
Prisma에서 관계 모델링의 핵심은 "데이터베이스의 물리적 컬럼(FK)"과 "애플리케이션의 논리적 연결(Relation Field)"을 구분하는 것입니다.
🔸 관계 필드 (Relation Field):
- Prisma 스키마에만 존재하며, 실제 DB 컬럼이 아닙니다.
- 객체 간의 연결을 정의합니다. (예: user User)
🔸 외래 키 필드 (Foreign Key Field):
- 실제 DB 테이블의 컬럼입니다.
- 관계의 '주인'이 누구인지 결정합니다. (예: userId Int)
🔸 참조 (References):
- 연결할 대상의 기준점(보통 PK인 id)을 지정합니다.
🔷 1:1 관계 (One-to-One)
사용자(User)와 상세 프로필(Profile). 사용자 한 명은 하나의 프로필만 가집니다.
▸ Unique 제약: 외래 키(userId)에 @unique를 붙여야만 1:1 관계가 유지됩니다.
▸ Optional 설정: Profile?처럼 물음표를 붙여 프로필이 없는 사용자도 생성 가능하게 합니다.
▸ 삭제 전략: 사용자가 삭제되면 프로필도 필요 없으므로 onDelete: Cascade를 주로 사용합니다.
model User {
id Int @id @default(autoincrement())
profile Profile? // 1:1 관계 (선택 사항)
}
model Profile {
id Int @id @default(autoincrement())
userId Int @unique @map("user_id") // 1:1 보장을 위한 유니크 설정
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
🔷 1:N 관계 (One-to-Many)
사용자(User)와 게시글(Post). 한 명이 여러 글을 쓸 수 있습니다.
▸ 배열 타입: '1' 쪽 모델(User)에 posts Post[]와 같이 배열로 정의합니다.
▸ 데이터 보호: 사용자를 지웠을 때 글이 모두 사라지면 위험하므로 onDelete: Restrict를 사용해 게시글이 있는 사용자는 삭제하지 못하게 방어하는 경우가 많습니다.
▸ 인덱스 최적화: @@index([authorId])를 설정하여 특정 사용자의 글을 빠르게 조회합니다.
model User {
id Int @id @default(autoincrement())
posts Post[] // 1:N (여러 개의 포스트)
}
model Post {
id Int @id @default(autoincrement())
authorId Int @map("author_id")
author User @relation(fields: [authorId], references: [id], onDelete: Restrict)
@@index([authorId]) // 조회 최적화
}
🔷 N:M 관계 (Many-to-Many)
게시글(Post)과 좋아요(User). 여러 명이 여러 글에 좋아요를 누를 수 있습니다.
✔️ 암시적(Implicit) 관계
추가 필드(좋아요 누른 시간 등)가 필요 없을 때 사용합니다. Prisma가 중간 테이블을 자동으로 관리합니다.
model Post {
id Int @id
tags Tag[] // 간단한 태그 기능에 적합
}
model Tag {
id Int @id
posts Post[]
}
✔️ 명시적(Explicit) 관계 : 권장
중간 테이블을 직접 정의합니다.
"언제 좋아요를 눌렀는지(createdAt)" 같은 추가 정보를 담을 수 있어 실무에서 대부분 이 방식을 사용합니다.
model PostLike {
userId Int @map("user_id")
postId Int @map("post_id")
createdAt DateTime @default(now()) // 추가 데이터 저장 가능
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@id([userId, postId]) // 복합 기본키: 한 사람이 한 글에 좋아요 한 번만 가능
}
🔷 관계 요약 및 삭제 전략(onDelete)
| 유형 | 예시 | onDelete 전략 | 설명 |
| 1:1 | User - Profile | Cascade (부모 삭제 시 자식도 삭제) | FK에 @unique 필수 |
| 1:N | User - Post | Restrict (글이 있으면 사용자 삭제 불가) | 자식 모델(Post)에 FK 필드 배치 (authorId) |
| 1:N | Post - Comment | Cascade (글 삭제 시 댓글도 모두 삭제) | 부모에 comments Comment[], 자식(Comment)에 FK 배치 (postId) |
| N:M | User - Post (Like) | Cascade (유저/글 삭제 시 연결 데이터 삭제) | 중간 테이블 모델 + 복합키 @@id([userId, postId]) 사용 |
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Prisma(ORM)' 카테고리의 다른 글
| [Prisma7] 6편. Prisma로 해결되지 않는 쿼리 다루기: Raw SQL 실전 활용 (0) | 2026.01.20 |
|---|---|
| [Prisma7] 5편. Prisma 관계 조회 심화: include / select와 중첩 관계 탐색 (0) | 2026.01.19 |
| [Prisma7] 4편. 조회 쿼리 고급 옵션(where)과 관계 데이터 생성(create) 패턴 이해 (0) | 2026.01.16 |
| [Prisma7] 3편. Prisma Client Query 구조 와 CRUD API 이해하기 (0) | 2026.01.14 |
| [Prisma7] 1편. 개발환경 구축과 프로젝트 초기화 (Node.js + Prisma 7) (0) | 2026.01.12 |