🗄️ 데이터베이스

PlanetScale

Vitess 기반 MySQL

YouTube를 확장한 Vitess 기반의 서버리스 MySQL 플랫폼으로, Git처럼 데이터베이스를 브랜칭하고, 다운타임 없이 스키마를 변경하며, 자동으로 수평 확장되는 MySQL 호환 데이터베이스입니다.

📖 상세 설명

PlanetScale은 YouTube의 MySQL 클러스터를 관리하기 위해 만들어진 오픈소스 프로젝트 Vitess를 기반으로 한 서버리스 MySQL 플랫폼입니다. 기존 MySQL과 100% 호환되면서도 대규모 트래픽을 처리할 수 있는 수평 확장성을 제공합니다.

핵심 기술: Vitess

YouTube가 개발한 MySQL 샤딩/클러스터링 기술. 단일 MySQL을 여러 샤드로 분산하면서도 애플리케이션에서는 단일 DB처럼 사용 가능.

PlanetScale 핵심 기능

🌿
Database Branching
Git처럼 DB 브랜치 생성
🔄
Non-blocking DDL
다운타임 없는 스키마 변경
📈
Horizontal Sharding
자동 수평 확장
🔍
Query Insights
쿼리 성능 분석
🐬
MySQL 호환
기존 MySQL 도구 사용
Rewind
특정 시점 복구

💻 코드 예제

1. PlanetScale CLI로 브랜치 워크플로우

CLI 명령어
# PlanetScale CLI 설치 brew install planetscale/tap/pscale # 로그인 pscale auth login # 데이터베이스 생성 pscale database create my-app --region us-east # 개발 브랜치 생성 (main에서 분기) pscale branch create my-app feature/add-orders # 브랜치에 연결 (로컬 프록시) pscale connect my-app feature/add-orders --port 3309 # 다른 터미널에서 MySQL 클라이언트로 접속 mysql -h 127.0.0.1 -P 3309 -u root # 브랜치에서 스키마 변경 pscale shell my-app feature/add-orders > CREATE TABLE orders ( > id BIGINT PRIMARY KEY AUTO_INCREMENT, > user_id BIGINT NOT NULL, > total DECIMAL(10,2) NOT NULL, > status ENUM('pending', 'paid', 'shipped') DEFAULT 'pending', > created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, > INDEX idx_user_id (user_id), > INDEX idx_status (status) > ); # Deploy Request 생성 (PR처럼) pscale deploy-request create my-app feature/add-orders # Deploy Request 적용 (main에 머지) pscale deploy-request deploy my-app 1

2. Node.js에서 PlanetScale 연결

TypeScript
import { connect } from '@planetscale/database'; // PlanetScale Serverless Driver (Edge 호환) const conn = connect({ host: process.env.DATABASE_HOST, username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, }); // 기본 쿼리 async function getUsers() { const results = await conn.execute('SELECT * FROM users WHERE active = ?', [true]); return results.rows; } // 파라미터화된 쿼리 (SQL Injection 방지) async function createOrder(userId: number, items: CartItem[]) { const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); // 트랜잭션 대신 단일 INSERT (PlanetScale 권장) const result = await conn.execute( 'INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)', [userId, total, 'pending'] ); const orderId = result.insertId; // 주문 상품 일괄 삽입 const values = items.map(item => `(${orderId}, ${item.productId}, ${item.quantity})`).join(','); await conn.execute( `INSERT INTO order_items (order_id, product_id, quantity) VALUES ${values}` ); return { orderId, total }; } // 복잡한 집계 쿼리 async function getOrderStats(userId: number) { const results = await conn.execute(` SELECT COUNT(*) as total_orders, SUM(total) as total_spent, AVG(total) as avg_order_value, MAX(created_at) as last_order_date FROM orders WHERE user_id = ? AND status != 'cancelled' `, [userId]); return results.rows[0]; }

3. Prisma와 PlanetScale 통합

