Alert

이 글은 Claude Code의 도움을 받아 작성되었습니다

TL;DR

  • GUID/UUID는 분산 시스템에서 고유 ID를 만들기 위한 표준이지만, RDBMS에 강한 건 아님
  • 랜덤 UUID를 PK로 무분별하게 사용하면서 인덱스 파편화와 성능 저하라는 업계 전반의 삽질이 발생
  • 업계는 프리픽스 붙이기, ASCII art, 비트 패킹 등 다양한 꼼수를 시도했지만 근본적 해결은 아니었음
  • 현재는 UUIDv7이 대안으로 부상 중이며, RDBMS 중심이라면 규칙 기반 정수 조합이 더 합리적일 수 있음

Source


1. ID가 왜 필요한가

데이터베이스에 저장된 데이터를 구분하려면 각 행(row)마다 고유한 식별자가 필요하다. 이것이 Primary Key(PK)다.

가장 단순한 방식은 auto-increment 정수다. 새 행이 추가될 때마다 1, 2, 3, 4… 순서대로 번호를 매긴다.

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);
-- INSERT 할 때마다 id가 1, 2, 3... 자동 증가

이 방식은 단순하고 빠르지만, 한 가지 근본적인 제약이 있다.

auto-increment의 한계

번호를 매기는 “카운터”가 DB 서버 한 대에 있다.
서버가 여러 대면 누가 몇 번을 쓸지 조율해야 하고, 이 조율 자체가 병목이 된다.
또한 id=42 같은 값은 외부에서 쉽게 추측할 수 있어 보안상 취약하다.


2. UUID란

UUID(Universally Unique Identifier)는 이 문제를 해결하기 위해 만들어진 식별자 표준이다.
GUID(Globally Unique Identifier)는 Microsoft가 부르는 이름이고, 실질적으로 같은 것이다.

핵심 아이디어는 간단하다. 중앙 서버 없이도 어디서든 만들 수 있고, 그래도 겹치지 않는 ID.

550e8400-e29b-41d4-a716-446655440000
  • 128비트(16바이트) 크기
  • 하이픈으로 구분된 32개의 16진수 문자열
  • 이론상 만들 수 있는 조합: 약 3.4 x 10^38

어느 서버, 어느 시점에 생성해도 사실상 겹칠 일이 없다. 이 성질 덕분에 분산 시스템에서 널리 채택되었다.


3. UUID version별 차이

UUID라고 다 같지 않다. 버전에 따라 생성 방식이 완전히 다르다.

v1 - timestamp + MAC address

|  타임스탬프(60비트)  |  클럭시퀀스(14비트)  |  MAC주소(48비트)  |

생성 시각과 네트워크 카드의 MAC 주소를 조합한다. 시간순 정렬이 가능하지만, MAC 주소가 그대로 노출되는 보안 문제가 있다.

v4 - random

|              122비트 랜덤              |  버전(4비트)  |  변형(2비트)  |

거의 전체를 랜덤 값으로 채운다. 가장 널리 사용되는 버전이다. 단순하고 안전하지만 시간순 정렬이 불가능하다.

v7 - timestamp + random (2024년 표준)

|  Unix타임스탬프(48비트)  |  랜덤(74비트)  |  버전+변형(6비트)  |

앞부분에 타임스탬프, 뒷부분에 랜덤 값을 넣었다. v4의 안전성과 v1의 정렬 가능성을 모두 갖춘 최신 표준(RFC 9562, 2024)이다.

버전생성 방식정렬 가능보안주 용도
v1타임스탬프 + MAC 주소OX사용하지 않는 것을 권장
v4완전 랜덤XO보안 토큰, API 키
v5SHA-1 해시 기반XO동일 입력 → 동일 UUID 필요 시
v7타임스탬프 + 랜덤OODB PK (현재 권장)

4. 업계가 UUID를 채택한 이유

auto-increment의 한계가 분산 시스템 시대에 점점 뚜렷해지면서, UUID가 표준 해법으로 자리잡았다.

  • 분산 시스템: 서버 A, B, C가 각자 ID를 만들어도 겹치지 않는다
  • 보안: /api/users/3 같은 예측 가능한 ID 대신 추측 불가능한 값을 사용
  • DB 병합: 서로 다른 DB를 합칠 때 ID 충돌이 발생하지 않는다
  • 클라이언트 ID 선생성: 서버에 요청하기 전에 프론트엔드에서 미리 ID를 만들 수 있다

