UUID(Universally Unique Identifier)는 전 세계적으로 고유한 128비트 식별자다. 중앙 서버 없이 누구나, 어디서나, 언제든 만들 수 있고 사실상 겹치지 않는다. 이 글에서는 UUID의 탄생 배경부터 버전별 구조, 언어별 사용법, DB 설계 시 고려사항까지 정리한다.


UUID란

550e8400-e29b-41d4-a716-446655440000
  • 128비트 (16바이트) 값
  • 8-4-4-4-12 형식의 16진수 문자열로 표현
  • 총 경우의 수: 2^128 = 약 3.4 × 10^38개

탄생 배경

문제 — 분산 환경에서 고유 ID를 어떻게 만들 것인가

// 자동 증가 방식 — 중앙 DB가 필요
서버A → DB에 INSERT → seq = 1
서버B → DB에 INSERT → seq = 2  (DB가 순서를 보장)

// DB 없이 ID를 만들고 싶다면?
서버A → ID = ???
서버B → ID = ???  (겹치면 안 된다)

1980년대 Apollo Computer의 Network Computing System(NCS)에서 처음 등장했다. 분산 시스템의 각 노드가 중앙 서버 없이 고유한 식별자를 생성할 필요가 있었고, 이를 위해 UUID 개념이 만들어졌다.

이후 OSF(Open Software Foundation)의 DCE(Distributed Computing Environment)에서 표준화되었고, IETF가 RFC 4122(2005)로 공식 표준을 정했다. 2024년에는 v6, v7, v8을 추가한 RFC 9562가 발표되었다.

타임라인

연도 사건
1980년대 Apollo NCS에서 UUID 개념 등장
1989 OSF DCE에서 UUID 표준화
1997 Microsoft COM/DCOM에서 GUID로 채택
2005 RFC 4122 — UUID v1~v5 표준
2024 RFC 9562 — UUID v6, v7, v8 추가

GUID(Globally Unique Identifier)는 Microsoft가 부르는 이름이다. UUID와 동일한 구조.


UUID 구조

550e8400-e29b-41d4-a716-446655440000
│        │    │ │  │
│        │    │ │  └─ node (48비트)
│        │    │ └─── variant (2비트) + clock_seq (14비트)
│        │    └───── version (4비트)
│        └────────── time_mid (16비트)
└─────────────────── time_low (32비트)

version 필드

4비트로 UUID 버전을 나타낸다. 위치는 세 번째 그룹의 첫 글자.

550e8400-e29b-41d4-a716-446655440000
                   ^
                   4 = v4
xxxxxxxx-xxxx-1xxx = v1
xxxxxxxx-xxxx-3xxx = v3
xxxxxxxx-xxxx-4xxx = v4
xxxxxxxx-xxxx-5xxx = v5
xxxxxxxx-xxxx-7xxx = v7

variant 필드

2비트로 UUID 표준의 종류를 나타낸다. RFC 4122 UUID는 항상 10으로 시작하므로 네 번째 그룹 첫 글자가 8, 9, a, b 중 하나다.

550e8400-e29b-41d4-a716-446655440000
                   ^
                   a → 이진수 1010 → 상위 2비트 = 10 (RFC 4122)

버전별 상세

v1 — 타임스탬프 + MAC 주소

타임스탬프 (60비트) + clock_seq (14비트) + MAC 주소 (48비트)
"언제"                "순번"               "어떤 기기에서"
  • 타임스탬프: 1582년 10월 15일 기준 100나노초 단위
  • 같은 시간 + 같은 기기가 아닌 이상 충돌 불가
  • 단점: MAC 주소 노출 → 보안 이슈
v1 예시: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
                                 ^^^^^^^^^^^^
                                 MAC 주소가 그대로 노출

v2 — DCE Security

v1과 유사하지만 POSIX UID/GID를 포함한다. DCE 보안 용도로 만들어졌으며 실무에서 거의 사용하지 않는다.

v3 — 이름 기반 (MD5)

UUID = MD5(네임스페이스 UUID + 이름)
  • 같은 입력 → 항상 같은 UUID (결정적)
  • 네임스페이스: URL, DNS, OID 등 미리 정의된 UUID 사용
// 같은 URL이면 항상 같은 UUID
UUID.nameUUIDFromBytes("https://example.com".toByteArray())
// → 항상 동일한 값

v4 — 완전 랜덤 (가장 많이 사용)

122비트 랜덤 + 4비트 버전 + 2비트 variant = 128비트
  • 시간, 기기 정보 없이 순수 랜덤
  • Java/Kotlin의 UUID.randomUUID()가 이것
  • 가장 보편적으로 사용

