데이터베이스 ID 설계: ID 방법 선택 및 Primary Key 전략

발행: (2025년 12월 7일 오전 11:53 GMT+9)
7 min read
원문: Dev.to

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 v436자×Native표준, 무작위
UUID v736자UUID 타입으로 저장시간 정렬 가능
ULID26자text 타입읽기 쉬운 문자 집합
CUID224자 이상×text 타입짧고 안전함
NanoID21자 이상×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 방식이유
콘텐츠 IDCUID2URL에 사용, 짧음을 우선
테이블 콘텐츠 행 IDUUID v7대량 처리, 성능 우선
사용자 IDBetter 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_idcontent_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: 데이터베이스 마이그레이션 모범 사례 – 안전하게 변경 적용하기
Back to Blog

관련 글

더 보기 »

PostgreSQL 로그 보기

Forem 로고https://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%...

PostgreSQL MERGE INTO

SQL UPDATE PSMT_INVOICE_M SET SHIPPING_COUNTRY_ID = SRC.COUNTRY_ID, SHIPPING_CITY_ID = SRC.CITY_ID, SHIPPING_TOWN_ID = SRC.TOWN_ID FROM SELECT PM.INVOICE_M_ID, ...

PostgreSQL의 잠금

번역할 텍스트를 제공해 주시겠어요? 텍스트를 주시면 한국어로 번역해 드리겠습니다.