UUID 사용기

블로그·2025년 11월 16일

1. 들어가며

최근 애플리케이션을 만들면서 UUID를 사용하였다. 이전에는 별 생각없이 사용했었는데 이번에 정리하고 싶어서 정리한다.

2. UUID 기본 개념

UUID(Universally Unique Identifier)는 전 세계적으로 중복될 가능성이 극히 낮은 고유 ID를 생성하는 표준이다. 대표적인 버전은 UUIDv4로, 랜덤 기반이다. 때문에 충돌 위험이 거의 없고 간단히 사용할 수 있어 TypeORM에서도 흔히 사용한다. 하지만 랜덤 기반이라는 특성 때문에 정렬성이 떨어지고, 인덱스 효율이 좋지 않다는 단점도 존재한다.

UUID는 버전이 여러 가지가 있는데, 그중 애플리케이션에서 실용적으로 고려할 만한 버전은 다음과 같다.

  • UUIDv1: timestamp + MAC 기반. 정렬성 있음. 단, 개인정보 노출 위험.
  • UUIDv4: 랜덤 기반. 가장 흔하게 사용. 정렬성 없음.
  • UUIDv7: timestamp 기반 + 랜덤. 정렬성과 분산성을 갖춘 최신 표준.

이 중 최근에 주목받는 것이 UUIDv7이다. 그리고 애플리케이션에 UUIDv7을 적용하였다.

3. UUID7(ULID-like)의 등장

UUID7은 시간 기반으로 생성되기 때문에 정렬성이 보장된다. 이는 데이터베이스의 클러스터링 인덱스와 결합될 때 큰 이점을 준다. 랜덤 기반인 UUIDv4는 매번 임의의 위치에 레코드를 삽입해야 하기 때문에 인덱스 페이지 분할과 디스크 I/O가 늘어난다. 반면 UUID7은 시간 순으로 증가하기 때문에 인덱스의 뒤쪽에 순차적으로 쌓여 성능이 좋아진다.

또한 UUID7은 랜덤 비트도 포함하기 때문에 분산환경에서도 충돌 위험이 낮다. 마치 ULID가 가진 장점과 UUID 표준이 가진 장점을 합친 형태라고 보면 된다.

TypeORM에서 UUID7은 직접 라이브러리를 사용해 생성하거나, uuid 패키지의 구현을 사용할 수 있다.

import { uuidv7 } from 'uuidv7';

// 사용 예
const id = uuidv7();

이렇게 생성된 UUID7은 문자열이지만, DB에 binary로 저장하면 더 큰 성능 이점을 얻을 수 있다.

4. DB에 UUID를 문자열로 저장하는 방식

DB에 UUID를 저장할 때 가장 흔한 방식은 char(36) 또는 varchar(36) 형태로 문자열로 저장하는 것이다. 사람이 읽기 편하고 디버깅도 쉽기 때문에 개발 초기에 매우 편하다.

장점

  • 가독성이 좋다.
  • 별도 변환 없이 바로 값을 확인할 수 있다.
  • DB 툴에서 다루기 쉽다.

단점

  • 저장 공간이 36바이트로 커서 비효율적이다.
  • 인덱스가 커져서 조회 성능이 떨어진다.
  • 랜덤성 높은 UUID라면 인덱스 페이지 분할이 많이 발생한다.

초기엔 편하지만, 대규모 테이블에서 성능 병목이 생기기 쉬운 방식이다.

5. DB에 UUID를 Binary(16 bytes)로 저장하는 방식

UUID 자체는 128bit = 16bytes이므로, 이를 binary(16)에 그대로 저장하면 공간을 16바이트로 줄일 수 있다. 문자열보다 절반 이하이며, 인덱스도 훨씬 작아지므로 성능상 유리하다.

장점

  • 저장 공간이 16bytes로 효율적이다.
  • 인덱스 크기가 작아져 성능이 좋아진다.
  • 특정 UUID 버전(v1, v7)과 조합하면 정렬성이 증가하여 insert 속도도 유리하다.

단점

  • 사람이 직접 읽을 수 없다.
  • 애플리케이션에서 문자열 ↔ binary 변환 로직이 필요하다.

하지만 변환 transformer만 구현하면 실사용에 큰 어려움은 없다.

6. TypeORM에서 UUID를 Binary(16)로 저장하기

6.1. TypeORM에서의 기본 방식

TypeORM은 기본적으로 uuid 타입을 지원하지만, 이는 문자열 기반이다. binary로 저장하려면 다음 두 가지가 필요하다.

  1. DB 컬럼 타입을 binary(16)으로 지정
  2. 문자열 UUID ↔ 16진 buffer 변환을 위한 transformer 구현

6.2. 문자열 UUID ↔ Buffer 변환 코드

UUID 문자열을 16바이트 binary로 바꾸기 위해 Buffer.from()을 그대로 쓰면 안 되고, 하이픈을 제거한 뒤 hex 처리해야 한다.

import { v4 as uuidv4 } from 'uuid';

export const uuidBinaryTransformer = {
  to(value: string): Buffer {
    // UUID 문자열에서 하이픈 삭제
    const hex = value.replace(/-/g, '');
    return Buffer.from(hex, 'hex');
  },
  from(value: Buffer): string {
    const hex = value.toString('hex');
    // 원래 UUID 형태로 하이픈 복원
    return [
      hex.substring(0, 8),
      hex.substring(8, 12),
      hex.substring(12, 16),
      hex.substring(16, 20),
      hex.substring(20),
    ].join('-');
  },
};

UUID7을 사용하려면 uuidv7()을 넣으면 된다.

6.3. 실제 TypeORM Entity 예시

import { Entity, PrimaryColumn, Column } from 'typeorm';
import { uuidv7 } from 'uuidv7';
import { uuidBinaryTransformer } from './uuid-binary.transformer';

@Entity()
export class User {
  @PrimaryColumn({
    type: 'binary',
    length: 16,
    transformer: uuidBinaryTransformer,
  })
  id: string = uuidv7();

  @Column()
  name: string;
}

이렇게 하면 DB에는 binary(16)로 저장되고, 애플리케이션에서는 문자열 UUID로 사용할 수 있다.

7. 마무리

현재 UUIDv7을 binary로 저장하는 것으로 선택하여 애플리케이션을 만들고 있다. 하지만 나는 UUID를 그냥 문자열로 저장하고 싶었다. 왜냐하면 이전 회사에서의 경험으로 문자열로 저장할 때의 단점을 직접 경험하지는 못했다. 또한 binary로 저장하면 내가 데이터베이스에서 직접 조회를 해볼 때, 그냥 조회를 할 수가 없고 BIN_TO_UUID() 함수를 사용해서만 조회를 할 수가 있기 때문이다. 나는 이 부분이 매우 불편하다고 느낀다. 하지만 binary로 저장하는 것으로 결정이 되었고 그렇게 실행중이다.

0개의 댓글