v5 — 이름 기반 (SHA-1)

v3와 동일한 방식이지만 MD5 대신 SHA-1 해시를 사용한다. v3보다 충돌 저항성이 높아 v3 대신 v5 사용이 권장된다.

v6 — 정렬 가능한 v1 (RFC 9562)

v1의 타임스탬프 비트 순서를 재배치해서 시간순 정렬이 가능하게 만들었다.

v1: 시간의 하위비트가 앞에 → 정렬 불가
    6ba7b810-9dad-11d1-...
    ^^^^^^^^
    time_low (시간의 하위 32비트)

v6: 시간의 상위비트가 앞에 → 정렬 가능
    1d19dad6-ba7b-6810-...
    ^^^^^^^^
    time_high (시간의 상위 32비트)

v1과 동일한 정보를 담지만 비트 배치만 바꿨다. 기존 v1 시스템의 마이그레이션용.

v7 — 타임스탬프 + 랜덤 (최신 권장)

Unix 타임스탬프 (48비트) + 랜덤 (74비트)
"밀리초 단위 시간"          "랜덤값"
01970a3f-1234-7abc-8def-567890abcdef
^^^^^^^^ ^^^^
밀리초 타임스탬프 (앞에 위치 → 자연스러운 시간순 정렬)
  • v4의 장점(랜덤, MAC 노출 없음) + 시간순 정렬 가능
  • DB 인덱스 친화적
  • 2024년 이후 신규 프로젝트에서 권장되는 버전

v8 — 커스텀

128비트 중 version/variant 필드를 제외한 나머지를 자유롭게 사용할 수 있다. 사내 규칙에 맞는 커스텀 UUID가 필요할 때 사용.

버전 선택 가이드

상황 권장 버전
대부분의 경우 (2024년 이후) v7
레거시 호환, 단순 랜덤 v4
같은 입력 → 같은 ID v5
v1 시스템 마이그레이션 v6
사내 커스텀 규칙 v8

충돌 확률

v4 기준

122비트 랜덤 → 경우의 수 약 5.3 × 10^36개

생성 속도 50% 충돌까지 걸리는 시간
초당 10억 개 약 100년
초당 1만 개 지구 나이(46억 년)보다 김

비유하면:

  • 로또 1등: 약 1/800만
  • UUID v4 충돌: 1초에 10억 개씩 1년 생성해도 약 10^-18 (로또 1등보다 수십 자릿수 낮음)

그래도 걱정되면

DB에 unique 제약조건을 걸면 된다.

CREATE TABLE orders (
    order_id UUID PRIMARY KEY  -- 충돌 시 INSERT 실패
);

자동 증가 ID vs UUID

-- 자동 증가
CREATE TABLE downtimes (
    downtime_seq SERIAL PRIMARY KEY  -- 1, 2, 3, 4 ...
);

-- UUID
CREATE TABLE faxes (
    fax_id UUID PRIMARY KEY DEFAULT gen_random_uuid()
);
  자동 증가 (INT/BIGINT) UUID
크기 4~8바이트 16바이트
가독성 높음 (1, 2, 3) 낮음
ID 생성 DB에 INSERT 해야 알 수 있음 INSERT 전에 미리 생성 가능
예측 가능 O (보안 이슈 가능) X
분산 환경 충돌 위험 안전
DB 인덱스 순차 삽입, 성능 좋음 v4는 랜덤 삽입으로 느림, v7은 순차적

UUID가 유리한 경우

// INSERT 전에 ID를 알아야 할 때
val faxId = UUID.randomUUID()
queue.send(FaxMessage(faxId, ...))   // 큐에 먼저 보냄
repository.insert(Fax(faxId, ...))   // 나중에 저장

// 분산 환경에서 ID 충돌 방지
// 서버 A, B가 동시에 생성해도 안전
서버A: UUID.randomUUID()  "550e8400-..."
서버B: UUID.randomUUID()  "6ba7b810-..."

자동 증가가 유리한 경우

- 단일 DB, 단일 서버
- ID가 순서 의미를 가질 때 (주문번호 등)
- 저장 공간이 중요할 때
- URL에 노출될 때 (/api/downtimes/1 vs /api/downtimes/550e8400-e29b-41d4-...)

언어별 사용법

Java / Kotlin

import java.util.UUID

// v4 생성 (랜덤)
val id = UUID.randomUUID()
println(id)  // 550e8400-e29b-41d4-a716-446655440000

