UUID는 대부분의 애플리케이션에서 기본 고유 식별자입니다. 하지만 UUID v4는 — 무작위 설계상 — 순서가 없습니다. 데이터베이스에 삽입된 행은 삽입 순서대로 저장되지 않습니다. 인덱스가 단편화됩니다. 쿼리가 느려집니다. ULID는 바로 이 문제를 해결하기 위해 설계되었습니다.

ULID 생성하기 →

ULID란 무엇인가

ULID는 Universally Unique Lexicographically Sortable Identifier (범용 고유 사전순 정렬 가능 식별자)의 약자입니다. 2016년 Alizain Feerasta가 만들었으며 공개 사양으로 정의되어 있습니다.

ULID는 이렇게 생겼습니다:

01HQ7V5J3Z2QK8MNRPXTYW9E4B

26자, 대문자, Crockford의 Base32 알파벳 사용 (시각적 혼동을 피하기 위해 I, L, O, U 제외).

구조

 01HQ7V5J3Z  2QK8MNRPXTYW9E4B
|-----------|-----------------|
  타임스탬프      무작위성
  48비트          80비트
  • 타임스탬프 (48비트): Unix 에포크로부터의 밀리초. 서기 10895년까지 유효.
  • 무작위성 (80비트): 암호학적으로 무작위. 밀리초당 약 1.2×10²⁴개의 가능한 값.

ULID vs UUID: 주요 차이점

기능UUID v4ULID
길이36자 (하이픈 포함)26자
정렬 가능아니오예 (사전순)
타임스탬프 내장아니오
충돌 확률매우 낮음매우 낮음
URL 안전아니오 (하이픈 포함)
사양RFC 4122ULID 사양

핵심 차이점: ULID는 생성 시간 순으로 정렬됩니다. ULID 키 테이블에 1000행을 삽입하면 기본 키 인덱스가 순차적으로 유지됩니다. 이것이 중요한 경우:

  • B-트리 인덱스: 순차적 삽입이 같은 페이지에 적중하여 단편화 감소.
  • 페이지네이션: 별도의 created_at 컬럼 없이 효율적인 커서로 WHERE id > :cursor 사용 가능.
  • 로그 파일: 파일 이름으로 정렬 = 시간으로 정렬.
  • 메시지 큐: 별도의 시퀀스 번호 없이 자연스러운 순서.

ULID vs UUID v7

UUID v7 (RFC 9562, 2024년 확정)도 처음 48비트에 밀리초 타임스탬프를 내장합니다 — 정렬 가능하게 만들기 위해. ULID의 성공에 대한 직접적인 대응입니다.

기능ULIDUUID v7
형식26자 Base3236자 16진수 (하이픈 포함)
RFC 표준아니오예 (RFC 9562)
DB 네이티브 타입TEXT 또는 BINARYUUID 타입 (PostgreSQL)

데이터베이스에 네이티브 UUID 지원이 있다면 (PostgreSQL의 uuid 타입), UUID v7이 더 편리할 수 있습니다. 문자열 기반 시스템에서 작업하거나 더 짧은 ID가 필요하다면 ULID가 여전히 좋은 선택입니다.

같은 밀리초 내 단조성

같은 밀리초 내에 여러 ULID가 생성되면 타임스탬프 부분이 동일합니다. ULID 사양은 단조성 확장을 정의합니다: 같은 밀리초 내의 후속 ULID마다 무작위 컴포넌트를 1씩 증가시킵니다. 이를 통해 고처리량 (초당 수백만 삽입)에서도 순서가 보장됩니다.

코드에서 ULID 생성

JavaScript / TypeScript

npm install ulid
import { ulid } from 'ulid';

const id = ulid();
// "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

// 커스텀 타임스탬프 사용
const id2 = ulid(Date.now());

Go

go get github.com/oklog/ulid/v2
import (
    "math/rand"
    "time"
    "github.com/oklog/ulid/v2"
)

ms := ulid.Timestamp(time.Now())
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
id := ulid.MustNew(ms, entropy)
fmt.Println(id) // "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

Python

pip install python-ulid
from ulid import ULID

id = ULID()
print(str(id))  # "01HQ7V5J3Z2QK8MNRPXTYW9E4B"
print(id.timestamp())  # datetime 객체

데이터베이스에 ULID 저장

PostgreSQL

-- 텍스트로 (26바이트)
CREATE TABLE events (
  id TEXT PRIMARY KEY,
  ...
);

PostgreSQL은 ULID를 네이티브로 이해하지 않지만 TEXT 또는 BYTEA로 저장할 수 있습니다. 많은 팀이 명확성을 위해 TEXT를 사용합니다. 26바이트 비용은 쿼리 단순성과 비교하면 무시할 수 있습니다.

MySQL / SQLite

같은 접근 방식 — CHAR(26) 또는 VARCHAR(26)으로 저장합니다. 고정 너비 문자열이므로 CHAR(26)이 약간 더 효율적입니다.

ULID 디코딩

타임스탬프는 처음 10자에 내장되어 있습니다. 추출할 수 있습니다:

import { decodeTime } from 'ulid';

const ts = decodeTime('01HQ7V5J3Z2QK8MNRPXTYW9E4B');
console.log(new Date(ts)); // 생성 날짜시간

이는 디버깅에 유용합니다: created_at을 쿼리하지 않고 ID만으로 레코드가 언제 생성되었는지 정확히 알 수 있습니다.

ULID를 언제 사용해야 하는가

  • 높은 삽입률 테이블에서 인덱스 단편화가 우려되는 경우
  • 이벤트 로그, 감사 추적, 또는 추가가 많은 워크로드
  • 별도의 타임스탬프 컬럼 없이 시간 순서 ID가 필요한 시스템
  • UUID보다 짧고 URL 안전하며 사람이 읽을 수 있는 ID가 필요한 API

지금 ULID 생성하기 →

관련 도구