UUID는 충돌하지 않는가 - 잘못된 운영과 구현으로 중복을 부르는 패턴
· 小村 豪 · UUID, 식별자, 분산 시스템, 데이터 설계, 구현
UUID를 주 키로 삼고 있었는데, 어느 날 duplicate key가 나온다.
이 순간 꽤 높은 확률로 「UUID는 결국 부딪히는 게 아닌가」 하는 이야기가 됩니다.
다만 실무에서 발생하는 UUID 중복의 대부분은, UUID라는 규격 자체의 문제라기보다 규격이 전제로 하고 있는 생성 조건을 구현이나 운영에서 망가뜨리고 있는 경우입니다. RFC 9562에서는 UUIDv4는 122비트의 랜덤 영역을 가지며, UUIDv7도 타임스탬프 이외의 74비트를 일의성을 위한 난수나 카운터로 쓰는 전제로 정의되어 있습니다. 한편 UUIDv8은 「구현 의존이며, 일의성을 전제로 해서는 안 된다」고 명기되어 있습니다.123
또한 Python 표준 라이브러리에서도 uuid4()는 암호학적으로 안전한 방법으로 생성된다고 설명되어 있으며, 적어도 「제대로 된 구현을 평범하게 쓰는」 한 UUID 쪽의 전제는 꽤 강합니다.4
이 글에서는 잘못된 운영이나 구현으로 UUID가 충돌해 버리는 전형 패턴을 재발 방지책과 세트로 정리합니다.
내용은 2026년 3월 시점에 확인할 수 있는 RFC 9562, Python 공식 문서, PostgreSQL 공식 문서를 바탕으로 합니다.546
1. 먼저 결론
짧게 요약하면 위험한 것은 다음 패턴입니다.
| 패턴 | 무엇이 일어나는가 | 우선 해야 할 대책 |
|---|---|---|
| 고정 seed나 약한 PRNG로 UUIDv4 같은 값을 자작 | 다른 프로세스나 다른 노드에서 같은 계열이 재현됨 | OS / 런타임 표준의 UUID API를 사용 |
| fork, VM snapshot, 컨테이너 복제 후 생성 상태를 그대로 승계 | 난수나 카운터 상태가 되감겨 중복이 발생 | fork 후 재 seed, clone 후 재초기화, 영속 상태 처리 재검토 |
| UUIDv3 / v5를 「매번 새로운 ID」로 오해해서 사용 | 같은 namespace와 same name에서 같은 UUID가 재생성됨 | 결정론적 ID임을 이해하고 용도를 한정 |
| UUIDv1 / v6 / v7 / v8을 자체 구현하고 clock rollback이나 node/counter를 거칠게 취급 | 고빈도 생성이나 다중 노드에서 중복되기 쉬워짐 | 기존 라이브러리를 쓰고, 독자 생성기를 줄임 |
| UUID를 중간에 절단하거나 다른 형식으로 찌그러뜨림 | 원본 128비트의 일의성을 스스로 버림 | 저장·비교는 풀 길이로 수행 |
| DB 쪽에 UNIQUE / PRIMARY KEY를 두지 않음 | 중복이 조용히 섞여 들어 원인 조사가 늦어짐 | 스토리지 층에 일의 제약을 둠 |
요컨대 UUID가 충돌했다기보다 UUID에 기대했던 일의성을 중간 설계에서 깎고 있는 경우가 많습니다.
2. 먼저 의심해야 하는 것은 「UUID의 수학」이 아니라 「생성과 운영」
UUID 이야기가 까다로워지는 이유는 버전마다 성질이 다르기 때문입니다.
- UUIDv4는 랜덤 기반입니다. RFC 9562에서는 version / variant를 제외한 122비트가 난수로 채워집니다.1
- UUIDv7은 시계열 정렬이 쉬운 구조이며, Unix 밀리초 타임스탬프에 더해 나머지를 난수나 carefully seeded counter로 구성합니다.2
- UUIDv3 / v5는 name-based입니다. 같은 namespace와 같은 canonical name이라면 같은 UUID가 나오는 것이 올바른 동작입니다.7
- UUIDv8은 실험용·벤더 독자용이며, 일의성은 구현 의존입니다. RFC는 「일의성을 전제로 해서는 안 된다」고 하고 있습니다.3
즉 「UUID를 쓰고 있다」고 해도, 그 안이
- 표준 라이브러리의
uuid4()인가 - 자체의
timestamp + random인가 uuid5(namespace, name)인가- 겉모습만 UUIDv8인 독자 포맷인가
에 따라 이야기가 완전히 달라집니다.
flowchart TD
A[UUID 중복이 발견됨] --> B{정말 어디서 같은 값이 되었는가}
B --> C[생성기가 약함]
B --> D[상태가 되감김]
B --> E[name-based UUID의 오용]
B --> F[저장 시 절단함]
B --> G[DB 쪽에 일의 제약이 없음]
C --> H[구현 실수]
D --> H
E --> H
F --> H
G --> H
실무에서는 이 그림의 오른쪽부터 보는 편이 빠릅니다.
3. 패턴1: UUIDv4라고 하면서 실제로는 약한 PRNG를 쓰고 있다
가장 흔한 것이 이것입니다.
Math.random()상당의 일반 용도 PRNG로 128비트를 만든다- 기동 시에
time()이나 PID로 seed를 넣는다 - 「UUID 형식 같은 32 hex 자리」를 스스로 조립한다
겉보기는 UUID라도 난수원이 약하면 같은 계열이 다른 프로세스나 다른 노드에서 재현됩니다.
RFC 9562는 UUID의 일의성과 예측 곤란성 양쪽을 위해 CSPRNG을 써야 한다고 합니다. 더욱이 process fork 같은 상태 변화 시에는 CSPRNG 상태를 적절히 재 seed해야 한다고 쓰여 있습니다.8
Python의 uuid.uuid4()도 암호학적으로 안전한 방법으로 랜덤 UUID를 생성한다고 설명하고 있습니다.4
여기서 실무상의 결론은 단순합니다.
- UUID를 자작하지 않는다
- 난수 seed를 손으로 만지지 않는다
- 표준 라이브러리나 널리 쓰이는 구현을 그대로 쓴다
「가벼우니까」 「옛날부터 쓰니까」로 독자 생성기를 계속 갖고 있으면 나중에 가장 비싸게 치릅니다.
4. 패턴2: fork, snapshot, clone으로 생성 상태를 되감는다
두 번째로 위험한 것은 생성기의 상태가 복제·되감김되는 운영입니다.
RFC 9562는 fork 후의 재 seed를 명시적으로 권하고 있으며, stable storage를 갖지 않는 구현은 clock sequence, counter, random data의 생성 빈도가 늘어나 중복 확률이 올라간다고 설명합니다.89
여기서 자연스럽게 나오는 실무상의 추론이 있습니다.
- VM snapshot 취득 후 같은 이미지를 여러 개 복원한다
- 컨테이너 이미지 기동 시 같은 초기 상태에서 독자 생성기가 올라온다
- worker fork 후에 PRNG 상태나 카운터 상태를 공유해 버린다
이런 운영에서는 UUID의 생성 계열이 의도치 않게 재현될 수 있습니다.
이는 RFC가 그대로 「snapshot은 위험」이라고 쓴 것은 아니지만, fork 후의 재 seed와 generator state 처리에 관한 주의에서 도출할 수 있는, 꽤 실무적인 주의점입니다.89
대책은 다음과 같습니다.
- 독자의 UUID 생성 상태를 오래 보유하지 않는다
- fork / clone / restore 직후에 재초기화한다
- 가능하면 OS 유래의 난수를 매번 이용하는 구현으로 치우친다
- 고빈도 생성기라면 상태 관리와 재 seed의 사양을 명문화한다
5. 패턴3: UUIDv3 / v5를 「매번 새로운 ID」로 오해한다
UUIDv3 / v5는 collision하기 어려운 랜덤 ID가 아닙니다.
같은 이름에서 같은 ID를 재생성할 수 있는 결정론적 ID입니다.
RFC 9562에서는 같은 canonical format의 same name을 same namespace에서 생성한 UUID는 같아야 한다고 쓰여 있습니다.7
즉 다음과 같은 사용 방식을 하면 중복은 사고가 아니라 사양대로입니다.
uuid5(NAMESPACE_URL, "https://example.com/users/42")을 매번 「신규 채번」으로 사용- tenant를 namespace에 넣지 않고, 전체 고객 공통 namespace + email로 발번
- 같은 논리명을 retry마다 재발번해도 다른 ID가 된다고 착각
반대로 name의 canonicalization이 흔들리면 같은 대상인데 다른 UUID가 됩니다. RFC도 canonical representation의 취급을 꽤 강조하고 있습니다.710
이 계통에서 중요한 것은,
- UUIDv3 / v5는 「중복되지 않는 채번」이 아니라 「같은 입력이라면 같은 ID」
- namespace 설계를 애매하게 두지 않는다
- name의 canonicalization을 사양화한다
이 3가지입니다.
6. 패턴4: 시각계 UUID나 UUIDv8을 자체 구현하고 있다
UUIDv1 / v6 / v7 / v8은 겉모습만 흉내 내면 위험합니다.
6.1 UUIDv1 / v6에서 node나 clock sequence를 거칠게 다룬다
RFC 9562에서는 UUIDv6은 DB locality 개선을 위해 UUIDv1을 재배열한 것이며, clock sequence나 node를 다룹니다. 더욱이 분산 환경의 node collision resistance나 state 보존에 대해 여러 주의사항이 있습니다.11912
그것도 RFC는 가상 머신이나 컨테이너의 등장으로 MAC address의 일의성은 이제 보장되지 않는다고까지 쓰고 있습니다.5
그래서,
- MAC 주소니까 일의일 것이라고 단정짓는다
- node ID를 이미지 굽기로 복제한다
- clock sequence를 재기동마다 고정값으로 되돌린다
같은 설계는 위험합니다.
6.2 UUIDv7을 자작해 counter rollover나 clock rollback을 방치한다
UUIDv7은 꽤 실용적이지만, RFC는 고빈도 생성 시의 monotonicity와 counter handling을 자세히 쓰고 있습니다. clock rollback이나 counter rollover로 중복을 knowingly return해서는 안 된다고도 명시되어 있습니다.213
즉,
- 동일 밀리초 내에 대량 발번하는데 counter 설계가 없다
- 시각이 되돌아갔을 때 아무것도 하지 않고 생성을 계속한다
- 여러 프로세스가 같은 internal counter를 따로따로 초기화한다
같은 구현은 위험합니다.
6.3 UUIDv8을 「새로운 UUID 규격」 정도의 가벼운 마음으로 쓴다
UUIDv8은 편리해 보이지만, RFC 9562는 꽤 분명하며 UUIDv8의 일의성은 구현 의존이며 전제로 해서는 안 된다고 하고 있습니다.3
즉,
- timestamp를 매립한다
- shard id를 매립한다
- 어떤 업무 의미를 매립한다
- 나머지는 적당히 random을 넣는다
라는 「자사 독자 UUID」는 그 설계서가 UUID의 일의성 사양 그 자체입니다.
리뷰 없이 도입하기에는 꽤 위험합니다.
7. 패턴5: UUID를 중간에 짧게 만들어 버린다
생성까지는 올바르더라도 저장이나 비교 단계에서 망가뜨리는 경우가 있습니다.
전형 예는 다음입니다.
- 선두 8자만 외부 키 대용으로 사용
- 128비트 UUID를 64비트 정수로 찌그러뜨린다
- 문자열 컬럼 길이가 부족해서 말미가 잘린다
- 로그나 화면 표시의 단축 표현을 그대로 일의 키로 취급한다
여기서 중요한 것은 표현을 바꾸는 것 자체가 나쁜 것은 아니라는 점입니다.
- 하이픈을 뺀다
- 소문자 / 대문자를 맞춘다
- 바이너리 16바이트로 보관한다
처럼 128비트를 떨어뜨리지 않는 변환은 문제없습니다.
위험한 것은 일의성의 재료 그 자체를 깎는 변환입니다.
특히 「사람이 보기 쉬운 단축 ID」를 별도로 만들었는데, 그것이 어느 틈엔가 본래 UUID보다 우선되는 설계는 사고가 나기 쉽습니다.
8. 패턴6: DB 쪽에 일의 제약이 없다
마지막으로 꽤 중요한 것이 이것입니다.
UUID가 충분히 충돌하기 어렵다고 해도, 정말로 중복을 허용할 수 없다면 저장 위치에도 일의 제약을 두어야 합니다.
PostgreSQL의 공식 문서에서는 unique constraint는 열이나 열 집합의 값이 테이블 전체에서 일의임을 보장하고, primary key는 unique이며 not null인 행 식별자가 된다고 설명되어 있습니다.6
RFC 9562도 UUID는 구현상 충분한 일의성을 제공할 수 있는 한편, 진정한 global uniqueness를 절대 보장할 수는 없다고 하고 있습니다. 또한 collision impact가 높은 용도에서는 더 강한 대책을 취해야 한다고 하고 있습니다.14
그래서 실무에서는 다음 조합이 기본입니다.
- UUID는 충돌하기 어려운 ID로 사용
- DB는 UNIQUE / PRIMARY KEY로 최종 방어선을 둠
- 중복 시의 retry / idempotency / incident logging을 설계
UUID를 사용하는 것과 일의 제약을 두지 않는 것은 동의어가 아닙니다.
9. 실무용 체크리스트
마지막으로 도입이나 감사에서 그대로 쓰기 쉬운 형태로 정리합니다.
- UUID를 자체 생성하고 있지 않은지 확인한다
uuid4()/uuid7()같은 표준 API로 치우칠 수 있다면 우선 치우칩니다. - UUID의 version을 사양으로 정한다
v4/v7은 랜덤계, v3/v5는 결정론적, v8은 독자 사양이라고 명기합니다. - seed와 generator state의 처리를 재고한다
fork, worker 재기동, snapshot, clone 후에 같은 상태를 승계하지 않도록 합니다. - 저장 시 풀 길이를 유지하고 있는지 확인한다
prefix 비교나 단축 표시를 본래 키로 쓰지 않도록 합니다. - DB에 UNIQUE / PRIMARY KEY를 둔다
UUID는 확률을 낮추는 구조이며, 제약 그 자체가 아닙니다. - 중복을 관측할 수 있게 한다
duplicate key를 꾹 누르지 말고, 어느 generator / node / deployment에서 나왔는지 따라갈 수 있도록 합니다.
10. 정리
UUID의 충돌 사고는 대개 UUID가 약한 것이 아니라 UUID의 전제를 구현이나 운영에서 망가뜨리고 있는 곳에서 시작됩니다.
- 약한 난수로 자작
- fork나 snapshot 후의 상태를 되감는다
- name-based UUID를 채번 용도로 쓴다
- v7이나 v8을 가볍게 자체 구현한다
- 중간에 단축해 일의성을 버린다
- DB 쪽의 일의 제약을 뺀다
이런 것들을 하면 「UUID가 충돌했다」기보다 이쪽에서 충돌하기 쉬운 상황을 만들고 있다에 가까워집니다.
중복을 발견하면 먼저 의심해야 하는 것은 UUID의 수학보다 생성기, 상태 관리, 저장 형식, 제약 설계입니다.
그 순서로 보면 대개 원인은 꽤 좁혀집니다.
11. 관련 기사
12. 참고 자료
-
IETF RFC 9562, Section 5.4 UUID Version 4. UUIDv4의 122비트 난수 영역에 대해. ↩ ↩2
-
IETF RFC 9562, Section 5.7 UUID Version 7. UUIDv7의 timestamp, random bits, counter의 사고방식에 대해. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.8 UUID Version 8. UUIDv8의 일의성은 구현 의존이며 전제로 해서는 안 된다는 것에 대해. ↩ ↩2 ↩3
-
Python 3.14 documentation,
uuidmodule.uuid4()의 cryptographically-secure generation,uuid5()의 deterministic behavior,uuid7()/uuid8()의 성질에 대해. ↩ ↩2 ↩3 -
IETF RFC 9562, Universally Unique IDentifiers (UUIDs). UUID의 형식, 각 version, best practices 전체의 기준 문서입니다. ↩ ↩2
-
PostgreSQL documentation, Constraints. UNIQUE 제약과 PRIMARY KEY에 의한 일의성 담보에 대해. ↩ ↩2
-
IETF RFC 9562, Section 6.5 Name-Based UUID Generation. same namespace + same name이 같은 UUID가 되는 것, canonicalization의 중요성에 대해. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.9 Unguessability. CSPRNG 이용과 fork 후의 재 seed에 대해. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 6.3 UUID Generator States. stable storage나 generator state의 처리에 대해. ↩ ↩2 ↩3
-
IETF RFC 9562, Section 5.5 UUID Version 5. namespace + canonical name에 기반한 name-based UUID의 사양에 대해. ↩
-
IETF RFC 9562, Section 5.6 UUID Version 6. UUIDv6의 node / clock sequence / DB locality에 대해. ↩
-
IETF RFC 9562, Section 6.4 Distributed UUID Generation. 분산 환경에서의 node collision resistance에 대해. ↩
-
IETF RFC 9562, Section 6.2 Monotonicity and Counters. clock rollback, counter rollover, batch generation 시의 주의에 대해. ↩
-
IETF RFC 9562, Sections 6.7 and 6.8. collision resistance와 global uniqueness의 사고방식에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows의 문자 코드와 개행 코드를 정리한다 - Shift_JIS / UTF-8 / UTF-16, 문자 깨짐, CRLF / LF, 왜 혼란스러운가
Windows에서 자주 섞이는 Shift_JIS와 UTF-8, UTF-16, BOM, CRLF/LF의 차이를 bytes 시점에서 분해하고, 문자 깨짐과 개행 문제를 나누어 다루는 실무 규칙과 사고 조사의 5문 체크리스트까지 정리했습니다.
의사난수와 진짜 난수의 차이란 - 어떻게 구별하는지 정리
의사난수와 진짜 난수(NRBG)를 구별하는 핵심을 정리합니다. 출력의 겉모습이 아니라 생성기 구성, seed, reseed, health test로 봐야 한다는 점과 보안 용도에서는 OS의 secure RNG와 CSPRNG가 본진임을 짧게 설명합니다.
GS1 등 바코드 규격에서 무엇이 정해져 있고, 실운용에서 무엇에 주의해야 하는가 - GTIN · AI · GS1-128 · GS1 DataMatrix의 정리
GTIN・AI・GS1-128・GS1 DataMatrix・GS1 QR 코드의 차이를 정리하고, 식별 키, 속성, 심볼, 스캐너 출력, 상품 마스터 운용까지 분리해 설계하는 실무 포인트와 도입 순서를 정리합니다.
COM 컴포넌트나 OCX / ActiveX 개발에서 빠지기 쉬운 것 - Visual Studio의 32bit / 64bit, 등록, 관리자 권한의 덫을 정리
COM 컴포넌트와 ActiveX, OCX 개발에서 자주 만나는 0x80040154나 0x80070005를 비트 수, 등록 방식, HKCU와 HKLM 스코프, 관리자 권한이라는 네 축으로 풀어 Visual Studio 2022의 64bit화 시대에...
어디서 예외를 `catch`하고 로그를 내며 에러 처리해야 하는가 - 호출 계층의 경계와 책무를 실무용으로 정리
깊은 helper에서 넓게 catch하지 않고 호출 계층의 경계에 책무를 모아, 어디서 예외를 잡고 어디서 단일 주요 로그를 남기며 어디서 결과화·회복을 결정할지 .NET 실무 관점으로 정리한 가이드입니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크