Supabase와 스키마 설계: 파티셔닝 및 정규화

발행: (2025년 12월 6일 오후 04:49 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Day 6 – Schema Design with Supabase

PostgreSQL은 스키마를 지원하는데, 이는 테이블을 그룹화하기 위한 네임스페이스 역할을 합니다.
주요 사용 사례는 다음과 같습니다:

  • 멀티 테넌시 – 고객마다 별도 스키마를 두어 완전한 데이터 격리를 구현.
  • 접근 제어 – 스키마 단위로 권한을 설정.
  • 확장 격리pgvector와 같은 확장을 전용 스키마에 보관.

많은 프로젝트에서는 모든 테이블이 기본 public 스키마에 존재합니다:

-- Default is public schema
SELECT * FROM public.users;

-- Create a separate schema
CREATE SCHEMA app_auth;
SELECT * FROM app_auth.users;

단일 public 스키마의 문제점

  • 테이블 수가 늘어날수록 가시성이 떨어짐.
  • 어떤 테이블이 어느 기능에 속하는지 파악하기 어려워짐.
  • 권한 관리가 복잡해짐.

이를 해결하기 위해 기능별로 스키마를 나누고 app_ 접두사를 추가했습니다.

The app_ Prefix

Supabase는 auth, storage, public 같은 시스템 스키마를 예약해 둡니다.
사용자 정의 스키마에 app_(application의 약자) 접두사를 붙이면:

  • Supabase 시스템 스키마와 명확히 구분됨.
  • pgAdmin 등 도구에서 알파벳 순으로 먼저 나타나 쉽게 찾을 수 있음.

Supabase 시스템 스키마

├── auth        # Supabase authentication
├── storage     # Supabase storage
├── public      # Default

Application schemas

├── app_auth      # Custom authentication
├── app_billing   # Billing
├── app_content   # Content management
├── app_admin     # Admin functions
├── app_ai        # AI features
├── app_social    # Social features
├── app_system    # System logs, etc.

Supabase의 시스템 스키마는 전혀 사용하지 않고 모든 것을 커스텀 스키마에 두어 벤더 락인 위험을 줄입니다. 작은 서비스에서는 스키마가 많아지는 오버헤드가 크게 느껴지지 않을 수도 있지만, SaaS가 성장함에 따라 큰 도움이 됩니다.

Current Schema Overview

SchemaResponsibilityExample Tables
app_adminAdmin functionstenants, teams, members
app_aiAI featuresembeddings, search_vectors
app_authAuthenticationusers, sessions, accounts
app_billingBillingsubscriptions, payment_history
app_contentContent managementcontents, pages, tables
app_socialSocial featuresbookmarks, comments, reactions
app_systemSystem logsactivity_logs, system_logs

Partitioning criteria

  1. Feature cohesion – 관련 테이블을 함께 배치 (app_auth 등).
  2. Change frequency – 자주 변경되는 테이블을 격리.
  3. Permission boundaries – 관리자 전용 데이터(app_admin)와 일반 사용자 데이터(app_content)를 분리.

Defining Schemas with Drizzle ORM

// database/app_auth/schema.ts
import { pgSchema } from 'drizzle-orm/pg-core';

export const appAuth = pgSchema('app_auth');
// database/app_auth/users.ts
import { appAuth } from './schema';
import { text, timestamp, uuid } from 'drizzle-orm/pg-core';

export const users = appAuth.table('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: text('name').notNull(),
  email: text('email').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
});

Directory layout matching schemas

src/database/
├── app_auth/
│   ├── schema.ts      # Schema definition
│   ├── users.ts       # Table definition
│   ├── sessions.ts
│   └── index.ts       # Exports
├── app_billing/
│   ├── schema.ts
│   ├── subscriptions.ts
│   └── index.ts
├── index.ts           # Aggregate exports
└── relations.ts       # Relation definitions

Normalization & Denormalization

인디 프로젝트라도 기본 정규화(1NF‑3NF)를 적용하면 좋습니다:

  • 1NF – 반복 그룹을 없애고 CSV 필드 대신 조인 테이블 사용.
  • 2NF – 부분 종속성을 제거; 복합 키의 일부에만 의존하는 컬럼을 분리.
  • 3NF – 이행 종속성을 제거; 파생값은 필요할 때만 저장.

Practical tips

  • Junction tables – 다대다 관계를 처리 (content_tags 등).
  • Composite primary keys – 멀티 테넌트 테이블에 (tenant_id, id) 사용.
  • Denormalization – 읽기 위주 쿼리를 위해 집계 카운트(예: 북마크 수) 저장.

Row‑Level Security (RLS)

RLS는 행 단위 접근 제어를 제공합니다. 현재는 애플리케이션 레이어에서 권한을 검증하고 있지만, 스키마가 파티셔닝돼 있으면 각 스키마마다 별도 정책을 적용하기 쉬워져 향후 RLS 적용이 간편해집니다.

Using Custom Schemas with Supabase

커스텀 스키마를 추가하면 다음 3가지를 설정해야 합니다. 그렇지 않으면 “table not found” 오류가 발생합니다.

1. Expose schemas in the Supabase dashboard

Project Settings → Data API → Exposed schemas → 스키마를 추가 (예: public, app_admin, app_ai, …).

2. Include schemas in DATABASE_URL

# .env.local
DATABASE_URL=postgresql://user:password@host:5432/postgres?schema=public,app_admin,app_ai,app_auth,app_billing,app_content,app_social,app_system

3. Configure Drizzle ORM’s schemaFilter

// drizzle.config.ts
import type { Config } from 'drizzle-orm';

export default {
  schema: [
    './src/database/app_admin/index.ts',
    './src/database/app_auth/index.ts',
    './src/database/app_billing/index.ts',
    // …
  ],
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  // Multiple schema support
  schemaFilter: [
    'public',
    'app_admin',
    'app_ai',
    'app_auth',
    'app_billing',
    'app_content',
    'app_social',
    'app_system',
  ],
} satisfies Config;

Takeaways

What works well

  • 기능 기반 스키마를 통한 명확한 책임 분리.
  • 디렉터리 구조가 스키마 이름과 일치해 가독성 향상.
  • 기본 정규화와 선택적 비정규화를 통한 성능 최적화.

Cautions

  • 스키마가 너무 많으면 복잡도가 증가할 수 있음.
  • Supabase 설정에서 커스텀 스키마를 반드시 노출할 것.
  • 작은 프로젝트라도 향후 확장을 고려한 스키마 설계가 도움이 됨.

What’s next

내일 포스트에서는 Database ID Design – UUID, CUID2, 순차 ID 등 선택에 대해 다룰 예정입니다.

  • 12/5 – Git Branch Strategy: A Practical Workflow for Indie Development
  • 12/7 – Database ID Design: Choosing Between UUID, CUID2, Sequential IDs, and More
Back to Blog

관련 글

더 보기 »

플랫폼용 Vercel 소개

오늘 발표된 새로운 제품으로 이제 플랫폼을 구축할 수 있어, 사용자를 대신해 고객 프로젝트를 쉽게 생성하고 실행할 수 있습니다. Vercel for Platform...