시작하며
UUID와 ULID는 데이터베이스와 분산 시스템에서 널리 사용되는 식별자입니다.
UUID는 분산 시스템에서 널리 이용되며, 버전별로 생성 방법과 특성이 다릅니다.
ULID는 UUID의 단점을 보완하기 위해 등장한 새로운 식별자로, 시간 순서대로 정렬할 수 있다는 것이 특징입니다.
이 글에서는 각 식별자의 특징과 어떤 상황에서 사용해야 하는지에 대해 깊이 있게 다루어 보겠습니다.
자동 증가형(primary key)만을 고려하고 있다면, 이 글을 참고하시기 바랍니다.
특징 | 자동 증가형 | UUID v4 | UUID v7 | ULID |
---|---|---|---|---|
데이터 타입(MySQL) | INT, BIGINT | CHAR(36) | CHAR(36) | CHAR(26) |
정렬 가능 여부 | ❌ | ❌ | ⭕️ | ⭕️ |
데이터 크기 | 4바이트(INT 기준) | 16바이트 | 16바이트 | 16바이트 |
예시 | 1, 2, 3, ... | 550e8400-e29b-41d4-a716-446655440000 | 018e4c59-64e3-7baf-bc9b-3d0fd7f7ddf4 | 01AN4Z07BY79KA1307SR9X4MV3 |
자동 증가형을 채택하고 싶지 않은 경우
Auto Increment는 데이터베이스에서 자동으로 고유한 식별자를 생성하는 메커니즘입니다.
일반적으로 숫자형 컬럼이 대상이 되며, 새로운 레코드가 삽입될 때마다 그 컬럼의 값이 자동으로 증가합니다.
전형적인 ID 방식이죠. 여기서는 일의성을 확보하는 문제나 데이터 이관 및 백업의 단점에 대해서는 언급하지 않고, 보안과 프라이버시 우려에 집중해서 살펴보겠습니다.
예측 가능성
Auto Increment형 ID는 연속된 번호이기 때문에, 다음에 생성될 ID를 쉽게 예측할 수 있습니다.
이로 인해 공격자가 시스템의 내부 구조를 추측하고, 불법 접근을 시도할 위험이 높아집니다.
정보 유출의 위험
연속된 ID는 데이터베이스의 삽입 순서를 반영하므로, 공개될 경우 기업의 활동 패턴이나 데이터 생성 빈도가 유출될 수 있습니다.
예시
경쟁 업체가 공개된 연속된 ID를 분석하여, 기업의 신제품 출시 빈도를 파악했습니다.
신제품의 ID가 순차적으로 증가하고 있다는 사실을 통해 출시 시기와 제품 개발 속도를 예측할 수 있었고, 이에 따라 자사 전략을 조정했습니다.
또한 결제 관리에 사용되는 ID가 연속된 번호로 되어 있어서, 공개되지 않은 사용자 등록 수나 유료 플랜 가입 수 등을 추측할 수 있었습니다.
이와 같은 위험이 발생할 수 있습니다.
UUID (Universally Unique Identifier)
UUID는 128비트의 식별자로, 여러 버전이 존재합니다. 각 버전은 서로 다른 방법으로 식별자를 생성합니다.
일부 버전은 ID 생성에 MAC 주소를 사용하여 프라이버시 문제를 일으키거나, MD5나 SHA-1을 사용하여 충돌 위험이 있어 사용되지 않습니다.
(참고: SHA1 충돌 최초 발생, MD5 충돌)
현재 일반적으로 사용되는 것은 UUID v4이며, 최근에 제안된 것은 UUID v7입니다.
UUID v4
ID 생성 방법
UUID v4는 무작위로 생성된 128비트 값을 기반으로 합니다.
이 버전의 UUID는 모든 비트가 무작위로 설정되므로, 생성이 매우 간단하고 높은 일의성을 가집니다.
또한 다음과 같은 고정된 정보도 포함되어 있습니다.
- 버전 비트를 설정: 128비트 중 특정 4비트(버전 필드)를 0100으로 설정
- 변형 비트를 설정: 다음으로, 특정 2비트(변형 필드)를 10으로 설정하여 UUID가 RFC 4122에 준수함을 나타냅니다.
import uuid
uuid_v4 = uuid.uuid4()
print(uuid_v4)
다음은 위에서 생성된 UUID v4입니다.
두 UUID 모두 세 번째 섹션이 4로 설정되어 있으며, 네 번째 섹션의 문자가 변형 비트 1(처음 두 비트가 10)로 설정되어 있습니다 (a와 8 부분).
dac78382-23f0-414b-ad31-9cbf3d872fab
5fca6ad9-149b-4392-8d28-6ba3acc96e08
UUID v7
다음은 UUID v7에 대한 설명입니다. 기본적으로 UUID v4를 정렬 가능하게 만든 것으로 보면 됩니다.
ID 생성 방법
- 타임스탬프를 가져오기: 현재 타임스탬프를 밀리초 단위로 가져와 48비트 비트열로 변환합니다.
- 무작위 비트 생성: 나머지 80비트를 암호학적으로 안전한 무작위 수로 채웁니다.
- 버전 비트 설정: 타임스탬프 일부를 사용하여 버전 필드를 0111로 설정합니다.
from uuid6 import uuid7
uuid_v7 = uuid7()
print(uuid_v7)
세 번째 섹션에 버전이 기재된 것을 볼 수 있습니다.
64비트가 타임스탬프가 되어 생성 순서대로 정렬이 가능하다는 특징이 있습니다.
018ffc72-5231-7262-bcf6-1434e89e9e0c
018ffc80-7e8b-70a9-994b-097a03bb60d1
타임스탬프가 포함되어 있어, 이를 해석해 시간으로 표시할 수 있습니다.
다음은 시간 추출 예제 코드입니다.
from uuid6 import uuid7
import datetime
def extract_timestamp_from_uuid7(uuid):
uuid_bytes = uuid.bytes
# 타임스탬프 부분을 추출
timestamp_ms = int.from_bytes(uuid_bytes[:6])
# 타임스탬프를 밀리초 단위의 Unix 타임스탬프로 해석
timestamp_s = timestamp_ms / 1000.0
# 타임스탬프를 datetime 객체로 변환
dt = datetime.datetime.fromtimestamp(timestamp_s)
return dt
uuid = uuid7()
print(uuid)
timestamp = extract_timestamp_from_uuid7(uuid)
print(timestamp)
출력 결과는 다음과 같습니다. 보시다시피 생성 시간을 확인할 수 있습니다.
018ffca2-c64b-7fef-9791-b4a3fdf06124
2024-06-09 10:54:37.131000
UUID v7의 특징은 처음 48비트가 타임스탬프로 인코딩되어 있어 UUID에서 생성 시간을 쉽게 알 수 있다는 점입니다.
따라서 생성 시간이 노출되면 안 되는 경우에는 v4를 사용하는 것이 좋습니다.
그러나 이 문제는 단순하지 않으며, 이에 대해서는 "요약" 섹션에서 다루고 있습니다.
ULID (Universally Unique Lexicographically Sortable Identifier)
ULID는 UUID의 단점을 보완하여 설계된 고유 식별자입니다. 특히, 시간 순서대로 정렬할 수 있다는 점이 큰 특징입니다.
ID 생성 방법
- 타임스탬프 가져오기: 현재 타임스탬프를 밀리초 단위로 가져와 48비트 비트열로 변환합니다.
- 무작위 값 생성: 나머지 80비트를 무작위 값으로 채웁니다.
- 인코딩: 생성된 비트열을 Crockford's Base32로 인코딩합니다.
이 생성 방법을 통해 다음과 같은 특징을 가집니다.
- 시간 순서대로 정렬 가능해 데이터 관리 및 검색이 용이합니다.
- 높은 일의성과 가독성을 가집니다.
- UUID와 비교해 짧아 URL 등에서 사용하기에 편리합니다.
import ulid
ulid_instance = ulid.new()
print(ulid_instance)
timestamp = ulid_instance.timestamp().datetime
print(timestamp)
위 결과는 다음과 같습니다. 이렇게 datetime 값을 간단히 추출할 수 있습니다.
01HZYC2028WMB3NJ16WCV9Z9E0
2024-06-09 11:27:38.056000+00:00
UUID v7과 ULID는 유사한 특징을 가지므로, 출력되는 ID의 포맷을 보고 시스템의 요구 사항에 맞춰 선택하면 됩니다.
UUID, ULID를 채택하지 않을 경우
Auto Increment의 문제로 UUID, ULID 채택을 고려하더라도, 앞서 언급했듯이 UUID, ULID에도 다른 문제가 있습니다.
다시 요약해보겠습니다.
UUID v4
- 완전히 랜덤한 값으로, 정렬이 불가능해 성능 저하
UUID v7 / ULID
- 자동 증가형 숫자와 비교해 성능이 떨어짐
- 생성 시간(타임스탬프) 노출
요약
MySQL에서는 클러스터 인덱스가 사용됩니다.
클러스터 인덱스는 키 값이 물리적으로 가까운 위치에 레코드 데이터를 배치합니다.
이를 통해 연속된 데이터가 물리적으로 근접하여 캐시 적중률이 높아지고 성능이 향상됩니다.
그러나 랜덤한 UUID를 기본 키로 사용하면, 쓰기 위치가 랜덤해져 캐시 적중률이 낮아지고 성능이 저하될 수 있습니다.
이로 인해 UUID v4는 성능 문제로 인해 사용하기 어렵습니다.
따라서 UUID v7 또는 ULID가 후보로 올라옵니다. 그러나 이들 또한 숫자형 자동 증가에 비해 성능이 떨어지고, 앞서 언급한 것처럼 타임스탬프 노출 문제가 있습니다.
이 문제를 피하기 위해, 기본 키로는 숫자형 자동 증가를 사용하고, 사용자에게 공개할 키로는 별도의 랜덤 문자열(UUID 또는 사용자 정의 랜덤 수)을 생성하여 사용하는 것이 좋습니다.
'Codings' 카테고리의 다른 글
Monorepo와 다수의 리포지토리: 소프트웨어 개발에 최적화된 방법은? (0) | 2024.09.10 |
---|---|
Tailwind CSS 속성 강좌 (0) | 2024.08.04 |
zsh 쉘에서 편리한 fish 쉘로 기본 쉘 변경하기 (0) | 2024.03.03 |
git switch와 git restore 알아보기 (0) | 2024.03.01 |
OpenAI 동영상 생성 기술 - Sora (0) | 2024.02.19 |