한때 NoSQL이 핫하던 시절, “분산환경에선 GUID가 답이다”, “auto-increment는 구식이다”라는 분위기가 퍼지면서 세상 모든 ID에 GUID를 쓰기 시작했다. 유저 ID도 GUID, 주문 ID도 GUID, 상품 ID도 GUID, 로그 ID도 GUID. 문제는 GUID가 분산에 강하지 RDBMS에 강한 건 아니라는 점이었다.


5. 문제 1 - DB Primary Key 성능 붕괴

UUID v4를 PK로 사용하면 데이터베이스 내부에서 심각한 문제가 생긴다.

B-tree와 Page Split

DB는 데이터를 빠르게 찾기 위해 B-tree라는 자료구조로 인덱스를 관리한다. 이 트리는 정렬된 순서로 데이터를 보관한다.

auto-increment ID(1, 2, 3…)는 항상 트리의 맨 끝에 추가된다. 깔끔하고 빠르다.

순차 키: [1, 2, 3, 4, 5] → [6] 항상 끝에 추가
랜덤 UUID: [a1, f3, b2, e7, c4] → [d5] 중간에 끼워넣기

랜덤 UUID는 트리 중간 어딘가에 삽입된다. 이때 해당 페이지가 꽉 차 있으면 페이지를 둘로 쪼개야 한다(Page Split). 이 작업이 매 INSERT마다 반복되면서 성능이 급격히 떨어진다.

  • 순차 키의 페이지 활용률: 약 94%
  • 랜덤 UUID의 페이지 활용률: 약 50% (절반이 빈 공간)

Storage overhead

ID 하나당 차지하는 공간 차이도 크다.

타입크기10억 행 기준 ID만
INT auto-increment4바이트~3.7GB
BIGINT auto-increment8바이트~7.5GB
BINARY(16) UUID16바이트~14.9GB
CHAR(36) UUID (문자열)36바이트~33.5GB

MySQL/InnoDB 특수 이슈

InnoDB는 PK를 클러스터드 인덱스로 사용한다. 즉, PK 순서가 곧 디스크에 저장되는 물리적 순서다. 랜덤 UUID PK는 데이터 자체의 물리적 배치를 망가뜨린다. 실제 사례로, 10억 행 테이블에서 UUID PK를 순차 키로 재구성하니 약 1TB에서 450GB로 줄었다는 보고가 있다.


6. 문제 2 - UUID v1 보안 취약점

UUID v1에는 생성 머신의 MAC 주소가 그대로 포함된다.

  • MAC 주소 노출: UUID 문자열을 파싱하면 어떤 기기에서 언제 만들었는지 알 수 있다
  • Sandwich Attack: 비밀번호 재설정 토큰에 v1을 사용하면, 공격자가 시간대 근처의 UUID를 관찰하여 다른 사용자의 토큰을 예측할 수 있다
  • 추적 가능성: MAC 주소로 동일 클라이언트를 추적할 수 있다

UUID v1은 사용하지 않는다

보안 토큰, API 키, 세션 ID 등 외부 노출되는 곳에 v1을 쓰면 안 된다.


7. 문제 3 - 정렬 불가와 충돌에 대한 오해

Ordering

UUID v4는 완전 랜덤이므로 생성 시간 순으로 정렬할 수 없다. ORDER BY id로 최신 데이터를 가져오는 패턴이 불가능하고, created_at 같은 별도 컬럼과 인덱스가 필요하다.

Collision

UUID v4에서 50% 확률로 충돌이 발생하려면 약 2.71 x 10^18(2.71경)개를 생성해야 한다. 초당 10억 개를 생성해도 약 86년이 걸린다.

실제로 “충돌”로 보고된 사례는 대부분 시스템 설정 오류나 잘못된 난수 생성기(PRNG) 사용이 원인이었다. UUID 자체의 결함이 아니다.


8. 업계의 꼼수들

GUID의 가독성과 성능 문제를 해결하려고 업계에서 다양한 시도가 나왔다. 하지만 대부분 근본적인 해결이 아니라 트레이드오프를 바꾼 것에 가깝다.

꼼수 1: 프리픽스 붙이기

GUID 앞에 대상을 나타내는 문자열을 붙인다.

ord-550e8400-e29b-41d4-a716-446655440000   (주문)
user-6ba7b810-9dad-11d1-80b4-00c04fd430c8  (사용자)

눈으로 보면 뭔지 바로 알 수 있다. 하지만 엄밀히 말하면 이건 GUID가 아니라 타입 + GUID를 문자열로 합친 것이다. DB에서 UUID 타입을 쓸 수 없고, 문자열로 저장해야 해서 저장 공간과 인덱스 효율이 떨어진다.

