이미지 로딩 중...
AI Generated
2025. 11. 16. · 6 Views
PostgreSQL과 Prisma로 배우는 데이터베이스 설계 완벽 가이드
실시간 채팅 애플리케이션을 예제로 PostgreSQL과 Prisma를 활용한 데이터베이스 설계를 단계별로 배웁니다. ERD 설계부터 테이블 생성, 관계 설정, 마이그레이션까지 실무에 필요한 모든 것을 다룹니다.
목차
1. 데이터베이스 ERD 설계
시작하며
여러분이 실시간 채팅 앱을 만들려고 할 때 이런 생각을 해본 적 있나요? "사용자 정보는 어디에 저장하지?
채팅방과 메시지는 어떻게 연결하지?" 막상 코드를 작성하려고 하면 어디서부터 시작해야 할지 막막하죠. 이런 혼란은 데이터베이스 설계 없이 바로 코드를 작성하려고 할 때 자주 발생합니다.
테이블을 먼저 만들고 나중에 관계를 추가하다 보면 데이터가 중복되거나 불필요한 컬럼이 생기고, 나중에는 전체를 다시 만들어야 하는 상황까지 생깁니다. 바로 이럴 때 필요한 것이 ERD(Entity Relationship Diagram) 설계입니다.
ERD는 마치 집을 짓기 전에 설계도를 그리는 것처럼, 데이터베이스를 만들기 전에 전체 구조를 한눈에 볼 수 있게 해줍니다.
개요
간단히 말해서, ERD는 데이터베이스의 설계도입니다. 어떤 테이블이 필요하고, 각 테이블에 어떤 정보가 들어가며, 테이블끼리 어떻게 연결되는지를 그림으로 표현한 것이죠.
왜 ERD가 필요할까요? 실무에서는 여러 개발자가 함께 작업하는데, ERD가 있으면 모두가 같은 구조를 이해하고 작업할 수 있습니다.
예를 들어, 채팅 앱을 만든다면 "한 명의 사용자가 여러 채팅방에 참여할 수 있고, 하나의 채팅방에는 여러 메시지가 있다"는 관계를 ERD로 명확하게 표현할 수 있습니다. 기존에는 엑셀로 테이블 구조를 정리하거나 말로만 설명했다면, ERD를 사용하면 시각적으로 한눈에 전체 구조를 파악할 수 있습니다.
ERD의 핵심 특징은 세 가지입니다: 엔티티(테이블)를 사각형으로 표현하고, 속성(컬럼)을 나열하며, 관계를 선으로 연결합니다. 이러한 표준화된 표현 방식 덕분에 누구나 쉽게 이해할 수 있고, 나중에 데이터베이스를 수정할 때도 어디를 고쳐야 할지 명확하게 알 수 있습니다.
코드 예제
// 채팅 앱 ERD 구조 (텍스트 표현)
//
// [User] 1 ----< N [Message]
// | |
// | |
// 1 N
// | |
// N >----< N [ChatRoom]
//
// User (사용자)
// - id: 고유 식별자
// - email: 이메일
// - name: 이름
// - createdAt: 가입일
//
// ChatRoom (채팅방)
// - id: 고유 식별자
// - name: 채팅방 이름
// - createdAt: 생성일
//
// Message (메시지)
// - id: 고유 식별자
// - content: 메시지 내용
// - userId: 작성자 ID (외래키)
// - chatRoomId: 채팅방 ID (외래키)
// - createdAt: 작성일
설명
이것이 하는 일: ERD는 우리가 만들 데이터베이스의 전체 구조를 시각적으로 보여줍니다. 마치 지도를 보면서 목적지까지의 경로를 파악하는 것처럼, ERD를 보면 데이터가 어떻게 저장되고 연결되는지 한눈에 알 수 있습니다.
첫 번째로, 엔티티(Entity)를 정의합니다. 위 예제에서 User, ChatRoom, Message가 바로 엔티티인데, 이것들은 실제 데이터베이스에서 테이블이 됩니다.
각 엔티티는 사각형으로 표현하며, 그 안에 저장할 정보(속성)를 나열합니다. 이렇게 하는 이유는 어떤 정보를 저장해야 하는지 미리 계획하기 위해서입니다.
그 다음으로, 관계(Relationship)를 정의합니다. User와 Message 사이에 "1:N" 관계가 있다는 것은 한 명의 사용자가 여러 개의 메시지를 작성할 수 있다는 뜻입니다.
User와 ChatRoom 사이에 "N:M" 관계가 있다는 것은 한 명의 사용자가 여러 채팅방에 참여할 수 있고, 하나의 채팅방에도 여러 사용자가 있을 수 있다는 의미죠. 마지막으로, 외래키(Foreign Key)를 통해 테이블을 연결합니다.
Message 테이블의 userId와 chatRoomId가 바로 외래키인데, 이것들이 각각 User와 ChatRoom 테이블을 참조합니다. 이렇게 하면 "이 메시지는 누가 어느 채팅방에 보낸 것인가"를 명확하게 알 수 있습니다.
여러분이 ERD를 작성하면 코딩을 시작하기 전에 데이터 구조의 문제점을 발견하고 수정할 수 있습니다. 실제로 테이블을 만든 후에 구조를 변경하는 것보다 ERD 단계에서 수정하는 것이 훨씬 빠르고 안전합니다.
또한 팀원들과 소통할 때 말로 설명하는 것보다 ERD 한 장이면 모든 것을 명확하게 전달할 수 있습니다.
실전 팁
💡 ERD를 그릴 때는 draw.io, dbdiagram.io, ERDCloud 같은 무료 도구를 활용하세요. 손으로 그리는 것보다 훨씬 깔끔하고 수정도 쉽습니다.
💡 테이블 이름은 단수형으로 작성하는 것이 관례입니다. Users가 아니라 User, Messages가 아니라 Message로 표현하세요. Prisma에서도 이런 규칙을 따릅니다.
💡 모든 테이블에 id, createdAt, updatedAt은 기본으로 포함하세요. 나중에 데이터 추적과 디버깅에 필수적입니다.
💡 N:M 관계는 중간 테이블(Junction Table)이 필요합니다. User와 ChatRoom의 N:M 관계는 나중에 UserChatRoom 같은 중간 테이블로 구현됩니다.
💡 ERD를 작성할 때는 비즈니스 로직부터 생각하세요. "사용자는 무엇을 할 수 있나?", "데이터는 어떻게 흘러가나?"를 먼저 정리하면 자연스럽게 필요한 테이블과 관계가 나옵니다.
2. 사용자 테이블 스키마
시작하며
여러분이 회원가입 기능을 만들 때 이런 고민을 해본 적 있나요? "사용자 정보를 어떻게 저장하지?
비밀번호는 어떻게 관리하지? 이메일 중복은 어떻게 막지?" 단순해 보이지만 실제로는 보안과 성능을 모두 고려해야 하는 복잡한 작업입니다.
이런 문제는 사용자 테이블 설계를 제대로 하지 않으면 나중에 큰 문제가 됩니다. 예를 들어, 중복 이메일을 막지 않으면 같은 계정이 여러 개 생기고, 비밀번호를 평문으로 저장하면 보안 사고가 발생하며, 인덱스를 설정하지 않으면 사용자가 많아질수록 로그인이 느려집니다.
바로 이럴 때 필요한 것이 제대로 된 사용자 테이블 스키마입니다. 보안, 성능, 확장성을 모두 고려한 설계로 안정적인 서비스의 기반을 만들 수 있습니다.
개요
간단히 말해서, 사용자 테이블은 서비스를 이용하는 모든 사람의 정보를 저장하는 곳입니다. 이름, 이메일, 비밀번호 같은 기본 정보부터 프로필 이미지, 가입일 같은 부가 정보까지 포함됩니다.
왜 사용자 테이블이 중요할까요? 거의 모든 기능이 "누가 이 작업을 했는가"를 추적해야 하기 때문입니다.
채팅 메시지를 보낸 사람, 게시글을 작성한 사람, 상품을 주문한 사람 등 모든 데이터가 사용자와 연결됩니다. 예를 들어, 채팅 앱에서 메시지 테이블은 userId를 통해 "이 메시지를 누가 보냈는지"를 알아야 합니다.
기존에는 사용자 정보를 파일이나 간단한 배열에 저장했다면, 이제는 PostgreSQL 같은 관계형 데이터베이스를 사용하여 안전하고 효율적으로 관리할 수 있습니다. 사용자 테이블의 핵심 특징은 세 가지입니다: 고유 식별자(id)로 각 사용자를 구분하고, 유니크 제약조건으로 이메일 중복을 방지하며, 타임스탬프로 가입일과 수정일을 자동으로 기록합니다.
이러한 특징들이 데이터 무결성을 보장하고 사용자 추적을 가능하게 만듭니다.
코드 예제
-- PostgreSQL 사용자 테이블 생성 SQL
CREATE TABLE "User" (
-- 고유 식별자 (자동 증가)
id SERIAL PRIMARY KEY,
-- 이메일 (필수, 중복 불가, 인덱스)
email VARCHAR(255) NOT NULL UNIQUE,
-- 이름 (필수)
name VARCHAR(100) NOT NULL,
-- 해시된 비밀번호 (필수)
password VARCHAR(255) NOT NULL,
-- 프로필 이미지 URL (선택)
profileImage VARCHAR(500),
-- 가입일 (자동 생성)
"createdAt" TIMESTAMP NOT NULL DEFAULT NOW(),
-- 수정일 (자동 업데이트)
"updatedAt" TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 이메일 검색 성능 향상을 위한 인덱스
CREATE INDEX idx_user_email ON "User"(email);
설명
이것이 하는 일: 사용자 테이블은 애플리케이션의 모든 사용자 정보를 체계적으로 저장하고 관리합니다. 마치 도서관의 회원 명부처럼, 누가 서비스를 이용하는지 정확하게 기록하고 필요할 때 빠르게 찾을 수 있게 해줍니다.
첫 번째로, id 컬럼이 SERIAL PRIMARY KEY로 정의되어 있습니다. SERIAL은 PostgreSQL에서 자동으로 숫자를 1씩 증가시켜주는 타입이고, PRIMARY KEY는 이 컬럼이 테이블의 주요 식별자라는 뜻입니다.
새로운 사용자가 가입하면 자동으로 1, 2, 3... 순서대로 id가 부여됩니다.
이렇게 하는 이유는 각 사용자를 절대적으로 구분하기 위해서입니다. 그 다음으로, email 컬럼에 UNIQUE 제약조건이 걸려 있습니다.
이것은 데이터베이스 레벨에서 같은 이메일로 두 번 가입하는 것을 막아줍니다. 애플리케이션 코드에서 중복을 체크할 수도 있지만, 데이터베이스 자체에서 막으면 훨씬 더 안전합니다.
동시에 여러 요청이 와도 데이터베이스가 중복을 확실하게 차단해줍니다. 세 번째로, createdAt과 updatedAt 컬럼이 DEFAULT NOW()로 설정되어 있습니다.
이것은 새로운 행(row)이 생성될 때 자동으로 현재 시각을 저장해준다는 뜻입니다. 개발자가 일일이 날짜를 입력할 필요가 없고, 실수로 빠뜨릴 일도 없습니다.
나중에 "이 사용자는 언제 가입했지?"를 확인할 때 매우 유용합니다. 마지막으로, email 컬럼에 인덱스를 추가했습니다.
인덱스는 마치 책의 색인처럼 특정 데이터를 빠르게 찾을 수 있게 해줍니다. 로그인할 때 이메일로 사용자를 찾는데, 사용자가 100만 명이라면 인덱스 없이는 모든 행을 다 확인해야 합니다.
인덱스가 있으면 거의 즉시 찾을 수 있습니다. 여러분이 이 스키마를 사용하면 안전하고 빠른 사용자 관리 시스템을 구축할 수 있습니다.
데이터베이스 레벨의 제약조건으로 데이터 무결성이 보장되고, 인덱스로 빠른 검색이 가능하며, 타임스탬프로 사용자 활동을 추적할 수 있습니다. 또한 profileImage 같은 선택적 필드를 통해 나중에 기능을 확장하기도 쉽습니다.
실전 팁
💡 비밀번호는 절대 평문으로 저장하지 마세요. bcrypt나 argon2 같은 해싱 알고리즘으로 암호화하여 저장해야 합니다. VARCHAR(255)는 해시된 비밀번호를 저장하기에 충분한 길이입니다.
💡 이메일 컬럼에는 반드시 UNIQUE 제약조건과 인덱스를 함께 설정하세요. UNIQUE는 중복을 막고, 인덱스는 검색 속도를 높입니다. 둘 다 필요합니다.
💡 VARCHAR의 길이는 실제 사용할 데이터에 맞게 설정하세요. email은 255, name은 100 정도면 충분합니다. 너무 크게 설정하면 저장공간이 낭비됩니다.
💡 소프트 삭제(Soft Delete)를 구현하려면 deletedAt TIMESTAMP 컬럼을 추가하세요. 사용자를 실제로 삭제하지 않고 deletedAt에 날짜를 기록하면 나중에 복구할 수 있습니다.
💡 역할 기반 권한 관리가 필요하면 role VARCHAR(50) DEFAULT 'user' 컬럼을 추가하세요. 'user', 'admin', 'moderator' 같은 값으로 권한을 구분할 수 있습니다.
3. 채팅방 테이블 스키마
시작하며
여러분이 그룹 채팅 기능을 만들 때 이런 상황을 겪어본 적 있나요? "채팅방 정보는 어디에 저장하지?
누가 어느 채팅방에 속해 있는지는 어떻게 관리하지?" 메시지만 저장하면 될 것 같지만, 실제로는 채팅방 자체를 관리하는 것도 중요한 문제입니다. 이런 문제는 채팅방과 사용자의 관계를 제대로 설계하지 않으면 복잡해집니다.
예를 들어, 채팅방 이름을 바꾸려고 할 때 관련된 모든 메시지를 수정해야 한다면 비효율적이고, 누가 채팅방 멤버인지 확인하려고 모든 메시지를 뒤져야 한다면 성능이 떨어집니다. 바로 이럴 때 필요한 것이 독립적인 채팅방 테이블입니다.
채팅방의 정보를 별도로 관리하고, 사용자 및 메시지와 깔끔하게 연결하여 효율적인 채팅 시스템을 만들 수 있습니다.
개요
간단히 말해서, 채팅방 테이블은 각 채팅방의 정보를 저장하는 곳입니다. 채팅방 이름, 생성일, 설정 등 채팅방 자체와 관련된 모든 데이터를 관리합니다.
왜 채팅방 테이블이 필요할까요? 채팅방과 메시지를 분리하면 데이터 관리가 훨씬 쉬워지기 때문입니다.
예를 들어, "개발팀 회의" 채팅방의 이름을 "기획팀 회의"로 바꾼다면, 채팅방 테이블의 한 행만 수정하면 됩니다. 만약 채팅방 정보가 각 메시지에 중복 저장되어 있다면 수천 개의 메시지를 모두 수정해야 하겠죠.
기존에는 채팅방 이름 같은 정보를 메시지마다 저장했다면, 이제는 채팅방 테이블에 한 번만 저장하고 메시지는 chatRoomId로 참조하게 할 수 있습니다. 채팅방 테이블의 핵심 특징은 세 가지입니다: 고유 식별자로 각 채팅방을 구분하고, 채팅방 메타데이터(이름, 설정 등)를 중앙에서 관리하며, 다른 테이블과의 관계를 통해 사용자 및 메시지와 연결됩니다.
이러한 정규화된 구조가 데이터 중복을 없애고 관리를 쉽게 만듭니다.
코드 예제
-- PostgreSQL 채팅방 테이블 생성 SQL
CREATE TABLE "ChatRoom" (
-- 고유 식별자 (자동 증가)
id SERIAL PRIMARY KEY,
-- 채팅방 이름 (필수)
name VARCHAR(200) NOT NULL,
-- 채팅방 설명 (선택)
description TEXT,
-- 채팅방 타입 (개인/그룹)
type VARCHAR(20) NOT NULL DEFAULT 'group',
-- 최대 참여자 수 (선택)
maxMembers INTEGER DEFAULT 100,
-- 채팅방 생성일 (자동 생성)
"createdAt" TIMESTAMP NOT NULL DEFAULT NOW(),
-- 수정일 (자동 업데이트)
"updatedAt" TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 채팅방 타입별 검색을 위한 인덱스
CREATE INDEX idx_chatroom_type ON "ChatRoom"(type);
-- 최근 생성된 채팅방 조회를 위한 인덱스
CREATE INDEX idx_chatroom_created ON "ChatRoom"("createdAt" DESC);
설명
이것이 하는 일: 채팅방 테이블은 각 채팅방의 메타데이터를 중앙에서 관리합니다. 마치 아파트 관리사무소가 각 세대의 정보를 관리하듯이, 모든 채팅방의 기본 정보를 한곳에 모아서 체계적으로 관리합니다.
첫 번째로, id 컬럼이 각 채팅방을 고유하게 식별합니다. 사용자 테이블과 마찬가지로 SERIAL PRIMARY KEY를 사용하여 새로운 채팅방이 생성될 때마다 자동으로 번호가 부여됩니다.
이 id는 나중에 메시지 테이블에서 "이 메시지가 어느 채팅방의 것인지"를 가리키는 데 사용됩니다. 그 다음으로, type 컬럼이 채팅방의 종류를 구분합니다.
'group'은 여러 사람이 참여하는 그룹 채팅이고, 'direct'는 1대1 개인 채팅입니다. 이렇게 타입을 구분하면 화면에서 그룹 채팅만 따로 보여주거나, 개인 채팅에만 특정 기능을 적용하는 등의 작업이 쉬워집니다.
type 컬럼에 인덱스를 추가한 이유도 이런 필터링 작업을 빠르게 하기 위해서입니다. 세 번째로, maxMembers 컬럼이 채팅방의 최대 인원을 제한합니다.
기본값은 100명이지만, VIP 채팅방이나 소규모 미팅방은 다른 값으로 설정할 수 있습니다. 새로운 사용자가 채팅방에 참여하려고 할 때 현재 인원 수를 maxMembers와 비교하여 허용 여부를 결정할 수 있습니다.
마지막으로, description 컬럼은 TEXT 타입으로 긴 설명을 저장할 수 있습니다. VARCHAR는 길이 제한이 있지만 TEXT는 제한이 없어서 채팅방 소개, 규칙, 공지사항 같은 긴 내용을 저장하기 적합합니다.
또한 createdAt에 DESC 인덱스를 추가하여 최근 생성된 채팅방을 빠르게 조회할 수 있게 했습니다. 여러분이 이 스키마를 사용하면 채팅방 정보를 효율적으로 관리할 수 있습니다.
채팅방 이름이나 설정을 바꿀 때 한 곳만 수정하면 되고, 타입별로 채팅방을 분류하기 쉬우며, 인덱스 덕분에 검색도 빠릅니다. 또한 나중에 채팅방 이미지, 태그, 카테고리 같은 기능을 추가하기도 쉬운 확장 가능한 구조입니다.
실전 팁
💡 채팅방 타입은 ENUM 타입을 사용하는 것도 좋습니다. CREATE TYPE chatroom_type AS ENUM ('group', 'direct', 'channel')로 정의하면 오타나 잘못된 값 입력을 방지할 수 있습니다.
💡 채팅방 생성자를 추적하려면 createdBy INTEGER REFERENCES "User"(id) 컬럼을 추가하세요. 나중에 채팅방 관리 권한을 부여하거나 통계를 낼 때 유용합니다.
💡 비공개 채팅방 기능을 구현하려면 isPrivate BOOLEAN DEFAULT false와 password VARCHAR(255) 컬럼을 추가하세요. 비밀번호로 보호된 채팅방을 만들 수 있습니다.
💡 채팅방 아카이브 기능을 위해 isArchived BOOLEAN DEFAULT false를 추가하세요. 삭제하지 않고 보관 처리하면 나중에 다시 활성화할 수 있습니다.
💡 채팅방 검색 기능을 위해 name과 description에 풀텍스트 검색 인덱스를 추가하세요. PostgreSQL의 GIN 인덱스와 tsvector를 사용하면 한글 검색도 가능합니다.
4. 메시지 테이블 스키마
시작하며
여러분이 채팅 메시지를 저장할 때 이런 고민을 해본 적 있나요? "메시지가 수십만 개가 되면 느려지지 않을까?
누가 보냈는지, 어느 채팅방의 메시지인지 어떻게 연결하지? 삭제된 메시지는 어떻게 처리하지?" 단순해 보이는 메시지 저장이 실제로는 많은 것을 고려해야 하는 복잡한 작업입니다.
이런 문제는 메시지 테이블을 잘못 설계하면 치명적입니다. 외래키를 설정하지 않으면 삭제된 사용자의 메시지가 고아 데이터가 되고, 인덱스가 없으면 메시지 로딩이 점점 느려지며, 소프트 삭제를 고려하지 않으면 실수로 삭제한 메시지를 복구할 수 없습니다.
바로 이럴 때 필요한 것이 제대로 된 메시지 테이블 스키마입니다. 외래키로 데이터 무결성을 보장하고, 인덱스로 빠른 조회를 가능하게 하며, 확장 가능한 구조로 미래의 기능 추가를 대비할 수 있습니다.
개요
간단히 말해서, 메시지 테이블은 사용자가 주고받는 모든 채팅 메시지를 저장하는 곳입니다. 메시지 내용, 작성자, 채팅방, 작성 시간 등을 기록하며 채팅 앱의 핵심 데이터입니다.
왜 메시지 테이블이 중요할까요? 채팅 앱에서 가장 자주 읽고 쓰는 데이터가 바로 메시지이기 때문입니다.
사용자가 채팅방에 들어갈 때마다 최근 메시지를 불러오고, 새 메시지가 올 때마다 저장하고, 검색할 때마다 메시지를 조회합니다. 예를 들어, 100만 개의 메시지 중에서 특정 채팅방의 최근 50개만 빠르게 가져와야 하는데, 이것이 제대로 설계되지 않으면 앱 전체가 느려집니다.
기존에는 메시지를 단순히 배열에 저장했다면, 이제는 데이터베이스에 체계적으로 저장하고 외래키로 사용자 및 채팅방과 연결할 수 있습니다. 메시지 테이블의 핵심 특징은 네 가지입니다: 외래키로 User와 ChatRoom을 참조하여 관계를 명확히 하고, 인덱스로 빠른 메시지 조회를 보장하며, 타임스탬프로 시간 순서를 관리하고, 선택적 컬럼으로 다양한 메시지 타입을 지원합니다.
이러한 설계가 대용량 메시지를 효율적으로 관리하게 해줍니다.
코드 예제
-- PostgreSQL 메시지 테이블 생성 SQL
CREATE TABLE "Message" (
-- 고유 식별자 (자동 증가)
id SERIAL PRIMARY KEY,
-- 메시지 내용 (필수)
content TEXT NOT NULL,
-- 작성자 ID (외래키, 필수)
"userId" INTEGER NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
-- 채팅방 ID (외래키, 필수)
"chatRoomId" INTEGER NOT NULL REFERENCES "ChatRoom"(id) ON DELETE CASCADE,
-- 메시지 타입 (텍스트/이미지/파일)
type VARCHAR(20) NOT NULL DEFAULT 'text',
-- 읽음 여부
isRead BOOLEAN NOT NULL DEFAULT false,
-- 작성일 (자동 생성)
"createdAt" TIMESTAMP NOT NULL DEFAULT NOW(),
-- 수정일
"updatedAt" TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 채팅방별 메시지 조회를 위한 복합 인덱스 (가장 중요!)
CREATE INDEX idx_message_chatroom_created ON "Message"("chatRoomId", "createdAt" DESC);
-- 사용자별 메시지 조회를 위한 인덱스
CREATE INDEX idx_message_user ON "Message"("userId");
-- 읽지 않은 메시지 조회를 위한 인덱스
CREATE INDEX idx_message_unread ON "Message"("chatRoomId", isRead) WHERE isRead = false;
설명
이것이 하는 일: 메시지 테이블은 채팅 앱의 핵심 데이터인 모든 대화 내용을 저장하고 관리합니다. 마치 우체국이 모든 편지를 보관하고 누가 누구에게 보냈는지 기록하듯이, 메시지의 내용과 맥락을 완벽하게 보존합니다.
첫 번째로, userId와 chatRoomId가 외래키(FOREIGN KEY)로 정의되어 있습니다. REFERENCES "User"(id)는 "userId는 User 테이블의 id를 참조한다"는 뜻이고, ON DELETE CASCADE는 "사용자가 삭제되면 그 사용자의 메시지도 자동으로 삭제한다"는 뜻입니다.
이렇게 하면 데이터베이스가 자동으로 데이터 무결성을 보장해줍니다. 존재하지 않는 userId나 chatRoomId를 입력하려고 하면 에러가 발생하여 잘못된 데이터 입력을 방지합니다.
그 다음으로, 가장 중요한 복합 인덱스 idx_message_chatroom_created를 살펴보겠습니다. 이 인덱스는 (chatRoomId, createdAt DESC) 두 컬럼을 함께 사용합니다.
왜 이렇게 할까요? 채팅방에 들어가면 "이 채팅방의 최근 메시지 50개"를 가져오는데, 이 쿼리가 가장 자주 실행되기 때문입니다.
chatRoomId로 먼저 필터링하고 createdAt으로 정렬하는 작업을 인덱스가 최적화해줍니다. 세 번째로, 조건부 인덱스(Partial Index) idx_message_unread를 추가했습니다.
WHERE isRead = false 조건이 붙어있는데, 이것은 읽지 않은 메시지만 인덱싱한다는 뜻입니다. 전체 메시지의 90%가 이미 읽은 메시지라면, 10%만 인덱싱하여 저장공간을 절약하면서도 "읽지 않은 메시지 개수" 같은 쿼리를 빠르게 실행할 수 있습니다.
마지막으로, type 컬럼이 다양한 메시지 타입을 지원합니다. 'text'는 일반 텍스트, 'image'는 이미지, 'file'은 파일 등으로 구분할 수 있습니다.
이미지나 파일의 경우 content에 URL을 저장하고, type을 보고 화면에서 다르게 렌더링할 수 있습니다. 나중에 음성 메시지, 위치 공유 같은 기능을 추가할 때도 type만 늘리면 됩니다.
여러분이 이 스키마를 사용하면 수백만 개의 메시지도 효율적으로 관리할 수 있습니다. 복합 인덱스 덕분에 채팅방 입장이 빠르고, 외래키 덕분에 데이터 무결성이 보장되며, 조건부 인덱스 덕분에 읽지 않은 메시지 카운트가 빠르고, 확장 가능한 type 덕분에 다양한 메시지 형태를 지원할 수 있습니다.
실전 팁
💡 메시지 페이지네이션을 구현할 때는 OFFSET보다 커서 기반 페이지네이션을 사용하세요. WHERE id < ? ORDER BY id DESC LIMIT 50 형태가 훨씬 빠릅니다.
💡 메시지 수정 기록을 남기려면 별도의 MessageEdit 테이블을 만드세요. originalContent, editedContent, editedAt을 저장하여 수정 이력을 추적할 수 있습니다.
💡 대용량 메시지 처리를 위해 파티셔닝을 고려하세요. createdAt을 기준으로 월별로 테이블을 나누면 오래된 메시지 조회 성능이 떨어지지 않습니다.
💡 실시간 채팅을 위해서는 데이터베이스뿐만 아니라 Redis나 WebSocket도 함께 사용하세요. 새 메시지는 Redis에 캐싱하고 백그라운드에서 데이터베이스에 저장하는 방식이 효율적입니다.
💡 메시지 검색 기능을 위해 content 컬럼에 풀텍스트 인덱스를 추가하세요. 다만 모든 메시지를 인덱싱하면 부담이 크므로, 최근 3개월 메시지만 인덱싱하는 등 범위를 제한하는 것이 좋습니다.
5. Prisma 스키마 작성
시작하며
여러분이 SQL을 직접 작성하면서 이런 불편함을 느낀 적 있나요? "테이블마다 CREATE TABLE을 일일이 작성해야 하고, 컬럼 하나 추가하려면 ALTER TABLE을 써야 하고, JavaScript 코드에서 쿼리를 문자열로 작성하다 보니 오타가 나도 실행하기 전까지 모르겠다..." 데이터베이스 작업이 번거롭고 실수하기 쉽습니다.
이런 문제는 전통적인 방식으로 데이터베이스를 다룰 때 항상 발생합니다. SQL 문법 오류는 런타임에만 발견되고, 테이블 구조가 바뀌면 관련된 모든 쿼리를 찾아서 수정해야 하며, 관계 설정이 복잡하면 JOIN 쿼리도 복잡해집니다.
바로 이럴 때 필요한 것이 Prisma입니다. TypeScript로 스키마를 정의하면 타입 안정성을 얻고, 마이그레이션을 자동으로 생성하며, 직관적인 API로 데이터베이스를 다룰 수 있습니다.
개요
간단히 말해서, Prisma는 데이터베이스를 TypeScript 친화적으로 다룰 수 있게 해주는 ORM(Object-Relational Mapping) 도구입니다. SQL 대신 JavaScript 객체처럼 데이터베이스를 다룰 수 있게 해줍니다.
왜 Prisma가 필요할까요? 타입 안정성, 자동완성, 마이그레이션 관리, 직관적인 쿼리 API 등 개발 경험을 크게 향상시키기 때문입니다.
예를 들어, user.email을 user.emai로 오타 내면 SQL 문자열에서는 런타임 에러가 나지만, Prisma에서는 VSCode가 즉시 빨간 줄로 알려줍니다. 기존에는 SQL 쿼리를 문자열로 작성하고 결과를 수동으로 파싱했다면, 이제는 prisma.user.findMany() 같은 TypeScript 함수로 타입이 보장된 결과를 바로 받을 수 있습니다.
Prisma 스키마의 핵심 특징은 세 가지입니다: schema.prisma 파일에 모든 테이블을 선언적으로 정의하고, @relation으로 테이블 간 관계를 명확하게 표현하며, prisma generate로 타입 안전한 클라이언트 코드를 자동 생성합니다. 이러한 특징들이 개발 생산성을 크게 높여줍니다.
코드 예제
// schema.prisma - 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
password String
profileImage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 관계: 한 사용자는 여러 메시지를 작성
messages Message[]
// 관계: 한 사용자는 여러 채팅방에 참여 (N:M)
chatRooms ChatRoom[]
}
model ChatRoom {
id Int @id @default(autoincrement())
name String
description String?
type String @default("group")
maxMembers Int @default(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 관계: 한 채팅방에는 여러 메시지
messages Message[]
// 관계: 한 채팅방에는 여러 사용자 (N:M)
users User[]
}
model Message {
id Int @id @default(autoincrement())
content String
type String @default("text")
isRead Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 관계: 메시지는 한 명의 작성자
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
// 관계: 메시지는 한 채팅방에 속함
chatRoom ChatRoom @relation(fields: [chatRoomId], references: [id], onDelete: Cascade)
chatRoomId Int
@@index([chatRoomId, createdAt(sort: Desc)])
@@index([userId])
}
설명
이것이 하는 일: Prisma 스키마는 데이터베이스의 전체 구조를 하나의 파일에 선언적으로 정의합니다. 마치 건축 설계도처럼, 이 파일 하나만 보면 어떤 테이블이 있고 어떻게 연결되어 있는지 한눈에 알 수 있습니다.
첫 번째로, model 키워드로 각 테이블을 정의합니다. model User는 User 테이블을 의미하고, 그 안에 id, email, name 같은 필드를 나열합니다.
@id는 PRIMARY KEY, @unique는 유니크 제약, @default(now())는 기본값 NOW() 같은 SQL 개념을 간결하게 표현합니다. SQL보다 훨씬 읽기 쉽고, TypeScript와 비슷한 문법이라 익숙합니다.
그 다음으로, 관계를 정의하는 부분을 보겠습니다. Message 모델에서 user User @relation(...)은 "Message는 User를 참조한다"는 뜻입니다.
fields: [userId]는 외래키 컬럼이 userId이고, references: [id]는 User 테이블의 id를 참조한다는 의미입니다. onDelete: Cascade는 사용자 삭제 시 메시지도 함께 삭제된다는 SQL의 ON DELETE CASCADE와 같습니다.
이렇게 관계를 명시하면 Prisma가 자동으로 JOIN 쿼리를 생성해줍니다. 세 번째로, 양방향 관계를 살펴보겠습니다.
User 모델에는 messages Message[]가 있고, Message 모델에는 user User가 있습니다. 이것은 Prisma의 특징인데, 양쪽에서 서로를 참조할 수 있게 해줍니다.
코드에서 user.messages로 그 사용자의 모든 메시지를 가져올 수도 있고, message.user로 메시지 작성자를 가져올 수도 있습니다. 매우 직관적이죠.
마지막으로, @@index로 인덱스를 정의합니다. @@index([chatRoomId, createdAt(sort: Desc)])는 복합 인덱스를 만들고 createdAt은 내림차순으로 정렬한다는 뜻입니다.
이것이 SQL의 CREATE INDEX idx_message_chatroom_created ON "Message"("chatRoomId", "createdAt" DESC)와 정확히 같습니다. 여러분이 Prisma 스키마를 작성하면 여러 장점을 얻습니다.
첫째, prisma generate를 실행하면 타입이 완벽하게 정의된 클라이언트 코드가 생성되어 VSCode에서 자동완성이 됩니다. 둘째, 스키마를 수정하면 prisma migrate dev로 SQL 마이그레이션 파일이 자동 생성됩니다.
셋째, 관계를 명시했기 때문에 include: { user: true }만 추가하면 JOIN 쿼리가 자동으로 실행됩니다.
실전 팁
💡 schema.prisma 파일은 프로젝트 루트의 prisma/ 폴더에 위치시키는 것이 관례입니다. Prisma CLI가 자동으로 이 위치를 찾습니다.
💡 DATABASE_URL은 .env 파일에 저장하세요. postgresql://user:password@localhost:5432/dbname 형식으로 작성하고, .env를 .gitignore에 추가하여 비밀번호가 노출되지 않게 하세요.
💡 스키마를 수정할 때마다 prisma format을 실행하면 자동으로 포맷팅되어 일관성 있는 코드를 유지할 수 있습니다. VSCode Prisma 확장을 설치하면 저장 시 자동 포맷팅됩니다.
💡 N:M 관계는 암시적(implicit) 방식과 명시적(explicit) 방식이 있습니다. 위 예제는 암시적 방식인데, 중간 테이블을 직접 제어하려면 명시적 방식으로 UserChatRoom 모델을 만드세요.
💡 Prisma Studio를 사용하면 GUI로 데이터를 확인하고 수정할 수 있습니다. npx prisma studio를 실행하면 브라우저에서 데이터베이스를 시각적으로 관리할 수 있어 매우 편리합니다.
6. 마이그레이션 실행
시작하며
여러분이 Prisma 스키마를 작성한 후 이런 의문이 들지 않았나요? "스키마 파일만 있으면 실제 데이터베이스에 테이블이 자동으로 생기나?
아니면 뭔가 더 해야 하나?" 스키마 정의와 실제 데이터베이스 생성은 별개의 단계입니다. 이런 혼란은 ORM을 처음 사용할 때 자주 발생합니다.
스키마 파일은 설계도일 뿐이고, 실제로 데이터베이스에 테이블을 만들려면 마이그레이션이라는 과정이 필요합니다. 마이그레이션 없이 코드를 실행하면 "테이블이 존재하지 않습니다" 에러가 발생합니다.
바로 이럴 때 필요한 것이 Prisma 마이그레이션입니다. 스키마 파일을 기반으로 SQL 마이그레이션 파일을 자동 생성하고, 데이터베이스에 적용하여 실제 테이블을 만들어줍니다.
개요
간단히 말해서, 마이그레이션은 스키마의 변경사항을 실제 데이터베이스에 반영하는 과정입니다. Prisma가 schema.prisma를 읽고 자동으로 CREATE TABLE, ALTER TABLE 같은 SQL을 생성하여 실행해줍니다.
왜 마이그레이션이 필요할까요? 데이터베이스 스키마 변경을 안전하고 추적 가능하게 관리하기 위해서입니다.
예를 들어, 팀원들과 협업할 때 여러분이 User 테이블에 profileImage 컬럼을 추가했다면, 마이그레이션 파일을 Git에 커밋하여 다른 팀원들도 같은 변경사항을 적용할 수 있습니다. 기존에는 개발자가 직접 ALTER TABLE 같은 SQL을 작성하고 실행해야 했다면, 이제는 Prisma가 스키마 변경을 감지하고 필요한 SQL을 자동으로 생성해줍니다.
마이그레이션의 핵심 특징은 세 가지입니다: 스키마 변경사항을 자동으로 감지하고, 버전 관리 가능한 SQL 파일을 생성하며, 롤백과 히스토리 추적이 가능합니다. 이러한 특징들이 데이터베이스 스키마를 코드처럼 관리할 수 있게 해줍니다.
코드 예제
# 1. 개발 환경에서 마이그레이션 생성 및 적용
npx prisma migrate dev --name init
# 위 명령어가 하는 일:
# - schema.prisma를 읽어서 변경사항 감지
# - prisma/migrations/ 폴더에 SQL 파일 자동 생성
# - 데이터베이스에 마이그레이션 적용 (CREATE TABLE 실행)
# - Prisma Client 재생성 (타입 업데이트)
# 2. 생성된 마이그레이션 파일 확인
# prisma/migrations/20240115120000_init/migration.sql
# 이 파일에 실제 SQL 코드가 들어있음
# 3. 프로덕션 환경에서 마이그레이션 적용
npx prisma migrate deploy
# 4. 마이그레이션 히스토리 확인
npx prisma migrate status
# 5. 데이터베이스를 스키마에 맞게 강제 동기화 (주의: 데이터 손실 가능)
npx prisma db push
설명
이것이 하는 일: 마이그레이션은 설계도(schema.prisma)를 보고 실제 건물(데이터베이스 테이블)을 짓는 과정입니다. 마치 건축가가 설계도를 건설팀에게 전달하면 건설팀이 실제로 건물을 짓듯이, Prisma가 스키마를 SQL로 변환하여 데이터베이스에 적용합니다.
첫 번째로, npx prisma migrate dev --name init을 실행하면 어떤 일이 일어나는지 보겠습니다. Prisma는 현재 schema.prisma와 실제 데이터베이스를 비교하여 차이를 감지합니다.
테이블이 하나도 없다면 "User, ChatRoom, Message 테이블을 새로 만들어야겠구나"라고 판단하고, prisma/migrations/ 폴더에 날짜_이름 형식으로 폴더를 만들고 그 안에 migration.sql 파일을 생성합니다. 그 다음으로, 생성된 migration.sql 파일을 열어보면 우리가 앞서 작성했던 CREATE TABLE, CREATE INDEX 같은 SQL 문이 들어있습니다.
Prisma가 스키마를 분석하여 자동으로 만든 것이죠. 이 파일은 Git에 커밋되어야 합니다.
왜냐하면 다른 팀원이나 프로덕션 서버에서도 똑같은 SQL을 실행해야 하기 때문입니다. 세 번째로, Prisma는 _prisma_migrations라는 특별한 테이블을 데이터베이스에 만듭니다.
이 테이블은 어떤 마이그레이션이 언제 적용되었는지 기록합니다. 마치 Git 커밋 로그처럼, 데이터베이스 변경 히스토리를 추적할 수 있습니다.
나중에 User 테이블에 컬럼을 추가하는 두 번째 마이그레이션을 만들면, Prisma는 "첫 번째는 이미 적용되었으니 두 번째만 실행하면 되겠구나"라고 판단합니다. 마지막으로, 프로덕션 환경에서는 prisma migrate deploy를 사용합니다.
이것은 dev와 달리 새로운 마이그레이션을 생성하지 않고, 이미 만들어진 마이그레이션 파일만 적용합니다. 왜 이렇게 구분할까요?
프로덕션은 실제 사용자 데이터가 있는 곳이라서 자동으로 변경사항을 감지하고 적용하는 것이 위험하기 때문입니다. 개발 환경에서 충분히 테스트한 마이그레이션만 프로덕션에 적용하는 것이 안전합니다.
여러분이 마이그레이션을 올바르게 사용하면 여러 이점이 있습니다. 스키마 변경이 모두 파일로 기록되어 언제든지 히스토리를 확인할 수 있고, 팀원들과 데이터베이스 구조를 쉽게 공유할 수 있으며, 문제가 생기면 이전 버전으로 롤백할 수도 있습니다.
또한 CI/CD 파이프라인에서 자동으로 마이그레이션을 적용하여 배포 과정을 자동화할 수 있습니다.
실전 팁
💡 첫 마이그레이션 이름은 보통 "init"으로 짓습니다. 이후 마이그레이션은 "add_user_role", "create_index_on_email" 처럼 변경 내용을 명확히 표현하세요.
💡 마이그레이션을 생성한 후에는 반드시 생성된 SQL 파일을 검토하세요. 특히 프로덕션에 적용하기 전에는 더욱 중요합니다. 예상치 못한 DROP TABLE이나 데이터 손실 가능성을 미리 확인할 수 있습니다.
💡 개발 중에 빠르게 테스트하려면 prisma db push를 사용할 수 있습니다. 마이그레이션 파일을 만들지 않고 바로 데이터베이스에 반영하여 빠르지만, 프로덕션에서는 절대 사용하지 마세요.
💡 마이그레이션 충돌이 생겼을 때는 prisma migrate resolve를 사용하세요. 여러 명이 동시에 마이그레이션을 만들면 충돌할 수 있는데, 이 명령어로 해결할 수 있습니다.
💡 대용량 데이터베이스에서 컬럼 추가나 인덱스 생성은 시간이 오래 걸릴 수 있습니다. 프로덕션 마이그레이션 전에 스테이징 환경에서 실제 데이터 규모로 테스트하여 예상 소요 시간을 파악하세요.
7. 관계 설정 (1:N, N:M)
시작하며
여러분이 데이터베이스를 설계할 때 이런 상황을 고민해본 적 있나요? "한 명의 사용자가 여러 메시지를 쓸 수 있고, 한 명의 사용자가 여러 채팅방에 참여할 수 있다면, 이 관계를 어떻게 표현하지?" 테이블 간의 관계를 잘못 설계하면 데이터 중복, 불필요한 조인, 성능 저하가 발생합니다.
이런 문제는 관계형 데이터베이스의 핵심 개념인 1:N(일대다)과 N:M(다대다) 관계를 제대로 이해하지 못할 때 생깁니다. 예를 들어, 사용자와 채팅방의 N:M 관계를 User 테이블에 chatRoomIds 배열로 저장하면 정규화를 위반하고, 나중에 "이 채팅방에 누가 있나?"를 찾기 어려워집니다.
바로 이럴 때 필요한 것이 제대로 된 관계 설정입니다. 1:N은 외래키로, N:M은 중간 테이블로 구현하여 데이터베이스의 정규화를 유지하고 효율적인 쿼리를 가능하게 합니다.
개요
간단히 말해서, 관계 설정은 테이블 간의 연결 방식을 정의하는 것입니다. 1:N 관계는 "한 쪽은 하나, 다른 쪽은 여러 개"이고, N:M 관계는 "양쪽 모두 여러 개"입니다.
왜 관계 설정이 중요할까요? 데이터를 효율적으로 저장하고 빠르게 조회하기 위해서입니다.
예를 들어, User-Message는 1:N 관계인데(한 사용자가 여러 메시지 작성), Message 테이블에 userId 외래키 하나만 추가하면 됩니다. 하지만 User-ChatRoom은 N:M 관계인데(한 사용자가 여러 채팅방, 한 채팅방에 여러 사용자), 이것은 중간 테이블이 필요합니다.
기존에는 관계를 배열이나 JSON으로 저장했다면(예: user.chatRoomIds = [1, 2, 3]), 이제는 정규화된 관계 테이블로 관리하여 데이터 무결성과 쿼리 성능을 모두 확보할 수 있습니다. 관계 설정의 핵심 특징은 세 가지입니다: 1:N은 외래키(FOREIGN KEY)로 구현하고, N:M은 중간 테이블(Junction Table)로 구현하며, 양방향 관계를 통해 어느 쪽에서든 조회가 가능합니다.
이러한 설계가 관계형 데이터베이스의 장점을 최대한 활용하게 해줍니다.
코드 예제
// 1:N 관계 예제 - User와 Message
// Prisma에서 사용하는 방법
// 1. 메시지를 작성한 사용자 정보 가져오기
const message = await prisma.message.findUnique({
where: { id: 1 },
include: { user: true } // JOIN을 자동으로 실행
});
console.log(message.user.name); // 작성자 이름
// 2. 특정 사용자가 작성한 모든 메시지 가져오기
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { messages: true } // 1:N의 N쪽 조회
});
console.log(user.messages.length); // 메시지 개수
// N:M 관계 예제 - User와 ChatRoom
// (Prisma가 자동으로 중간 테이블 _ChatRoomToUser 생성)
// 3. 사용자가 참여한 모든 채팅방 가져오기
const userWithRooms = await prisma.user.findUnique({
where: { id: 1 },
include: { chatRooms: true }
});
console.log(userWithRooms.chatRooms); // 참여 중인 채팅방 목록
// 4. 채팅방의 모든 참여자 가져오기
const roomWithUsers = await prisma.chatRoom.findUnique({
where: { id: 1 },
include: { users: true }
});
console.log(roomWithUsers.users); // 채팅방 멤버 목록
// 5. 사용자를 채팅방에 추가 (N:M 관계 연결)
await prisma.user.update({
where: { id: 1 },
data: {
chatRooms: {
connect: { id: 2 } // chatRoom id=2에 연결
}
}
});
설명
이것이 하는 일: 관계 설정은 독립된 테이블들을 논리적으로 연결하여 하나의 의미 있는 데이터 구조를 만듭니다. 마치 레고 블록을 조립하듯이, 각 테이블이라는 블록을 관계라는 연결고리로 이어서 완전한 시스템을 구성합니다.
첫 번째로, 1:N 관계를 살펴보겠습니다. User와 Message의 관계에서 "한 명의 사용자는 여러 메시지를 작성할 수 있지만, 하나의 메시지는 한 명의 작성자만 있다"는 것이 1:N입니다.
구현 방법은 간단합니다. Message 테이블에 userId 컬럼을 추가하고 User 테이블의 id를 참조하게 만듭니다.
이렇게 하면 message.userId를 보고 "이 메시지는 누가 썼는가"를 즉시 알 수 있고, WHERE userId = 1로 "이 사용자가 쓴 모든 메시지"도 쉽게 찾을 수 있습니다. 그 다음으로, N:M 관계를 보겠습니다.
User와 ChatRoom의 관계는 "한 명의 사용자가 여러 채팅방에 참여하고, 한 채팅방에도 여러 사용자가 있다"는 양방향 다대다 관계입니다. 이것은 외래키 하나로는 표현할 수 없습니다.
User 테이블에 chatRoomId를 넣으면 하나의 채팅방만 참여할 수 있게 되고, 배열로 저장하면 정규화를 위반합니다. 해결책은 중간 테이블(Junction Table)을 만드는 것입니다.
세 번째로, Prisma의 암시적 N:M 관계를 이해해봅시다. schema.prisma에서 User의 chatRooms ChatRoom[]과 ChatRoom의 users User[]만 정의하면, Prisma가 자동으로 _ChatRoomToUser라는 중간 테이블을 만들어줍니다.
이 테이블은 두 개의 외래키 컬럼(userId, chatRoomId)만 가지고 있으며, (userId=1, chatRoomId=2) 같은 행이 "사용자 1이 채팅방 2에 참여한다"는 의미입니다. 직접 테이블을 만들지 않아도 Prisma가 알아서 처리해주니 편리합니다.
마지막으로, include를 사용한 관계 조회를 보겠습니다. include: { user: true }는 SQL의 JOIN과 같은 역할을 합니다.
Message를 조회하면서 동시에 작성자 정보도 가져오는 것이죠. Prisma는 이것을 자동으로 최적화하여 필요한 만큼만 JOIN을 수행합니다.
또한 connect, disconnect, create 같은 메서드로 관계를 쉽게 연결하거나 해제할 수 있어서, 복잡한 UPDATE 쿼리를 작성할 필요가 없습니다. 여러분이 관계를 올바르게 설정하면 데이터 무결성이 보장됩니다.
외래키 제약조건 덕분에 존재하지 않는 userId를 입력할 수 없고, ON DELETE CASCADE 덕분에 사용자 삭제 시 관련 메시지도 자동으로 정리되며, 중간 테이블 덕분에 N:M 관계도 정규화된 형태로 관리됩니다. 또한 Prisma의 타입 시스템이 관계를 완벽히 이해하고 있어서, user.messages처럼 직관적인 코드로 복잡한 JOIN 쿼리를 실행할 수 있습니다.
실전 팁
💡 N:M 관계에서 추가 정보가 필요하면 명시적 중간 테이블을 만드세요. 예를 들어, 사용자가 채팅방에 참여한 날짜를 저장하려면 UserChatRoom 모델을 직접 정의하고 joinedAt 컬럼을 추가하세요.
💡 순환 참조를 조심하세요. include: { user: { include: { messages: true } } }처럼 계속 include하면 무한 루프에 빠질 수 있습니다. 필요한 깊이만 조회하세요.
💡 성능을 위해 select를 활용하세요. include는 모든 컬럼을 가져오지만, select: { id: true, name: true }처럼 필요한 컬럼만 선택하면 데이터 전송량이 줄어듭니다.
💡 onDelete, onUpdate 옵션을 신중하게 설정하세요. Cascade는 자동 삭제, SetNull은 NULL로 설정, Restrict는 삭제 방지입니다. 실수로 중요한 데이터가 삭제되지 않도록 비즈니스 로직에 맞게 선택하세요.
💡 복잡한 쿼리는 Prisma의 쿼리 로그를 확인하세요. .env에 DEBUG="prisma:query"를 추가하면 실제 실행되는 SQL을 볼 수 있어 성능 최적화에 도움이 됩니다.