UUID 를 쓸 것이냐? ID 를 쓸 것이냐? 혹은 둘 다 쓸 것이냐?
오래된 논쟁이지만, 모든 상황에서 절대적으로 옳은 선택지가 존재하지 않는 논쟁이라고 생각하기에, 무의미한 논쟁에서 벗어나
내 시스템에는 어떤 선택지가 좋을까? 에 대해 같이 고민해봅시다 :)
들어가기전에
auto increment ID 는 DB 에서 PK 로 자주 사용됩니다.
하지만 ID 만 사용하게 되면 보안 문제가 생길 수 있습니다. 예를 들어, 일련번호와 같은 연속된 ID를 사용하는 경우, 공격자가 다른 사용자의 데이터에 접근할 가능성이 있습니다. 반면, UUID는 범용 고유 식별자로 128비트(아닌 경우도 있는데, 아래에서 다루겠습니다) 크기의 "거의" 중복되지 않는 값을 생성합니다.
ID 의 단점
ID 의 단점은 앞서 설명드린것 처럼, 보안상의 문제 뿐만 아니라 다른 문제들도 존재합니다.
대부분의 데이터베이스 시스템에서 ID 는 일반적으로 해당 DB 내에서만 고유하게 관리되는 값입니다. 즉, 서로 다른 DB 혹은 테이블에서는 같은 ID 값을 가진 데이터가 존재할 수 있습니다.
예를 들어, 데이터베이스 A와 B를 마이그레이션하는 상황을 가정해보겠습니다.
A에는 ID가 1인 데이터가 있고, B에도 ID가 1인 데이터가 있다면, 이 두 데이터를 가져올 때 데이터가 중복되는 문제가 발생합니다. 이는 ID collision 이라고 부르며, 이를 해결하기 위한 추가적인 작업이 필요합니다.
반면, UUID는 이러한 문제를 해결할 수 있습니다. UUID는 전역적으로 고유한 값을 가지므로, 서로 다른 데이터베이스 혹은 테이블에서 같은 UUID 값을 가진 데이터가 존재할 가능성이 극히 낮습니다. 따라서 UUID 를 사용하면, 데이터베이스 간의 데이터 이동이나 병합 시 collision 없이 안전하게 이동할 수 있습니다.
UUID 의 단점
그렇다면 UUID 의 단점은 없을까? 하면 크게 2가지를 꼽을 수 있을 것 같습니다.
- 크기
auto increment 에 비해 저장 공간이 더 많이 필요합니다. 이것은 결국 디스크 공간 사용량을 증가시키며, 인덱스 크기 역시 증가시킵니다. 더 큰 인덱스는 메모리 사용량을 증가시키고, 쿼리 성능을 저하시킬 수 있습니다. - 랜덤성
랜덤성이 왜 문제가 되지? 좋은거 아냐? 라고 의문을 품으실 수 있는데 "정렬될 수 없는 랜덤성"은 문제가 될 수 있습니다.
새로운 UUID가 데이터베이스의 여러 위치에 분산되게 만들며, 이는 clustered index 에서 특히 문제가 될 수 있습니다. 새로운 row 가 계속 랜덤한 위치에 삽입되면, DB 는 페이지 분할을 수행할 것이고, 이는 성능에 부정적인 영향을 미칠 수 있습니다.
또한, 이로 인해 WAL 에 많은 데이터가 쌓이게 되어 복구 시간이 증가하게 됩니다.
해결 방법은 없을까?
- 시간 기반 UUID 사용 : UUID v1 이나 v6 같은 시간 기반의 UUID 를 사용하면, 생성된 UUID 가 시간 순서대로 정렬되므로, 새로운 row 가 데이터베이스의 동일한 위치에 삽입됩니다. (인덱스와 디스크 공간의 사용량을 최적화하는 데에도 도움이 됨)
- 복합 키 사용 : UUID 와 시퀀스 번호 또는 타임스탬프를 결합하여 복합 키를 사용하는 방법입니다. 이렇게 하면 쿼리 성능을 개선하고, 동시에 UUID의 장점을 유지할 수 있습니다.
- UUID 압축 : UUID 를 바이너리 형식으로 저장하여 저장 공간을 줄일 수 있습니다. 이렇게 하면 UUID 의 크기를 16바이트로 줄일 수 있습니다 (물론 바이너리를 다시 핸들링 해줘야 하는 경우 비용 발생) (이외에도 base64uid 같은 방법도 있는데, 밑에서 자세하게 다루겠습니다)
UUID 와 ID 를 같이 쓴다면?
ID와 UUID를 함께 사용하면, 복잡도가 높아지는 것은 사실입니다. 하지만 감당하기 힘들 정도는 아니며, 서비스가 성장하고 나서 마이그레이션을 할 때도 편리합니다.
예를 들어, 서비스가 성장하면서 데이터베이스 시스템을 변경하거나 분산 시스템으로 전환해야 할 때가 있습니다. 이런 경우, UUID 를 사용하면 ID collision 부담감이 줄어들고, UUID 는 전체 시스템에서 고유함을 보장하기 때문에, 여러 데이터베이스나 서비스에서 데이터를 병합하거나 이동시키는 것이 더욱 용이해집니다.
따라서 저희는 ID 만 사용하는것이 아닌 UUID 혹은 ID 와 UUID 를 모두 사용하는 선택지로 진행해봅시다 :)
(우선, UUID 종류를 살펴보고 그 다음 상황마다 어떤 선택지가 좋을지 살펴볼게요!)
UUID 의 종류
UUIDv1 ~ UUIDv8, ULID, snowflake, baidu 등 다양한 선택지가 있습니다.
하나씩 차례로 살펴봅시다 : )
UUIDv1
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_mid | time_hi_and_version |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res | clk_seq_low | node (0-1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2-5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
이 버전의 UUID는 MAC 주소와 현재 시간을 결합하여 생성됩니다. 이 방식의 장점은 시간을 기반으로 하므로 시간 순서대로 UUID를 생성할 수 있습니다. (배치로 UUID 를 생성해야 하는 경우에는 카운터(counter)를 사용하여 각각의 UUID가 유일하게 생성되도록 할 수 있습니다)
UUIDv2
이 버전은 POSIX UID와 GID를 포함하여 생성됩니다. 일반적으로는 잘 사용되지 않기에 넘어가겠습니다
UUID v3
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| md5_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| md5_high | ver | md5_mid |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| md5_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| md5_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
이 버전은 MD5 해시와 이름 공간 (예: URL, 도메인 이름, 등등)을 결합하여 이름 기반의 UUID를 생성합니다.
따라서, UUID v3는 항상 같은 입력에 대해 같은 UUID를 생성합니다. (마찬가지로 자주 사용되지 않기에 넘어가겠습니다)
UUID v4
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random_a | ver | random_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| random_c |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random_c |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
이 버전은 난수를 기반으로 UUID를 생성합니다. 이로 인해 생성된 UUID 는 거의 고유하며, 임의성이 높습니다(물론, 이론적으로는 충돌 가능성 존재)
UUID v5
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| sha_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| sha_high | ver | sha_mid |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| sha_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| md5_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
이 버전은 UUID v3 와 비슷하지만, SHA-1 해시 알고리즘을 사용하여 생성됩니다.
(UUIDv3 와 마찬가지로 같은 입력에 대해 항상 같은 UUID 를 생성합니다)
UUID v6
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_mid | time_low_and_version |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res | clk_seq_low | node (0-1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2-5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
이 버전은 UUIDv1 과 유사한 방식을 사용하지만 더 표준화된 방식을 사용합니다.
바로, 타임스탬프를 UUID 의 시작 부분에 배치하여 시간 순서대로 정렬할 수 있게 해줍니다. 이 점이 바로 v1 과의 가장 큰 차이점으로 이를 통해 DB 에서 효율적인 인덱싱이 가능합니다.
UUIDv7
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
이 방식은 비교적 최근에 제안된 UUID 입니다. 1582년 10월 15일 00:00:00.00 부터 100 나노초 단위로 계산하는 대신, 1970년 1월 1일 0시(UTC)부터 밀리초 단위로 계산하는 유닉스 시간을 사용합니다 (따라서 유닉스 시간을 사용하는것이 장점이자 단점)
구조는 다음과 같습니다.
- 처음 48비트는 유닉스 시간을 밀리초 단위로 표현한 것입니다.
- 다음 4비트는 버전 비트로서 (0111), 이어서 12비트는 pseudo-random 데이터입니다.
- 다음은 2비트 변형 비트, 마지막으로 62비트는 pseudo-random 데이터입니다.
UUIDv8
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| custom_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| custom_a | ver | custom_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| custom_c |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| custom_c |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
UUIDv8 는 UUIDv4 와 비슷한 방식을 사용하지만, 더 많은 사용자 정의 옵션을 제공합니다. UUIDv8 의 122비트 중 일부는 시간 기반으로 생성되지만, 나머지 비트는 사용자가 원하는 방식으로 생성할 수 있습니다.
(사용자 정의 옵션이 많기 때문에, 특정 어플리케이션의 요구 사항에 맞게 UUID를 생성할 수 있습니다. 하지만 결국 어플리케이션 간에 UUIDv8 의 구현이 일관되지 않을 수 있다는 단점으로 작용할 수 있음)
구조는 다음과 같습니다
- 48비트는 사용자 정의 구현에 따라 달라집니다.
- 다음 4비트는 버전 비트로서 (1000), 이어서 12비트는 사용자 정의 구현에 따라 달라집니다.
- 다음은 2비트 변형 비트, 마지막으로 62비트는 사용자 정의 구현에 따라 달라집니다.
Snowflake
Snowflake 는 분산 시스템의 특성을 최대한 활용하기 위해 설계되었습니다.
Snowflake ID는 64비트로 구성되며, 시간 순서, 서버 ID, 그리고 시퀀스 숫자를 포함합니다. 이러한 방식은 분산 시스템에서 동시에 생성되는 ID 의 중복을 방지하고 일관성을 유지합니다.
구조는 다음과 같습니다
- 앞 41 비트는 밀리세컨드 단위의 타임스탬프입니다.
- 다음 10 비트는 워커 노드 또는 데이터 센터의 식별자입니다.
- 마지막 12 비트는 같은 타임스탬프에서 생성될 수 있는 연속된 ID 를 구분하기 위한 시퀀스 번호입니다.
주요 장점
- 분산 환경 지원 : 각 노드는 독립적으로 ID를 생성할 수 있으며, 이 ID는 전체 시스템에서 고유합니다.
- 충돌방지 : worker ID 와 시퀀스 번호를 결합하여 각 ID가 시스템 전체에서 고유함을 보장합니다
- 64비트 정수 ID : 64비트 정수 ID를 생성합니다. 이는 저장 공간을 효율적으로 활용하며, 정수 기반의 ID는 처리가 빠르고, 데이터베이스에서 쿼리를 수행하기도 편리합니다.
- 순차적 증가 : Snowflake에 의해 생성된 ID는 (특히 시간 부분에 대해) 크게 보면 순차적으로 증가합니다. 이는 데이터베이스 인덱스의 효율성을 향상시키는데 도움이 됩니다.
- 고성능과 확장성 : 초당 수만 건의 ID를 생성할 수 있으며, 서비스의 확장성을 유지하면서 더 많은 요청을 처리할 수 있습니다.
- 시간 역행 문제 방지 : 시스템 시계가 뒤로 가는 경우를 처리하기 위한 로직을 포함하고 있기에, "시간 역행" 문제를 방지합니다.
Snowflake의 장점은 고유하며 시간 순서대로 생성되는 ID 를 빠르게 생성할 수 있다는 것입니다. 또한, 분산 시스템에서 사용하기 위해 설계되어 동시성 문제를 잘 처리합니다.
마지막으로 간단한 예제 코드를 살펴보고 넘어가겠습니다 : )
예제 코드
private static final int EPOCH_BITS = 41;
private static final int NODE_ID_BITS = 10;
private static final int SEQUENCE_BITS = 12;
private static final long MAX_NODE_ID = (1L << NODE_ID_BITS) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
private static final long DEFAULT_CUSTOM_EPOCH = 1680000000000L;
private volatile long lastTimestamp = -1L;
private volatile long sequence = 0L;
private final long nodeId;
private final long customEpoch;
public Snowflake(long nodeId, long customEpoch) {
if (nodeId < 0 || nodeId > MAX_NODE_ID)
throw new IllegalArgumentException(String.format("invalid-node-id : node id must be between %d and %d", 0, MAX_NODE_ID));
this.nodeId = nodeId;
this.customEpoch = customEpoch;
}
public Snowflake(long nodeId) {
this(nodeId, DEFAULT_CUSTOM_EPOCH);
}
public Snowflake() {
this.nodeId = createNodeId();
this.customEpoch = DEFAULT_CUSTOM_EPOCH;
}
public synchronized long nextId() {
long currentTimestamp = timestamp();
if (currentTimestamp < lastTimestamp)
throw new IllegalStateException("invalid-system-clock");
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0)
currentTimestamp = waitNextMillis(currentTimestamp);
} else
sequence = 0;
lastTimestamp = currentTimestamp;
return currentTimestamp << (NODE_ID_BITS + SEQUENCE_BITS)
| (nodeId << SEQUENCE_BITS)
| sequence;
}
private long timestamp() {
return System.currentTimeMillis() - customEpoch;
}
private long waitNextMillis(long currentTimestamp) {
while (currentTimestamp == lastTimestamp)
currentTimestamp = timestamp();
return currentTimestamp;
}
private static long createNodeId() {
long nodeId;
try {
StringBuilder sb = new StringBuilder();
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
byte[] mac = networkInterface.getHardwareAddress();
if (mac != null)
for (byte macPort : mac)
sb.append(String.format("%02X", macPort));
}
nodeId = sb.toString().hashCode();
} catch (Exception ignored) {
nodeId = new SecureRandom().nextInt();
}
return nodeId & MAX_NODE_ID;
}
public long[] parse(long id) {
long maskNodeId = ((1L << NODE_ID_BITS) - 1) << SEQUENCE_BITS;
long maskSequence = (1L << SEQUENCE_BITS) - 1;
long timestamp = (id >> (NODE_ID_BITS + SEQUENCE_BITS)) + customEpoch;
long nodeId = (id & maskNodeId) >> SEQUENCE_BITS;
long sequence = id & maskSequence;
return new long[] {timestamp, nodeId, sequence};
}
@Override
public String toString() {
return "My Custom Snowflake [EPOCH_BITS=" + EPOCH_BITS + ", NODE_ID_BITS=" + NODE_ID_BITS
+ ", SEQUENCE_BITS=" + SEQUENCE_BITS + ", CUSTOM_EPOCH=" + customEpoch
+ ", NodeId=" + nodeId + "]";
}
Baidu의 UID Generator
이는 Baidu 에서 개발한 분산 시스템용 uuid generator 로, 시간 기반의 생성 전략을 사용합니다.
구조는 다음과 같습니다
- 앞 28비트는 시간을 초 단위로 표현합니다.
- 다음 22비트는 워커 노드의 식별자입니다.
- 마지막 13비트는 같은 초에 생성될 수 있는 연속된 ID 를 구분하기 위한 시퀀스 번호입니다.
이러한 방식은 고유하고 시간 순서대로 생성하며, 분산 시스템에서 동시성 문제를 잘 처리할 수 있지만, (시간 기반이기에 당연히) 시간 동기화에 대한 요구사항이 있으며(사실 분산시스템에서 시간 동기화는 정말 어려운 문제입니다. 이 내용도 언제 기회가 되면...) 초 단위로 ID를 생성하기 때문에 초당 ID 생성 수에 제한이 생길 수 있습니다.
또한 RingBuffer 를 사용하면 CPU 캐시 매커니즘으로 인해, 읽기 성능을 향상시킬 수 있습니다. 하지만 False Sharing 이라는 사이드 이팩트도 존재하는데, baidu 는 이 문제를 해결하기 위해 캐시 라인 패딩을 적용했습니다.
+ Baidu uid generator 는 비록 snowflake 에 비해 복잡도가 높지만, 여기에 미처 다 소개하지 못한 여러 재미있는 아이디어들이 많아, 꼭 한번 읽어보시길 바랍니다 :)
이제까지 다양한 선택지들을 살펴보았습니다. 이러한 방법 중 어느 것을 선택할지는 여러 가지 요인을 고려해야 합니다.
주요 요인으로는 시스템 별로 요구사항이 있을 수 있고, 분산 처리 요구사항, 생성 속도, ID 의 시간 순서 등이 있는데,
아래 상황별 유즈케이스를 통해 살펴봅시다 :)
상황별 유즈케이스
만약, UUID 의 길이가 부담이라면?
원칙적으로는 RFC 2616 (Hypertext Transfer Protocol HTTP / 1.1) 섹션 3.2.1 에 따라
HTTP 프로토콜은 URI 길이에 대한 사전 제한을 두지 않습니다. 서버는 제공하는 모든 리소스의 URI 를 처리할 수 있어야하며 이러한 URI 를 생성 할 수있는 GET 기반 양식을 제공하는 경우 무제한 길이의 URI 를 처리 할 수 있어야합니다 (SHOULD). URI가 서버가 처리 할 수있는 것보다 길면 서버는 414 (Request-URI Too Long) 상태를 반환해야합니다 (섹션 10.4.15 참조).
와 같지만, 사실 대부분의 브라우저는 2000(2048, 2083 등) 자 제약이 존재합니다.
이 제약조건이 애매하게 걸쳐져 있는 경우엔 base64uid 와 같은 선택지도 고려해볼 수 있습니다.
monolithic 인 상황이라면?
사실 모놀리식 서버를 운영중인 상황이라면 문제는 심플한 편이기에 선택지가 많아집니다.
snowflake, baidu 와 같은 선택지가 아니더라도 기본 UUID 로도 충분한 경우가 많기에 니즈를 쪼개서 살펴볼게요!
- (흔치는 않지만) 아주 높은 속도로 UUID 를 생성해야하는 경우
-> UUIDv4 (랜덤 기반)를 사용하는 것이 가장 간단하고 효율적일 수 있습니다. (다만, 위에서 설명 드린 것처럼 성능 문제가 있음) - 서비스가 급속도로 성장하는 경우
서비스가 급격히 성장하면서 데이터의 삽입 속도가 빨라질 경우, UUIDv7(여기에서도 UUIDv7 을 권장하고 있음) 또는 ULID 와 같은 시간 기반의 UUID 스킴을 고려해 볼 수 있습니다. 위 스킴은 생성된 순서대로 UUID를 부여하므로, 삽입 시 인덱스 재조정을 최소화하고 성능을 향상시킬 수 있습니다. (이 경우, 배치로 UUID 를 생성해야 하는 경우에는 카운터(counter)를 사용하여 각각의 UUID가 유일하게 생성되도록 할 수 있습니다)
분산 환경인 경우
분산 환경인 경우 고려해야 할 제약조건들이 많아집니다. 물론 Baidu 도 괜찮은 선택지일 수 있지만, 개인적으로는 Snowflake 가 제너럴한 케이스에서 더 괜찮은 선택지라고 생각합니다. 앞선 내용을 토대로 봤을 때, UUIDv1 혹은 v6 의 경우 발생할 수 있는 (시간 역행 문제를 비롯한 몇가지) 문제점들을 snowflake 가 우아한 방법으로 해결했기에 가장 많이 선택되고 있습니다.
(+ CharSyam 님께서 Snowflake 를 직접 구현해서 쓰셨다는 생생한 경험담을 얘기해주셨는데,
직접 인프라운영할 때와 aws 에서 돌리는게 운영 차이가 있기도하고,
중앙 집중식 보다 차라리 겹칠 경우 fail & retry 가 더 나았다는 경험담을 전해주셨다
귀중한 경험담을 공유해주신님께 다시 한번 감사 말씀 드립니다)
참고자료
- https://azure.microsoft.com/en-us/blog/uniqueidentifier-and-clustered-indexes/
- https://blog.daveallie.com/ulid-primary-keys
- https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-00.html
- https://blog.devgenius.io/7-famous-approaches-to-generate-distributed-id-with-comparison-table-af89afe4601f
혹시 틀린 부분이 있다면, 언제든 편하게 지적해주세요!
이메일 ian.ilminmoon@gmail.com
'Simple Topics' 카테고리의 다른 글
A Brief Introduction to Modern Password Hashing: Argon2 Variants and Balloon Algorithm (0) | 2023.09.19 |
---|---|
MySQL 8.1 출시 (Innovation Release와 LTS 알아보기) (1) | 2023.07.19 |
JDBC 살펴보기 (JDBC communication internals with postgresql) (1) | 2023.05.14 |