최근 애플리케이션을 만들면서 UUID를 사용하였다. 이전에는 별 생각없이 사용했었는데 이번에 정리하고 싶어서 정리한다.
UUID(Universally Unique Identifier)는 전 세계적으로 중복될 가능성이 극히 낮은 고유 ID를 생성하는 표준이다. 대표적인 버전은 UUIDv4로, 랜덤 기반이다. 때문에 충돌 위험이 거의 없고 간단히 사용할 수 있어 TypeORM에서도 흔히 사용한다. 하지만 랜덤 기반이라는 특성 때문에 정렬성이 떨어지고, 인덱스 효율이 좋지 않다는 단점도 존재한다.
UUID는 버전이 여러 가지가 있는데, 그중 애플리케이션에서 실용적으로 고려할 만한 버전은 다음과 같다.
이 중 최근에 주목받는 것이 UUIDv7이다. 그리고 애플리케이션에 UUIDv7을 적용하였다.
UUID7은 시간 기반으로 생성되기 때문에 정렬성이 보장된다. 이는 데이터베이스의 클러스터링 인덱스와 결합될 때 큰 이점을 준다. 랜덤 기반인 UUIDv4는 매번 임의의 위치에 레코드를 삽입해야 하기 때문에 인덱스 페이지 분할과 디스크 I/O가 늘어난다. 반면 UUID7은 시간 순으로 증가하기 때문에 인덱스의 뒤쪽에 순차적으로 쌓여 성능이 좋아진다.
또한 UUID7은 랜덤 비트도 포함하기 때문에 분산환경에서도 충돌 위험이 낮다. 마치 ULID가 가진 장점과 UUID 표준이 가진 장점을 합친 형태라고 보면 된다.
TypeORM에서 UUID7은 직접 라이브러리를 사용해 생성하거나, uuid 패키지의 구현을 사용할 수 있다.
import { uuidv7 } from 'uuidv7';
// 사용 예
const id = uuidv7();
이렇게 생성된 UUID7은 문자열이지만, DB에 binary로 저장하면 더 큰 성능 이점을 얻을 수 있다.
DB에 UUID를 저장할 때 가장 흔한 방식은 char(36) 또는 varchar(36) 형태로 문자열로 저장하는 것이다. 사람이 읽기 편하고 디버깅도 쉽기 때문에 개발 초기에 매우 편하다.
장점
단점
초기엔 편하지만, 대규모 테이블에서 성능 병목이 생기기 쉬운 방식이다.
UUID 자체는 128bit = 16bytes이므로, 이를 binary(16)에 그대로 저장하면 공간을 16바이트로 줄일 수 있다. 문자열보다 절반 이하이며, 인덱스도 훨씬 작아지므로 성능상 유리하다.
장점
단점
하지만 변환 transformer만 구현하면 실사용에 큰 어려움은 없다.
TypeORM은 기본적으로 uuid 타입을 지원하지만, 이는 문자열 기반이다. binary로 저장하려면 다음 두 가지가 필요하다.
binary(16)으로 지정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()을 넣으면 된다.
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로 사용할 수 있다.
현재 UUIDv7을 binary로 저장하는 것으로 선택하여 애플리케이션을 만들고 있다. 하지만 나는 UUID를 그냥 문자열로 저장하고 싶었다. 왜냐하면 이전 회사에서의 경험으로 문자열로 저장할 때의 단점을 직접 경험하지는 못했다. 또한 binary로 저장하면 내가 데이터베이스에서 직접 조회를 해볼 때, 그냥 조회를 할 수가 없고 BIN_TO_UUID() 함수를 사용해서만 조회를 할 수가 있기 때문이다. 나는 이 부분이 매우 불편하다고 느낀다. 하지만 binary로 저장하는 것으로 결정이 되었고 그렇게 실행중이다.