# Postgres에서 UUID 버전 4 기본 키를 피해야 하는 이유

> Clean Markdown view of GeekNews topic #25109. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=25109](https://news.hada.io/topic?id=25109)
- GeekNews Markdown: [https://news.hada.io/topic/25109.md](https://news.hada.io/topic/25109.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-12-16T10:08:52+09:00
- Updated: 2025-12-16T10:08:52+09:00
- Original source: [andyatkinson.com](https://andyatkinson.com/avoid-uuid-version-4-primary-keys)
- Points: 10
- Comments: 1

## Summary

**UUID v4 기본 키**는 무작위성이 높아 PostgreSQL의 **B-Tree 인덱스 효율을 크게 떨어뜨립니다.** 삽입 시 페이지 분할과 랜덤 접근이 잦아져 WAL 로그가 커지고, 캐시 적중률이 낮아져 쓰기 지연이 발생합니다. 또한 16바이트 크기로 bigint보다 두 배의 공간을 차지해 메모리 낭비가 불가피합니다. 신규 프로젝트에서는 정수형 시퀀스나 **시간순 UUID v7**을 사용하는 것이 더 안정적인 선택입니다.

## Topic Body

- **UUID v4**는 무작위성이 높아 **인덱스 비효율과 과도한 I/O**를 유발하며, PostgreSQL에서 기본 키로 사용할 경우 성능 저하가 발생함  
- 무작위 삽입으로 인해 **페이지 분할(page split)** 과 **인덱스 단편화**가 잦아지고, **WAL 로그 크기 증가** 및 쓰기 지연이 발생함  
- UUID는 **16바이트 크기**로 bigint보다 두 배의 공간을 차지하며, 캐시 적중률 저하와 **메모리 낭비**로 이어짐  
- 보안 식별자로 오해받지만, RFC 4122에 따르면 **UUID는 추측 방지용 보안 수단이 아님**  
- 새로운 데이터베이스에는 **정수형 시퀀스 기반 키**를 사용하고, 불가피할 경우 **시간순 UUID v7**을 사용하는 것이 권장됨  

---

### UUID v4의 성능 문제
- PostgreSQL에서 **UUID v4 기본 키**를 사용한 데이터베이스는 지난 10년간 일관되게 **성능 저하와 과도한 I/O**를 보임  
  - UUID v4는 122비트가 무작위로 생성되어 인덱스 정렬이 불가능함  
  - 삽입 시 순차적 페이지에 저장되지 않아 **랜덤 접근**이 발생, 업데이트·삭제 시에도 비효율적 탐색이 필요함  
- **B-Tree 인덱스**는 정렬된 데이터를 전제로 하지만, UUID v4는 순서성이 없어 **삽입 효율이 낮음**  
  - 각 삽입이 임의의 페이지에 기록되어 **중간 페이지 분할**이 자주 발생  
  - 이로 인해 **쓰기 지연(latency)** 과 **WAL 증가**가 발생함  

### UUID의 구조와 대안
- UUID는 128비트(16바이트) 크기의 식별자이며, PostgreSQL에서는 **binary uuid 타입**으로 저장됨  
- **UUID v4**는 무작위 비트 기반, **UUID v7**은 앞 48비트에 **타임스탬프**를 포함해 인덱스 효율이 높음  
- PostgreSQL 18(2025년 예정)에서 UUID v7이 기본 지원 예정  
- **UUID v7**은 시간순 정렬이 가능해 **페이지 밀도와 캐시 효율**이 개선됨  

### UUID 선택 이유와 한계
- 다중 클라이언트나 마이크로서비스 환경에서 **충돌 없는 식별자 생성**이 필요할 때 UUID가 사용됨  
  - 예: 여러 데이터베이스 인스턴스에서 동시에 ID 생성  
- 그러나 RFC 4122는 “UUID는 추측이 어렵다고 가정하지 말라”고 명시, **보안 식별자로 부적합**  
- 충돌 확률은 2.71×10¹⁸개 생성 시 50%로, 현실적으로 충돌 가능성은 낮지만 **성능 비용이 큼**  

### UUID의 공간 및 I/O 비효율
- UUID는 **bigint(8바이트)** 의 두 배, **int(4바이트)** 의 네 배 공간을 차지  
  - 대규모 테이블에서는 **저장 공간 증가**와 **백업·복원 시간 증가**로 이어짐  
- **인덱스 페이지 밀도** 실험 결과  
  - integer 인덱스: 97.64%  
  - UUID v4 인덱스: 79.06%  
  - UUID v7 인덱스: 90.09%  
- Cybertec 테스트에서 **UUID v4 인덱스 조회 시 8.5백만 개 추가 페이지 접근**, **31229% I/O 증가** 확인  
  - 동일 조건에서 bigint 인덱스는 27,332 버퍼 접근, UUID v4는 8,562,960 버퍼 접근  

### 캐시 및 메모리 영향
- UUID는 랜덤 분포로 인해 **버퍼 캐시 적중률(cache hit ratio)** 이 낮음  
  - 더 많은 페이지를 캐시에 로드해야 하며, 필요한 페이지가 자주 **퇴출(eviction)** 됨  
- 캐시 효율 저하로 쿼리 지연이 발생하며, **메모리 사용량 증가**  
- 성능 유지를 위해 **정기적인 인덱스 재구성(REINDEX CONCURRENTLY)** 또는 **pg_repack** 사용 권장  

### 성능 완화 방안
- **메모리 확장**: 데이터베이스 크기의 4배 RAM 확보 권장 (예: DB 25GB → 메모리 128GB)  
- **work_mem 조정**: 정렬 연산 시 더 많은 메모리 할당으로 성능 개선 가능  
- **Rails 환경**에서는 `implicit_order_column` 설정을 통해 UUID 대신 `created_at` 등 정렬 가능한 필드 사용  
- **CLUSTER 명령**으로 정렬 가능한 필드 기준으로 테이블 재배치 가능하나, **배타적 락** 필요  

### 정수형 키와 시퀀스 권장
- 신규 데이터베이스에는 **정수형 시퀀스 기반 키** 사용 권장  
  - `integer`(4바이트)는 약 20억 개, `bigint`(8바이트)는 훨씬 더 많은 고유 값 제공  
- 대부분의 비즈니스 앱은 integer로 충분하며, 대규모 서비스는 **bigint** 사용이 적합  
- **UUID v4 대신 UUID v7** 또는 **sequential_uuids 확장**을 사용하는 것이 현실적 대안  

### 요약
- UUID v4는 **랜덤성으로 인한 인덱스 비효율, 높은 I/O, 낮은 캐시 효율**을 초래  
- **보안 식별자**로 사용할 수 없으며, **공간 낭비**가 큼  
- **정수형 시퀀스 키**가 대부분의 애플리케이션에 더 적합  
- 불가피하게 UUID를 사용해야 한다면 **시간순 UUID v7**을 선택해야 함  
- PostgreSQL에서 **gen_random_uuid()** 를 기본 키로 사용하는 것은 피해야 함

## Comments



### Comment 47814

- Author: neo
- Created: 2025-12-16T10:08:52+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=46272487) 
- 이건 전형적인 **조기 최적화**의 예시임  
  영구 식별자에 데이터를 담는 건 데이터 관리의 금기사항임  
  노르웨이 주민번호처럼 생년월일을 ID에 넣으면, 나중에 출생일을 잘못 알았던 이민자들이 생기거나, 1월 1일 생이 너무 많아 번호가 모자라는 문제를 겪게 됨  
  과거 카드 카탈로그 시절엔 조회 비용을 줄이려 데이터와 식별자를 섞는 게 이해되지만, 지금은 강력한 데이터베이스가 있으니 굳이 그럴 필요가 없음  
  - 이 예시는 사실 잘못된 **기본값 설정** 문제라고 생각함  
    모르는 생일을 1월 1일로 지정한 게 문제였지, 날짜를 키에 넣은 게 본질적 문제는 아님  
    만약 00이나 99 같은 비날짜 값을 썼다면 충돌이 없었을 것임  
    UUID에 타임스탬프를 넣는 건 의미 부여가 아니라 **성능 최적화** 목적임  
    시간 순으로 증가하는 키는 B-tree 재작성 비용을 줄여 DB 삽입 성능을 높임  
  - 이탈리아 주민번호도 성별을 포함하는데, 성전환 수술 후엔 문제가 생김  
    “영구 식별자엔 데이터를 담지 말라”는 건 일반론일 뿐, 상황에 따라 **트레이드오프**를 감수할 수도 있음  
    예를 들어 md5 해시를 UUID로 써서 인덱스를 구성하면 단편화는 생기지만 관리 가능한 수준임  
  - UUIDv7은 단순히 **시간 편향(random bias)** 을 가진 생성 방식일 뿐, 실제 정보를 담는 건 아님  
    랜덤 vs 시간 기반 UUID 선택은 밀리초 단위가 아니라 초 단위의 성능 차이를 만들 수 있음  
  - 작은 DB에선 조기 최적화지만, 규모가 커지면 오히려 반대의 접근이 필요함  
    대규모 DB에선 샤딩과 분산이 필수라 **UUID가 오토인크리먼트보다 낫게 작동**함  
  - 노르웨이 주민번호 예시에 대해, 실제로 1월 1일 생이 그렇게 많을 수 있는지 의문임  
    포맷이 DDMMYYXXXXX라면 10만 명까지 커버 가능한데, 정말 그 정도로 몰릴 수 있는지 궁금함  
    아마 특정 연도에 난민이 대거 유입된 경우 같은 특수 상황일 것 같음  

- UUID는 **보안 토큰**처럼 쓰면 안 됨  
  추측이 어렵다는 이유만으로 보안 기능으로 쓰는 건 위험함  
  랜덤 값의 목적은 추측 방지뿐 아니라, 연속된 ID 간의 관계를 숨기는 데 있음  

- DB 종류에 따라 **PK 전략**이 완전히 달라짐  
  Postgres에선 랜덤 PK가 비효율적이지만, Cockroach나 Spanner 같은 분산 DB에선 오히려 단조 증가 키가 **핫샤드 문제**를 일으킴  
  - 분산 DB에서도 완전 랜덤보단 **증가 경향이 있는 키**가 좋음  
    UUIDv7은 상위 비트가 정렬 가능하고 하위 비트는 랜덤이라 노드 간 분산성과 로컬 저장 효율을 동시에 얻을 수 있음  
  - DB 종류보단 **DB 구조**의 차이로 봐야 함  
    일반적인 비샤딩 DB에선 랜덤 키가 B-tree 단편화를 유발함  
  - Google Cloud Bigtable에선 순차 키를 **역순(reverse)** 으로 써서 자동 분산을 이끌어냄  
  - 샤딩된 Postgres라면 랜덤 PK가 유리함  
    하지만 범위 조회(range query)가 많다면 랜덤 키는 불리함  
    결국 **워크로드 특성**에 따라 선택해야 함  
  - 쓰기 중심, 시간 편향이 큰 워크로드라면 Postgres에서도 랜덤 PK가 더 나을 수 있음  

- 글의 요지는 UUIDv4를 PK로 쓰는 단점을 잘 짚었지만, 제시된 **정수 난독화 방식**은 실서비스엔 부적합해 보임  
  소규모 DB라면 UUIDv7이 합리적 절충안임  
  - 나는 UUIDv7 대신 UUIDv4를 선호함  
    생성 시각이 노출되길 원치 않기 때문임  
    데이터가 많아 UUIDv4의 랜덤성이 성능 문제를 일으키지 않는 한, v4가 더 안전한 선택임  
  - Postgres에선 단일 시퀀스를 쓰는 걸 좋아함  
    약간의 정보 노출은 있지만, **운영상 충분히 모호함**  
  - 단순히 사용자 수를 숨기고 싶다면, 오토인크리먼트 키에 **암호화(permutation)** 를 적용하면 됨  
    예를 들어 AES-128로 변환 후 base64로 인코딩하면 YouTube 영상 ID처럼 보이게 만들 수 있음  

- 나는 기술 실사 중 많은 회사를 보는데, **빠른 샤딩 가능성**이 기업 성장의 핵심임  
  UUID를 모든 테이블에 두면 샤딩 시 구조 변경 없이 확장 가능함  
  약간의 공간·시간 손해보다 훨씬 큰 **확장성 이점**을 줌  
  - UUIDv7은 단조 증가 특성 덕분에 Postgres에서도 성능상 이점이 있음  
  - 우리도 샤딩 과정에서 UUID가 없어 고생했음  
    결국 복잡한 데이터 모델이라 UUID 유무보다 마이그레이션 자체가 어려웠음  

- 우리 앱은 정수 PK를 **암호화해 UUID처럼 보이게** 만듦  
  순차 ID가 노출되면 고객 수 추정이나 **딕셔너리 공격**이 가능하기 때문임  
  암호화된 ID는 복호화 실패로 스캐닝 시도를 즉시 탐지할 수 있음  
  - 하지만 키 분실이나 교체 시 **복호화 불가** 문제가 생길 수 있음  
  - 키 관리 방식이 궁금함 — 환경 변수로 주입하는지, 코드에 내장하는지, AES-GCM 같은 **AEAD 스킴**을 쓰는지 등 보안 관리가 중요함  

- “20억 개면 충분하다”는 말은 위험함  
  모든 DBA는 그런 결정으로 시작된 **악몽 같은 사례**를 하나쯤 갖고 있음  

- 글에서 “랜덤 값은 정렬이 비효율적”이라 했는데, 사실 **바이트 순서 정렬**은 가능함  
  다만 랜덤 키는 순차 삽입이 아니므로 B-tree 재균형이 자주 일어나 성능 저하가 생김  
  - UUIDv4는 분산 환경에서 유용하지만, 128비트 공간과 비순차성의 대가를 감수해야 함  
  - 글쓴이는 이후 B-tree 인덱스 비교 실험을 추가했다고 함  
    정수 PK는 인덱스가 메모리에 잘 맞고, UUIDv4는 페이지 접근이 많아 **지연(latency)** 이 커졌다고 함  
  - 기술적으로 근거가 약하다는 의견도 있었음  
  - B-tree는 증가 키일수록 삽입 효율이 높고, 랜덤 키는 **캐시 친화성**이 떨어짐  
  - 데이터 접근이 생성 시점과 밀접할수록, 시간 순 정렬이 성능상 유리함  

- 이 글은 문제보다 **해결책이 먼저 나온 조기 최적화**로 보임  
  UUIDv4는 대부분의 경우 충분히 괜찮음  
  성능 문제는 실제로 발생할 때 고려해야 함  
  - 하지만 한 번 UUIDv4로 시작하면 나중에 int64로 **재키(rekey)** 하긴 거의 불가능함  
  - 실제로 성능 문제가 생길 땐 이미 성장 단계라 PK를 바꿀 여유가 없음  

- 요약하자면, Postgres에선 UUIDv7이 v4보다 약간 더 나은 성능을 보임  
  최신 버전에선 **플러그인 없이도 UUIDv7 지원**이 가능함  
  - 다만 글의 핵심은 “가능하면 **시퀀스·정수형 PK**를 쓰라”는 권장임  
  - Postgres 18부터는 내장 uuidv7() 함수가 있지만, 확장 기능이 더 많은지는 아직 불분명함  
  - 대부분의 사용자는 이제 별도 확장이 필요 없을 것임
