4.Node.js/Prisma(ORM)

[Prisma7] 2편. schema.prisma 설계 규칙: 필드,제약,인덱스,Relation

쿼드큐브 2026. 1. 13. 08:55
반응형
반응형

 

2편. schema.prisma 설계 규칙: 필드,제약,인덱스,Relation

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

 

Prisma 모델링 삽화 이미지
Prisma 모델링 삽화 이미지

 

📂 [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 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형