// v3 생성 (이름 기반, MD5)
val id3 = UUID.nameUUIDFromBytes("hello".toByteArray())

// 문자열 → UUID
val parsed = UUID.fromString("550e8400-e29b-41d4-a716-446655440000")

// UUID → 문자열
val str = id.toString()

// 비교
id == parsed  // 값 비교 (Kotlin)

Java 표준 라이브러리는 v4와 v3만 지원한다. v7을 쓰려면 별도 라이브러리가 필요하다.

// v7 — java-uuid-generator 라이브러리
// implementation("com.fasterxml.uuid:java-uuid-generator:5.1.0")
import com.fasterxml.uuid.Generators

val v7 = Generators.timeBasedEpochGenerator().generate()
// v7 — uuid-creator 라이브러리
// implementation("com.github.f4b6a3:uuid-creator:6.0.0")
import com.github.f4b6a3.uuid.UuidCreator

val v7 = UuidCreator.getTimeOrderedEpoch()

JavaScript / TypeScript

// 브라우저 내장 (v4)
const id = crypto.randomUUID();
console.log(id);  // "550e8400-e29b-41d4-a716-446655440000"

// Node.js 내장 (v4, Node 14.17+)
import { randomUUID } from 'crypto';
const id = randomUUID();
// uuid 라이브러리 — v1, v3, v4, v5 지원
// npm install uuid
import { v1, v4, v5 } from 'uuid';

const id1 = v1();   // 타임스탬프 기반
const id4 = v4();   // 랜덤
const id5 = v5('hello', v5.URL);  // 이름 기반
// uuidv7 라이브러리
// npm install uuidv7
import { uuidv7 } from 'uuidv7';

const id = uuidv7();  // 시간순 정렬 가능

Python

import uuid

# v1 — 타임스탬프 + MAC
id1 = uuid.uuid1()

# v4 — 랜덤
id4 = uuid.uuid4()
print(id4)  # 550e8400-e29b-41d4-a716-446655440000

# v3 — 이름 기반 (MD5)
id3 = uuid.uuid3(uuid.NAMESPACE_URL, "https://example.com")

# v5 — 이름 기반 (SHA-1)
id5 = uuid.uuid5(uuid.NAMESPACE_URL, "https://example.com")

# 문자열 → UUID
parsed = uuid.UUID("550e8400-e29b-41d4-a716-446655440000")
# v7 — uuid_utils 라이브러리
# pip install uuid-utils
from uuid_utils import uuid7

id7 = uuid7()

Go

// google/uuid 라이브러리
// go get github.com/google/uuid
import "github.com/google/uuid"

// v4 — 랜덤
id := uuid.New()
fmt.Println(id)  // 550e8400-e29b-41d4-a716-446655440000

// v7
id7, _ := uuid.NewV7()

// 문자열 → UUID
parsed, err := uuid.Parse("550e8400-e29b-41d4-a716-446655440000")

언어별 기본 지원 정리

언어 기본 지원 버전 v7 사용 시
Java/Kotlin v3, v4 java-uuid-generator, uuid-creator
JavaScript v4 uuidv7
Python v1, v3, v4, v5 uuid-utils
Go v4, v7 (google/uuid) 기본 지원

DB별 UUID 지원

PostgreSQL

-- UUID 타입 네이티브 지원
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid()  -- v4 자동 생성
);

-- 조회
SELECT * FROM orders WHERE id = '550e8400-e29b-41d4-a716-446655440000';

PostgreSQL은 UUID를 16바이트 바이너리로 저장한다. 문자열(36바이트)보다 효율적.

MySQL

-- UUID 함수는 있지만 타입은 없음
CREATE TABLE orders (
    id CHAR(36) PRIMARY KEY  -- 문자열로 저장 (비효율)
);
INSERT INTO orders (id) VALUES (UUID());  -- v1 생성

-- 바이너리로 저장하면 효율적
CREATE TABLE orders (
    id BINARY(16) PRIMARY KEY
);
INSERT INTO orders (id) VALUES (UUID_TO_BIN(UUID(), true));  -- swap_flag로 정렬 가능
SELECT BIN_TO_UUID(id, true) FROM orders;

Oracle

-- RAW(16) 타입 사용
CREATE TABLE orders (
    id RAW(16) DEFAULT SYS_GUID() PRIMARY KEY
);

DB별 정리

DB UUID 타입 자동 생성 저장 방식
PostgreSQL UUID (네이티브) gen_random_uuid() 16바이트 바이너리
MySQL 없음 UUID() CHAR(36) 또는 BINARY(16)
Oracle RAW(16) SYS_GUID() 16바이트 바이너리