꼼수 2: ASCII art 방식

“order”라는 문자열을 비슷하게 보이는 16진수로 바꿔서 GUID 앞부분에 심는다. 로그에서 hex 문자열을 보면 어떤 종류의 ID인지 드러난다. 하지만 이미 랜덤 GUID가 아니다. 표현 가능한 공간을 줄이고 구조를 심어 넣은 것이므로 사실상 구조화된 ID다.

꼼수 3: 비트 패킹

GUID의 상위 몇 바이트에 타입, 그 다음에 날짜, 나머지에 시퀀스를 넣는 방식이다. 128비트 공간이 크니까 꽤 많은 데이터를 넣을 수 있다. 하지만 이건 그냥 비트 패킹이지 GUID가 아니다. 내부 규칙으로 정의된 자산에만 적용 가능하고, 유저 인터랙션으로 생기는 무수한 데이터에는 결국 랜덤 GUID로 가야 한다.


9. 대안: 규칙 기반 정수 조합

영상에서 제시하는 실용적 대안은 GUID 대신 10진수 자리 규칙으로 정수를 조합하는 방식이다.

| 타입(2자리) | 날짜(6자리) | 시퀀스(4자리) |
| 01         | 260504     | 0001       |
→ 012605040001
  • 16비트, 32비트, 64비트 정수 안에 충분히 들어가는 경우가 많다
  • GUID보다 훨씬 작고, 인덱스 효율도 좋고, 정렬도 안정적이다
  • 로그에서 읽을 수 있다 (hex 감성은 없지만)
  • DB 설계가 단순해진다: 타입 컬럼 따로 안 둬도 되고, 인덱스 하나로 필터링 가능
  • 일부는 DB lookup 자체를 안 해도 된다 — 코드에서 계산으로 풀 수 있다

GUID로는 거의 못 하는 접근

규칙 기반 정수는 ID 자체에 의미가 담겨 있어서, ID만 보고 타입/날짜를 추출할 수 있다. 이건 순수 랜덤인 GUID로는 불가능한 장점이다. 다만 분산 환경에서 충돌 방지를 위한 별도 설계(머신ID 포함 등)가 필요하다.


10. 현대적 대안

“정렬 가능한 고유 ID”를 만들기 위한 다양한 방식이 등장했다.

방식크기핵심 아이디어적합한 상황
UUIDv7128비트타임스탬프 앞배치 + 랜덤DB PK (가장 범용적)
ULID128비트타임스탬프 + 랜덤, Base32문자열 정렬이 중요한 경우
Snowflake ID64비트타임스탬프 + 머신ID + 시퀀스초고처리량 분산 시스템
NanoID가변커스텀 알파벳, 크기 조절URL용 짧은 ID
KSUID160비트타임스탬프 + 랜덤시간 정렬 + 높은 랜덤성

UUIDv7이 주류로 부상한 이유

기존 UUID 타입(uuid 컬럼)을 그대로 쓸 수 있어 마이그레이션 부담이 적다. 시간순 정렬이 가능해 B-tree 인덱스 끝에 순차 삽입된다. RFC 9562로 2024년에 공식 표준화되어 언어/프레임워크 지원이 빠르게 확산 중이다.


11. Best Practices

UUID를 써야 하는 경우:

  • 분산 시스템에서 중앙 조율 없이 ID 생성이 필요할 때
  • 마이크로서비스 간 데이터 공유/병합이 필요할 때
  • 외부 API에 노출되는 ID (예측 불가능해야 할 때)

UUID를 쓰지 않아도 되는 경우:

  • 단일 DB, 소규모 시스템에서는 auto-increment 정수가 더 효율적
  • 64비트로 충분하면 Snowflake ID가 저장 효율 2배

쓴다면 이렇게:

  • DB PK에는 UUIDv7 (시간순 정렬, 인덱스 최적화)
  • 보안 토큰/API 키에는 UUIDv4 (예측 불가)
  • 저장 시 CHAR(36) 대신 BINARY(16) (공간 절약)
  • 하이브리드 전략: 내부 PK는 정수, 외부 노출용 ID만 UUID

기술은 도구다

GUID는 나쁜 기술이 아니다. 분산 시스템에 강하고 충돌 확률 거의 없고 중앙 발급기가 필요 없다. 하지만 “분산이니까 무조건 GUID”는 설계가 아니라 유행이다. 대부분의 경우 auto-increment가 더 단순하고 더 빠르고 더 안정적이었다. 유행은 설계 기준이 아니다.


References