데이터베이스 ID 설계: ID 방법 선택 및 Primary Key 전략
Source: Dev.to
ID 설계 시 고려 사항
데이터베이스의 기본 키(ID)를 선택하는 일은 생각보다 깊은 주제입니다. 일부 프레임워크는 기본값을 제공하지만, 대부분 직접 선택해야 합니다. PostgreSQL에서는 자동 증가가 SERIAL 타입(내부적으로 SEQUENCE)으로 구현됩니다.
UUID, CUID2 와 같은 ID 방식은 타임스탬프와 무작위 값을 결합해 중앙 관리 없이 고유 ID를 생성합니다. 이를 통해 분산 시스템에서도 충돌을 걱정하지 않고 데이터를 만들 수 있습니다.
ID 선택 기준을 정리한 도움이 되는 영상:
https://www.youtube.com/watch?v=pmqRaEcDxl4
주요 ID 방식 비교
| 방법 | 길이 | 시간 정렬 가능 | PostgreSQL 저장 방식 | 특성 |
|---|---|---|---|---|
| Sequential (SERIAL/SEQUENCE) | 최대 19자리 | ○ (실질적으로) | Native | 간단하고 예측 가능함 |
| UUID v4 | 36자 | × | Native | 표준, 무작위 |
| UUID v7 | 36자 | ○ | UUID 타입으로 저장 | 시간 정렬 가능 |
| ULID | 26자 | ○ | text 타입 | 읽기 쉬운 문자 집합 |
| CUID2 | 24자 이상 | × | text 타입 | 짧고 안전함 |
| NanoID | 21자 이상 | × | text 타입 | 가장 짧고 빠름 |
선택 기준
- ID가 URL에 노출될까요? → 노출된다면 순차 ID는 피하세요.
- 시간 기반 정렬이 필요합니까? → UUID v7 또는 ULID 사용.
- 쓰기 성능이 중요한가요? → 대용량 데이터셋에는 순차 ID.
- ID 길이가 중요한가요? → URL용으로는 NanoID 또는 CUID2.
내 인디 프로젝트 채택 전략
기본: CUID2
콘텐츠 ID(페이지, 테이블, 대시보드 등)에는 CUID2를 사용합니다.
CUID2 선택 이유:
- 짧음: 24자 (UUID v4의 36자 대비).
- URL‑안전: 하이픈 없이 소문자 알파벳·숫자만 사용.
- 더블‑클릭 선택 가능: 하이픈이 없어 전체 ID를 한 번에 선택 가능.
- 보안: SHA‑3 기반으로 추측이 어려움.
// id-generator.ts
import { init } from '@paralleldrive/cuid2';
// 고정 24자 길이로 초기화
const createCuid = init({ length: 24 });
export function generateContentId(): string {
return createCuid();
}
// 예시: "clhqr8x9z0001abc123def45"
예외: 대량 삽입 테이블은 UUID v7
대량 삽입이 발생할 수 있는 테이블(table_rows 등)에는 UUID v7을 사용합니다.
UUID v7 선택 이유:
- 삽입 성능: 시간 순서대로 정렬된 ID가 B‑tree 인덱스에 효율적.
- PostgreSQL 호환: 네이티브
UUID타입으로 저장 가능. - RFC 표준: RFC 9562(2024년 제정) 준수.
import { v7 as uuidv7 } from 'uuid';
export function generateRowId(): string {
return uuidv7();
}
// 예시: "018c1234-5678-7abc-9def-0123456789ab"
선택 기준 요약
| 사용 사례 | ID 방식 | 이유 |
|---|---|---|
| 콘텐츠 ID | CUID2 | URL에 사용, 짧음을 우선 |
| 테이블 콘텐츠 행 ID | UUID v7 | 대량 처리, 성능 우선 |
| 사용자 ID | Better Auth에서 생성 | 인증 라이브러리에 위임 |
복합 기본 키 설계
특히 다중 테넌트 SaaS 환경에서 데이터 격리와 검색 효율성을 위해 단일 기본 키와 복합 기본 키 중 선택하는 것이 중요한 설계 결정입니다.
단일 vs 복합 기본 키
-- 단일 기본 키
CREATE TABLE contents (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
...
);
-- 복합 기본 키
CREATE TABLE contents (
tenant_id TEXT NOT NULL,
content_id TEXT NOT NULL,
...
PRIMARY KEY (tenant_id, content_id)
);
복합 기본 키의 장점
- 인덱스 효율성: 테넌트 범위 검색이 빠름(
tenant_id가 인덱스 앞에 위치). - 데이터 격리: 테넌트 간 데이터 접근 방지.
- 고유성 보장:
tenant_id와content_id조합을 통해 고유성 확보.
Drizzle ORM으로 정의하기
import { primaryKey, text } from 'drizzle-orm/pg-core';
export const contents = appContent.table(
'contents',
{
tenant_id: text('tenant_id').notNull(),
content_id: text('content_id').notNull(),
title: text('title').notNull(),
// ...
},
table => ({
pk: primaryKey({ columns: [table.tenant_id, table.content_id] }),
})
);
실무 팁
ID 검증 함수 준비하기
검증 함수를 두면 잘못된 ID로 인한 오류를 예방할 수 있습니다.
export function validateCuid2(id: string): void {
const cuid2Regex = /^[a-z0-9]{24}$/;
if (!cuid2Regex.test(id)) {
throw new Error('Invalid CUID2 format');
}
}
export function validateUuidV7(id: string): void {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(id)) {
throw new Error('Invalid UUID v7 format');
}
}
요약
잘 작동하는 점
- CUID2로 짧고 다루기 쉬운 URL.
- UUID v7로 대량 처리 시 성능 향상.
- 복합 기본 키로 다중 테넌트 데이터 격리.
주의할 점
- 최적의 ID는 요구사항에 따라 달라지며 정답은 하나가 아니다.
- 기존 데이터를 마이그레이션할 때는 신중히 계획.
- 인증 라이브러리 등 외부 의존성의 ID 형식과 맞추기.
영상에서 결론처럼, 최적의 ID는 프로젝트 요구사항에 따라 결정됩니다. 상황에 맞는 방식을 선택하세요.
내일은 **“데이터베이스 마이그레이션 모범 사례: 안전하게 변경 적용하기”**에 대해 설명하겠습니다.
이 시리즈의 다른 글
- 12/6: Supabase와 함께하는 스키마 설계 – 테이블 파티셔닝 및 정규화 실전
- 12/8: 데이터베이스 마이그레이션 모범 사례 – 안전하게 변경 적용하기