PlanetScale 서버리스 MySQL: 데이터베이스 브랜칭, Deploy Request, 스키마 변경, Prisma·Drizzle
이 글의 핵심
PlanetScale는 Vitess 위에서 MySQL 프로토콜을 제공하는 관리형 데이터베이스로, Git처럼 데이터베이스 브랜치와 Deploy Request로 스키마를 안전하게 배포합니다. 서버리스 런타임·ORM과의 연동, 외래 키와 스키마 되돌리기, 운영 시 알아야 할 제약까지 한 번에 정리합니다.
이 글의 핵심
PlanetScale은 Vitess를 기반으로 한 MySQL 호환 관리형 데이터베이스 서비스입니다. 단순히 호스팅된 MySQL이 아니라, 데이터베이스 브랜치와 Deploy Request로 스키마 변경을 코드 리뷰와 비슷한 절차로 옮기고, 프로덕션 테이블을 잠그지 않는 온라인 스키마 변경으로 배포 부담을 줄이도록 설계된 제품입니다.
가격·플랜·기능은 자주 바뀌었습니다. 2024년에는 무료 Hobby 플랜이 종료되었고, 이후 PostgreSQL 제품과 로컬 NVMe 기반 구성도 추가되었습니다. 운영 결정 전에는 PlanetScale 공식 문서와 가격 페이지를 반드시 확인하세요. 이 글은 MySQL(Vitess) 제품을 기준으로 설명합니다.
PlanetScale의 핵심 개념
Vitess와 MySQL 호환의 의미
PlanetScale의 MySQL 데이터베이스는 Vitess 클러스터 위에서 동작합니다. Vitess는 YouTube에서 MySQL을 수평 확장하기 위해 만든 샤딩·연결 프록시 계층으로, 여러 MySQL 인스턴스를 하나의 논리적 데이터베이스처럼 보이게 합니다. 애플리케이션은 VTGate라는 프록시에 접속하고, VTGate가 쿼리를 적절한 MySQL 인스턴스로 라우팅합니다.
애플리케이션은 MySQL 프로토콜과 익숙한 SQL을 쓰므로 mysql2, Prisma, Drizzle을 그대로 쓸 수 있습니다. 다만 Vitess를 거치기 때문에 일부 MySQL 기능(특정 저장 프로시저 사용 방식, 일부 세션 변수 등)은 지원되지 않거나 동작이 다를 수 있고, 운영 규약도 단일 인스턴스 MySQL과 다릅니다. 대표적으로 프로덕션 스키마는 직접 ALTER TABLE로 바꾸지 않고 브랜치에서 검증한 뒤 Deploy Request로 배포합니다.
”서버리스 MySQL”이라는 표현
PlanetScale이 쿼리마다 인스턴스가 새로 뜨는 형태의 서버리스는 아닙니다. 대신 동시 연결 수가 급변하는 서버리스 함수와 잘 맞도록 VTGate가 대량의 클라이언트 연결을 받아 내고, TCP 소켓을 쓰기 어려운 엣지 런타임을 위해 HTTP 기반 드라이버(@planetscale/database)를 제공합니다.
그래서 연결 방식은 런타임에 따라 고릅니다. Cloudflare Workers처럼 TCP가 제한적이거나 요청마다 새 인스턴스가 뜨는 환경에서는 HTTP 드라이버가 맞고, 오래 실행되는 Node.js 서버에서는 mysql2와 커넥션 풀이 여전히 효율적입니다.
스키마 배포와 애플리케이션 배포의 분리
일반 MySQL에서 큰 테이블에 ALTER TABLE을 실행하면 테이블 재구축과 메타데이터 잠금 때문에 쓰기가 막힐 수 있습니다. PlanetScale은 Deploy Request를 배포할 때 Vitess의 온라인 스키마 변경을 씁니다. 새 스키마로 섀도 테이블을 만들고, 기존 데이터를 복사하면서 진행 중인 변경을 따라잡은 뒤 마지막에 테이블을 교체하는 방식이라 배포 중에도 읽기·쓰기가 계속됩니다.
이 구조 때문에 애플리케이션 배포(컨테이너·함수)와 스키마 배포(Deploy Request)는 별도 절차가 됩니다. 두 배포의 순서를 어떻게 맞출지 팀에서 초기에 합의해 두어야 합니다.
브랜칭과 Deploy Request
데이터베이스 브랜치
프로덕션 브랜치는 실제 서비스 트래픽이 붙는 데이터베이스입니다. 개발 브랜치는 프로덕션 브랜치의 스키마를 복사해 만든 독립된 데이터베이스로, 여기서 스키마 변경을 자유롭게 실험합니다. 개발 브랜치에는 기본적으로 데이터가 복사되지 않으므로 시드 데이터를 따로 넣어야 하고, 프로덕션 데이터가 필요한 경우에는 백업에서 브랜치를 만드는 기능을 씁니다(개인정보가 함께 복사된다는 점을 감안해야 합니다).
프로덕션 브랜치에 안전 마이그레이션(safe migrations)을 켜면 그 브랜치에서는 직접 DDL 실행이 거부되고 Deploy Request로만 스키마를 바꿀 수 있습니다. Git의 보호된 main 브랜치와 같은 역할입니다.
Deploy Request 워크플로
먼저 개발 브랜치에서 마이그레이션 SQL이나 ORM으로 스키마를 변경하고, 애플리케이션 코드와 함께 호환성을 검증합니다. 그다음 Deploy Request를 열면 PlanetScale이 두 브랜치의 스키마 차이를 계산해 보여 주고, 충돌 여부를 검사합니다. 리뷰와 승인이 끝나면 프로덕션에 배포되며, 이때 앞서 설명한 온라인 스키마 변경이 실행됩니다.
배포 직후 일정 시간(현재 문서 기준 30분) 동안은 스키마 변경을 되돌릴 수 있습니다. 되돌리는 동안에도 배포 이후 기록된 데이터는 유지되므로, 일반 MySQL에서 롤백 DDL을 급히 작성하는 것보다 위험이 훨씬 작습니다. 다만 애플리케이션이 이미 새 컬럼에 의존하고 있다면 스키마만 되돌려서는 해결되지 않으므로, 아래의 확장/축소 패턴과 함께 써야 합니다.
CLI(pscale)와 자동화
# 개발 브랜치 생성
pscale branch create my-db add-user-nickname
# 로컬에서 브랜치에 접속 (인증된 로컬 프록시를 띄움)
pscale connect my-db add-user-nickname --port 3309
# 스키마 변경 후 Deploy Request 생성
pscale deploy-request create my-db add-user-nickname
CI에서 브랜치 생성과 Deploy Request 생성을 자동화하는 팀이 많습니다. 이때 서비스 토큰의 권한 범위를 최소화하고, 프로덕션 배포 단계에는 사람의 승인을 두는 것이 안전합니다. 스키마 변경도 하나의 릴리스로 취급해 변경 이력과 담당자를 남기면 장애 대응이 쉬워집니다.
연결 방식
연결 문자열
콘솔에서 발급하는 연결 정보에는 호스트, 사용자, 비밀번호, 데이터베이스 이름이 들어 있고 TLS 연결이 필수입니다. 비밀번호는 브랜치별로 발급되며 읽기 전용·읽기 쓰기 같은 역할을 지정할 수 있습니다.
자주 생기는 문제는 세 가지입니다. 프로덕션 비밀번호를 로컬 .env에 복사해 두었다가 테스트 데이터를 프로덕션에 넣는 사고, 서버리스 함수의 동시 실행 수만큼 연결이 늘어나 한도에 걸리는 문제, 애플리케이션과 데이터베이스 리전이 멀어 쿼리마다 왕복 지연이 쌓이는 문제입니다. 마지막 문제는 쿼리 수가 많은 페이지일수록 크게 드러나므로, 데이터베이스 리전은 애플리케이션 서버와 같은 지역으로 고릅니다.
서버리스 드라이버: @planetscale/database
import { connect } from '@planetscale/database';
const conn = connect({
url: process.env.DATABASE_URL,
});
export async function listPosts() {
const result = await conn.execute(
'SELECT id, title FROM posts WHERE published = ? LIMIT ?',
[true, 10],
);
return result.rows;
}
execute의 두 번째 인자로 값을 넘기면 드라이버가 안전하게 이스케이프하므로 SQL 인젝션을 막을 수 있습니다. HTTP 드라이버는 쿼리 하나가 HTTP 요청 하나이므로, N+1 쿼리 패턴이 있으면 왕복 지연이 그대로 곱해집니다. 여러 쿼리를 원자적으로 실행해야 한다면 conn.transaction(async (tx) => { ... })을 씁니다.
장기 실행 서버의 mysql2
오래 실행되는 Node.js 서버에서는 mysql2와 커넥션 풀이 효율적입니다. TLS를 켜야 하고(ssl: { rejectUnauthorized: true }), 풀 크기는 애플리케이션 인스턴스 수를 곱한 총 연결 수가 플랜 한도를 넘지 않게 잡습니다.
Boost에 대해
PlanetScale은 한때 쿼리 결과를 자동으로 갱신되는 캐시에 올려 두는 Boost 기능을 베타로 공개했지만, 이후 제공이 중단되었습니다. 오래된 글에서 Boost 설정 방법을 보더라도 현재 제품에서는 쓸 수 없으므로, 반복 읽기 성능은 인덱스와 쿼리 개선, 애플리케이션 캐시(Redis 등)로 해결해야 합니다.
스키마 변경 전략
한 번에 ALTER하면 위험한 이유
Vitess가 온라인 스키마 변경으로 테이블 잠금을 피해 주더라도, 스키마와 코드 배포 시점이 어긋나는 문제는 남습니다. 새 코드가 아직 없는 컬럼을 읽으면 실패하고, NOT NULL 제약을 추가하는 순간 그 컬럼을 모르는 옛 코드의 INSERT가 실패합니다. 인덱스를 추가하거나 지우면 쿼리 실행 계획이 바뀌어 예상하지 못한 지연이 생길 수도 있습니다.
확장/축소(Expand/Contract) 패턴
가장 보편적인 안전 패턴은 네 단계입니다. 먼저 새 컬럼·테이블·인덱스를 기존 코드와 호환되게(NULL 허용이나 기본값을 두고) 추가합니다. 다음으로 백필 작업으로 기존 행의 데이터를 채우는데, 대용량이라면 배치 크기를 작게 나누고 사이에 쉬는 시간을 둬서 복제 지연과 부하를 조절합니다. 그다음 애플리케이션이 새 스키마를 읽고 쓰도록 배포하고, 마지막으로 더 이상 쓰지 않는 컬럼과 인덱스를 제거합니다.
컬럼 이름을 바꾸는 경우라면 새 컬럼 추가, 양쪽 컬럼에 동시에 쓰기(dual write), 백필, 읽기를 새 컬럼으로 전환, 옛 컬럼 쓰기 중단, 옛 컬럼 제거 순서가 됩니다. 각 단계를 별도 Deploy Request로 나누면 어느 단계에서 문제가 생겼는지 추적하기 쉽고, 30분 되돌리기도 단계별로 쓸 수 있습니다.
인덱스
성능 문제의 상당수는 인덱스가 없거나 너무 많아서 생깁니다. WHERE·JOIN·ORDER BY에 함께 쓰이는 컬럼은 순서를 고려해 복합 인덱스로 만들고, 성별처럼 값의 종류가 적은 컬럼 하나만으로 만든 인덱스는 거의 도움이 되지 않는다는 점을 기억해 두세요. 인덱스는 읽기를 빠르게 하는 대신 쓰기마다 갱신 비용이 들므로, 쓰지 않는 인덱스는 지우는 것이 좋습니다. PlanetScale의 Insights 화면은 쿼리 패턴별 실행 시간과 읽은 행 수를 보여 주므로, 인덱스가 필요한 쿼리를 찾는 데 유용합니다.
Prisma 통합
마이그레이션 흐름
Prisma는 MySQL 프로바이더로 PlanetScale에 연결합니다. 이때 prisma migrate deploy로 프로덕션 스키마를 직접 바꾸는 흐름은 Deploy Request 워크플로와 맞지 않습니다. PlanetScale과 Prisma가 안내하는 방식은 개발 브랜치에 prisma db push로 스키마를 반영하고, 그 브랜치로 Deploy Request를 만들어 프로덕션에 배포하는 것입니다. 마이그레이션 이력은 Prisma의 _prisma_migrations 테이블이 아니라 PlanetScale의 Deploy Request 기록이 대신합니다.
외래 키와 relationMode
Vitess는 오랫동안 외래 키 제약을 지원하지 않았기 때문에, Prisma에서는 데이터베이스 대신 Prisma 클라이언트가 참조 무결성을 흉내 내는 relationMode = "prisma"를 써야 했습니다.
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model Post {
id Int @id @default(autoincrement())
authorId Int
author User @relation(fields: [authorId], references: [id])
@@index([authorId]) // relationMode = "prisma"에서는 외래 키 인덱스가 자동 생성되지 않음
}
relationMode = "prisma"에서는 데이터베이스가 외래 키 인덱스를 자동으로 만들어 주지 않으므로, 관계 필드에 @@index를 직접 달지 않으면 조인과 관계 조회가 풀스캔이 됩니다. 또 Prisma를 거치지 않은 쓰기(다른 서비스, 수동 SQL)에는 참조 무결성이 적용되지 않습니다.
현재 PlanetScale은 데이터베이스 설정에서 외래 키 제약 지원을 켤 수 있습니다. 샤딩하지 않은 데이터베이스라면 이 설정을 켜고 Prisma의 기본 relationMode(foreignKeys)를 쓰는 것이 더 단순합니다. 샤딩된 키스페이스에서는 외래 키에 제한이 있으므로 문서를 확인해야 합니다.
서버리스에서 Prisma 쓰기
Prisma는 기본적으로 TCP로 접속하므로 엣지 런타임에서는 쓸 수 없습니다. 이때는 Prisma의 드라이버 어댑터 기능으로 @prisma/adapter-planetscale과 @planetscale/database를 연결하면 HTTP 드라이버 위에서 Prisma 클라이언트를 쓸 수 있습니다. 어댑터 설정 방법은 Prisma 버전에 따라 바뀌어 왔으므로 설치한 버전의 문서를 기준으로 하세요.
Drizzle 통합
import { drizzle } from 'drizzle-orm/planetscale-serverless';
import { connect } from '@planetscale/database';
import * as schema from './schema';
const client = connect({ url: process.env.DATABASE_URL });
export const db = drizzle(client, { schema });
Drizzle은 스키마를 TypeScript로 정의하고 SQL에 가까운 쿼리 빌더를 제공해, HTTP 드라이버 환경에서 쿼리가 몇 번 실행되는지 예측하기 쉽습니다. 관계 조회(db.query.posts.findMany({ with: { author: true } }))도 단일 SQL로 변환되므로 N+1을 피하기 좋습니다. 오래 실행되는 서버라면 drizzle-orm/mysql2 드라이버와 커넥션 풀을 쓰고, 이때는 풀 크기·타임아웃·TLS 설정이 핵심입니다.
스키마 변경은 drizzle-kit push로 개발 브랜치에 반영한 뒤 Deploy Request로 배포하면 Prisma와 같은 흐름이 됩니다.
성능 최적화
PlanetScale로 옮겨도 성능을 좌우하는 요인은 대부분 그대로입니다. 애플리케이션과 DB 사이 왕복 횟수(N+1 쿼리), 필요 없는 열까지 가져오는 SELECT *, 적절한 인덱스 없이 큰 테이블을 정렬하거나 조인하는 쿼리가 대표적입니다. 성능 작업은 측정에서 시작해야 하므로, APM의 요청 추적과 PlanetScale Insights의 쿼리 통계, 애플리케이션 로그의 느린 쿼리를 같은 시간대로 맞춰 보는 것이 좋습니다.
서버리스 환경에서는 동시 실행 수와 요청당 쿼리 수의 곱이 곧 연결 수와 비용 압력이 됩니다. 자주 읽지만 거의 바뀌지 않는 데이터는 애플리케이션 캐시나 CDN으로 옮기고, 여러 건의 쓰기는 배치로 묶어 쿼리 수를 줄입니다. 글로벌 서비스라면 사용자, 애플리케이션, 데이터베이스 사이의 거리를 함께 줄여야 하는데, 읽기 전용 리전 복제본 같은 옵션은 플랜에 따라 다르므로 성능 목표를 먼저 수치로 정한 뒤 문서와 대조해 고르는 편이 좋습니다.
가격과 제한
관리형 DB 비용은 월 고정료뿐 아니라 저장 용량, 인스턴스 크기, 추가 기능(복제본, 지원 수준)이 합쳐진 값입니다. PlanetScale은 행 읽기·쓰기 기반 과금에서 인스턴스 크기 기반 과금으로 바뀌는 등 요금 구조가 여러 번 바뀌었으므로, 이 글에서는 숫자를 고정해 적지 않습니다.
도입 전에는 PoC로 검증해야 할 항목이 있습니다. 애플리케이션이 Vitess에서 지원하지 않는 MySQL 기능에 의존하지 않는지, 필요한 트랜잭션 격리 수준과 외래 키 동작이 맞는지, 대용량 백필이나 오래 걸리는 DDL을 어떤 절차로 운영할지, 데이터 상주 지역 같은 규제 요건을 만족하는지 확인합니다.