6P by GN⁺ 13시간전 | ★ favorite | 댓글 1개
  • 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() 를 기본 키로 사용하는 것은 피해야 함
Hacker News 의견들
  • 이건 전형적인 조기 최적화의 예시임
    영구 식별자에 데이터를 담는 건 데이터 관리의 금기사항임
    노르웨이 주민번호처럼 생년월일을 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() 함수가 있지만, 확장 기능이 더 많은지는 아직 불분명함
    • 대부분의 사용자는 이제 별도 확장이 필요 없을 것임