UUID v4 vs v7 — DB 인덱스 성능

v4의 문제

v4는 완전 랜덤이라 B-Tree 인덱스에서 삽입 위치가 매번 달라진다.

v4 INSERT 순서:
  "f47ac10b-..."  → B-Tree 끝쪽
  "1a2b3c4d-..."  → B-Tree 앞쪽
  "9e8f7a6b-..."  → B-Tree 중간
  "3c4d5e6f-..."  → B-Tree 앞쪽

→ 페이지 분할 빈번, 캐시 효율 저하

v7의 해결

v7은 타임스탬프가 앞에 있어 시간순으로 증가한다.

v7 INSERT 순서:
  "01970a3f-1000-..."  → B-Tree 끝
  "01970a3f-1001-..."  → B-Tree 끝
  "01970a3f-1002-..."  → B-Tree 끝
  "01970a3f-1003-..."  → B-Tree 끝

→ 순차 삽입, 자동 증가 INT와 유사한 성능
  자동 증가 UUID v4 UUID v7
삽입 패턴 순차 랜덤 순차
인덱스 성능 좋음 나쁨 좋음
분산 생성 불가 가능 가능

v7 = v4의 분산 장점 + 자동 증가의 인덱스 장점


실무에서 UUID를 쓰는 곳

1. 분산 시스템의 PK

// 여러 서버가 동시에 ID 생성
val orderId = UUID.randomUUID()

2. API의 리소스 식별자

// 자동 증가 — 예측 가능 (보안 취약)
GET /api/users/1
GET /api/users/2   ← 다른 사용자 ID를 추측 가능

// UUID — 예측 불가
GET /api/users/550e8400-e29b-41d4-a716-446655440000

3. 파일명

// 업로드된 파일명 충돌 방지co
val filename = "${UUID.randomUUID()}.png"
// "550e8400-e29b-41d4-a716-446655440000.png"

4. 세션/토큰 ID

val sessionId = UUID.randomUUID().toString()
val resetToken = UUID.randomUUID().toString()

5. 이벤트/메시지 ID

// 메시지 큐에서 중복 처리 방지 (멱등성 키)
val event = Event(
    eventId = UUID.randomUUID(),
    type = "ORDER_CREATED",
    payload = orderData
)
messageQueue.send(event)

6. 멀티 테넌트 환경

// 테넌트별 독립 ID 체계
테넌트A DB: UUID 생성
테넌트B DB: UUID 생성
→ 나중에 데이터 병합해도 충돌 없음

MyBatis에서 UUID 사용

MyBatis는 UUID 타입을 기본 지원하지 않는다. TypeHandler가 필요하다.

// TypeHandler — UUID ↔ DB 문자열 변환
@MappedTypes(UUID::class)
@MappedJdbcTypes(JdbcType.OTHER)
class UuidTypeHandler : BaseTypeHandler<UUID>() {

    override fun setNonNullParameter(ps: PreparedStatement, i: Int, parameter: UUID, jdbcType: JdbcType?) {
        ps.setObject(i, parameter)
    }

    override fun getNullableResult(rs: ResultSet, columnName: String): UUID? {
        return rs.getString(columnName)?.let { UUID.fromString(it) }
    }
}
# application.yml — TypeHandler 등록
mybatis:
  type-handlers-package: com.knet.commons.mybatis.typehandler

등록하면 도메인 클래스에서 UUID를 바로 사용할 수 있다.

data class Fax(
    val faxId: UUID,   // TypeHandler가 자동 변환
    val title: String
)

jOOQ + R2DBC에서 UUID 사용

jOOQ는 PostgreSQL의 UUID 타입을 네이티브로 지원한다. 별도 설정 없이 바로 사용 가능.

// jOOQ — 별도 TypeHandler 불필요
dsl.insertInto(FAXES)
    .set(FAXES.FAX_ID, UUID.randomUUID())
    .set(FAXES.TITLE, "test")
    .execute()

val fax = dsl.selectFrom(FAXES)
    .where(FAXES.FAX_ID.eq(faxId))  // UUID 타입 그대로 사용
    .awaitSingleOrNull()

정리

항목 결론
신규 프로젝트 PK UUID v7 권장 (시간순 정렬 + 분산 안전)
단순 랜덤 ID UUID v4 (UUID.randomUUID())
단일 DB + 순서 필요 자동 증가 (SERIAL/BIGSERIAL)
같은 입력 → 같은 ID UUID v5
DB 선택 PostgreSQL이면 UUID 네이티브, MySQL이면 BINARY(16) 권장