티스토리 뷰
AI와 UUID 그리고 MySQL
서론
프로젝트를 진행하다가 UUID 기본키(PK)에 대한 내용이 나오게 되었습니다.
분산 데이터베이스나 클라우드 환경에서 UUID를 PK로 사용하면 충돌 위험 없이 고유성을 유지할 수 있다는 이야기입니다.
충돌 위험이 없다는 것은 각 데이터 처리가 독립적이라는 것과 같기에
UUID를 사용하는 것은 AI(Auto-Increment)에서 발생할 수 있는 동시성 문제와 병목현상을 줄일 수 있는 좋은 방법입니다. 🪄
AI(Auto-Increment)
자동 증가라는 이름 그대로 데이터베이스에서 새로운 레코드가 삽입될 때마다,
자동으로 숫자 값을 생성하여 테이블의 특정 열에 할당하는 기능입니다.
주로 PK에 사용되며 고유한 식별자를 보장하는 데 유용합니다.
MySQL에서는 AUTO_INCREMENT 속성을 통해 설정할 수 있습니다.
CREATE TABLE users (
id INT AUTO_INCREMENT,
username VARCHAR(50),
PRIMARY KEY (id)
);
AI PK는 정수형 숫자로 설정되기에 간단하고 직관적이며 저장 용량이 적다는 장점이 있습니다.
또한 정수값은 비교가 빠르고 인덱싱 효율이 좋기에 조회 성능이 좋죠.
하지만 위에서 언급했다시피 분산 처리 환경에서 한계점이 나타나게 됩니다.
- 중앙 집중식 키 생성 방식이기에 동시에 AI 값을 할당받으려는 트랜잭션이 발생할 경우, 락이 걸리며 성능 저하가 발생할 수 있습니다.
- 데이터베이스를 여러 인스턴스나 분산 환경으로 확장하거나 마이그레이션 할 때, 각 인스턴스에서 생성된 AI 값이 충돌할 수 있습니다.
- 시스템 규모를 외부에서 쉽게 예측할 수 있어 보안상 좋지 않습니다.
- 저장 가능한 최대 값에 도달하면 새로운 키를 생성할 수 없게 됩니다.
UUID(Universally Unique Identifier)
UUID는 128비트 크기의 숫자로, 고유한 식별자를 생성하기 위해 사용되는 표준방식입니다.
보통 다음과 같이 32개의 16진수 문자와 4개의 하이픈으로 구성됩니다. (8-4-4-4-12 형식)
123e4567-e89b-12d3-a456-426614174000
UUID의 버전
UUID는 여러 버전이 있으며, 각 버전은 고유한 생성 방식을 가집니다.
UUID v1 ⭐
시간 기반의 UUID입니다. 다음과 같이 구성됩니다.
- time_low: 타임스탬프의 하위 32비트
- time_mid: 타임스탬프의 중간 16비트
- time-hi 및 version: 타임스탬프의 상위 12비트 / UUID 버전 4비트
- clock_seq 및 variant : 클럭 시퀀스 14 비트 / UUID의 레이아웃 형식 2비트
- node: UUID를 생성하는 시스템의 노드, 일반적으로 네트워크 카드의 MAC 주소를 사용합니다.
일반적으로 컴퓨터에서는 유닉스 시간(1970년 1월 1일을 기준으로 사용)을 사용하지만,
v1에서는 1568년 10월 10일을 기준으로 한 100 나노초 단위의 시간을 사용합니다.
UUID v2
v2는 v1과 유사하지만, 일부 필드가 다르게 사용됩니다.
특정 사용자 및 그룹에 대한 식별 정보(POSIX UID, GID)가 포함되기 때문에 보안 및 복잡성 문제가 있어 잘 사용되지 않습니다.
UUID v3, v5
v3과 v5은 특정한 정보(네임스페이스와 이름)를 바탕으로 항상 같은 UUID를 생성하는 방식을 가집니다.
둘의 차이점은 사용하는 해싱 알고리즘에 있습니다.
v3는 MD5 해싱 알고리즘*을 사용하고, v5 SHA-1 해싱 알고리즘을 사용합니다.
SHA-1이 더 안전하고 충돌 가능성이 낮습니다.
*해싱 알고리즘은 입력값을 고정된 길이의 난수로 변환해 주는 함수를 뜻합니다.
UUID v4 ⭐
UUID 버전 4는 거의 모든 비트가 무작위 값으로 채워집니다. 때문에 대부분의 경우 고유성과 예측 불가능성을 보장합니다.
고정된 값이 할당되는 위치는 다음과 같습니다.
- 세 번째 세그먼트의 첫 번째 위치 : 버전 정보로 항상 4가 들어갑니다.
- 네 번째 세그먼트의 첫 번째 위치 : variant를 의미하며 : 상위 두 비트가 8, 9, A, B 중 하나로 들어갑니다.
충돌 가능성이 극히 낮아 분산 시스템 및 데이터베이스에서 고유 식별자로 널리 사용됩니다.
UUID v6
시간 기반 UUID입니다. v1과 구조적으로 비슷하지만, 타임스탬프의 비트 순서를 뒤집어 큰 단위들이 앞에 오도록 합니다.
- v1: 시간 정보가 뒤죽박죽으로 배치되어 정렬이 어렵습니다.
- v6: 시간의 큰 단위(연도, 월, 일 순)가 앞에 오도록 배치되어 정렬이 쉽습니다.
이는 데이터베이스나 로그 파일에서 시간순으로 정렬해야 할 때 유리합니다.
UUID v7
v7 또한 시간 기반 UUID입니다.
v1과 다르게 유닉스 타임스탬프를 사용하고, 노드에 무작위로 생성된값을 포함하여 보안과 프라이버시를 강화합니다.
v6과 같이 큰 단위의 시간들이 앞에 오도록 해 정렬에 유리합니다.
- v1 : 1568년 10월 10일을 기준으로 한 타임스탬프 / 노드 값으로 주로 시스템의 MAC 주소가 사용됩니다.
- v7 : 유닉스 시간을 기준으로한 타입 스탬프 / 노드 값이 무작위로 생성되어 특정 하드웨어 정보를 기반하지 않습니다.
UUID v8
v8은 최신버전의 다른 버전의 고정된 생성 방식에서 벗어나
사용자가 자신의 필요에 맞게 유연하게 UUID를 생성도록 지원합니다.
세 번째 세그먼트에 버전 번호 '8'을 포함하는 것 외에 나머지 비트들은 자유롭게 정의할 수 있습니다.
MySQL과 UUID
그렇다면 PK는 무조건 UUID를 사용하는 게 좋을까요? 🤔
MySQL에서 UUID PK를 사용할 때에 고려해야 할 트레이드오프*에 조금 더 알아보도록 하겠습니다.
* 트레이드오프(trade-off): 상충관계, 하나를 얻으면 다른 하나를 잃을 수 있는 관계를 말합니다.)
삽입 성능
MySQL은 테이블에 새 레코드가 삽입될 때마다 기본 키와 관련된 인덱스를 업데이트해야 합니다.
MySQL은 인덱스로 B+트리 구조를 사용하고 있기에,
UUID를 PK로 사용 시 삽입 연산 때마다 트리에 UUID를 삽입해야 합니다.
UUID는 랜덤 하게 생성되므로, 삽입 위치가 트리 전체에 걸쳐 불규칙적으로 분산됩니다.
이는 빈번한 트리 분할을 야기해 삽입 성능을 저하시키게 됩니다.
(B+ 트리의 특성상 인덱스의 끝에 삽입되는 것이 가장 효율적입니다!)
저장 공간의 활용도
UUID는 128비트의 크기를 가집니다.
대체로 32비트의 정수를 저장하는 AI보다 훨씬 많은 저장 공간을 차지하게 되죠.
문자열 형태로 저장하는 경우에는 더 많은 공간을 차지하게 됩니다.
(UUID를 CHAR(36)으로 저장하면 688비트의 공간을 차지하게 되는데, 이때는 AI보다 20배 더 많은 용량을 사용하게 됩니다.)
기본키가 크면 보조 인덱스 또한 용량이 늘어나기 때문에, 보조인덱스를 사용하는 경우 용량이 더욱 커지게 됩니다.
그 외에 InnoDB와 같이 페이지 분할 방식*에 의존하는 경우 성능저하가 일어날 수 있습니다.
*페이지 분할 방식
데이터베이스 엔진이 데이터를 디스크에 저장할 때 페이지(Page)라는 단위로 나눠서 저장하는 것을 의미합니다.
페이지 분할 방식은 보통 데이터가 증가하는 순서에 따라 페이지를 나누는데
UUID는 무작위성을 가지기 때문에 값의 순서를 예측하는 것이 어려워집니다.
MySQL에서 UUID 사용하기
그럼에도 UUID PK를 사용해야 하는 경우, 위에서 소개한 리스크를 줄일 수 있는 몇 가지 방법을 소개합니다.✌️
순차적인 UUID 사용하기
UUID v6과 v7과 같이 시간 기반의 UUID를 사용하면 고유성을 보장하면서도 성능 및 저장 공간 문제를 완화할 수 있습니다.
해당 버전의 UUID를 이용하면 여러 시스템에서 생성되더라도 어느 정도 순차적인 값을 제공하기에
인덱싱 및 페이지 분할 문제를 완화하는 데 도움이 됩니다.
v1도 시간 기반의 UUID지만, 작은 단위의 시간을 먼저 배치해 위의 버전들만큼 순차성을 제공하지 않기에 적합하지 않습니다.
(MySQL 내장 함수 UUID_TO_BIN()과 swap flag에 대한 내용은 생략합니다.)
BINARY(16)에 저장하기
UUID를 바이너리 형식으로 저장하면 문자열로 저장하는 것에 비해 저장 공간을 절약할 수 있습니다.
BINARY(16)을 통해 각 값당 16 바이트만 사용해 저장할 수 있습니다.
그래도 32비트 정수보다는 크지만 CHAR(36)을 사용하는 것보다는 용량을 절약할 수 있는 좋은 방법입니다.
다른 식별자 사용하기
UUID 외에도 사용할 수 있는 식별자들이 존재합니다!
Snowflake ID, ULID, NanoID 등이 있으며, 요구 사항에 따라 별도의 식별자를 선택해 성능을 최적화할 수 있습니다.
- Snowflake ID : Twitter에서 개발한 식별자 생성기로, 순차적으로 증가하면서 고유성을 보장한 64비트 정수를 생성합니다.
- ULID : 48비트의 타임스탬프와 80비트의 랜덤 값으로 구성된 정렬가능한 문자열을 생성합니다. URL-safe
- NanoID : 보통 21자 길이의 문자열로 생성됩니다. 짧은 길이로도 높은 고유성 보장합니다. 짧고 비순차적인 식별자가 필요한 경우 사용됩니다.
결론
PK는 시스템의 규모와 요구 사항, 운영 환경을 고려해 선택하는 것이 좋습니다.
분산 시스템에서의 고유성 보장과 확장성을 중시한다면 UUID가 적합하며,
단일 데이터베이스 환경에서의 성능 최적화와 보다 간단한 관리가 필요하다면 AI를 사용하는 것을 추천합니다.
각 선택에서 발생할 수 있는 트레이드오프를 잘 이해하고 적용하는 것이 좋은 데이터베이스 설계라 생각합니다.
감사합니다.
공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.
'DB' 카테고리의 다른 글
[DB] 인덱스에서 B+Tree를 사용하는 이유 (0) | 2024.03.12 |
---|---|
[DB] MySQL의 락 (feat. Auto Increment Lock) (0) | 2024.03.10 |
[DB] 락(Lock)과 트랜잭션 (0) | 2024.03.09 |
[DB] 정규형(Normal form) (0) | 2024.03.08 |
[DB] 인덱스(Index) (0) | 2024.03.07 |