schema.prisma
generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") relationMode = "prisma" // PlanetScale은 FK 미지원 } model User { id BigInt @id @default(autoincrement()) email String @unique name String? orders Order[] createdAt DateTime @default(now()) @@index([email]) } model Order { id BigInt @id @default(autoincrement()) userId BigInt user User @relation(fields: [userId], references: [id]) total Decimal @db.Decimal(10, 2) status OrderStatus @default(PENDING) items OrderItem[] createdAt DateTime @default(now()) @@index([userId]) @@index([status]) } model OrderItem { id BigInt @id @default(autoincrement()) orderId BigInt order Order @relation(fields: [orderId], references: [id]) productId BigInt quantity Int @@index([orderId]) } enum OrderStatus { PENDING PAID SHIPPED DELIVERED CANCELLED }

4. Non-blocking Schema Changes

Deploy Request 워크플로우
# 1. 개발 브랜치에서 스키마 변경 pscale shell my-app dev-branch # 대용량 테이블에 컬럼 추가 (기존 MySQL에서는 락 발생) ALTER TABLE orders ADD COLUMN shipping_address TEXT; ALTER TABLE orders ADD INDEX idx_created_at (created_at); # 2. Deploy Request 생성 pscale deploy-request create my-app dev-branch --notes "Add shipping address column" # 3. 스키마 변경 사항 확인 pscale deploy-request diff my-app 1 # 출력 예시: # -- orders # + shipping_address TEXT # + INDEX idx_created_at (created_at) # 4. Deploy Request 승인 및 적용 # (PlanetScale이 내부적으로 Ghost/pt-online-schema-change 같은 기법 사용) pscale deploy-request deploy my-app 1 # 결과: 테이블 락 없이 수백만 행 테이블도 스키마 변경 완료

5. Vercel + PlanetScale 통합

Next.js API Route
// pages/api/orders/[id].ts import { connect } from '@planetscale/database'; import type { NextApiRequest, NextApiResponse } from 'next'; const conn = connect({ host: process.env.DATABASE_HOST!, username: process.env.DATABASE_USERNAME!, password: process.env.DATABASE_PASSWORD!, }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { id } = req.query; if (req.method === 'GET') { const result = await conn.execute( `SELECT o.*, u.name as user_name, u.email FROM orders o JOIN users u ON o.user_id = u.id WHERE o.id = ?`, [id] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Order not found' }); } return res.json(result.rows[0]); } if (req.method === 'PATCH') { const { status } = req.body; await conn.execute( 'UPDATE orders SET status = ? WHERE id = ?', [status, id] ); return res.json({ success: true }); } res.status(405).json({ error: 'Method not allowed' }); }

💬 현업 대화 예시

DBA
"PlanetScale 쓰면 ALTER TABLE 할 때 테이블 락 안 걸려. 수천만 행 테이블도 다운타임 없이 스키마 바꿀 수 있어서 좋아."
백엔드 개발자
"Database Branching이 진짜 편해. PR 올리기 전에 브랜치 만들어서 스키마 테스트하고, 문제 없으면 Deploy Request 머지하면 돼."
시니어 개발자
"Foreign Key 안 되는 게 좀 불편하긴 해. Prisma에서 relationMode를 prisma로 설정하면 애플리케이션 레벨에서 관계 관리해."
DevOps 엔지니어
"Query Insights로 슬로우 쿼리 바로 찾을 수 있고, 인덱스 추천도 해줘서 성능 튜닝하기 좋아."

⚠️ 주의사항

Foreign Key 미지원

Vitess 아키텍처 특성상 데이터베이스 레벨 Foreign Key를 지원하지 않습니다. 애플리케이션 레벨에서 참조 무결성을 관리해야 합니다.

무료 플랜 제한

2024년 기준 무료 플랜이 폐지되었습니다. 취미 프로젝트나 소규모 앱에서는 비용을 고려해야 합니다.

일부 MySQL 기능 제한

일부 고급 MySQL 기능(저장 프로시저, 트리거 등)이 제한될 수 있습니다. 마이그레이션 전 호환성을 확인하세요.

🔗 관련 용어

📚 더 배우기