ABP(Abp.io) 기반 애플리케이션에서 GUID를 기본 키로 사용할 경우, 무작위 GUID는 인덱스 단편화를 유발하여 성능 저하를 초래할 수 있다. 이를 해결하기 위해 데이터베이스 종류에 따라 최적화된 순차적(Sequential) GUID를 생성하는 구현 방법을 소개한다.
순차 GUID 생성기 인터페이스
public interface IGuidGenerator
{
Guid Create();
}
데이터베이스별 순차 GUID 구현
using System;
using System.Security.Cryptography;
public class SequentialGuidGenerator : IGuidGenerator
{
public static readonly SequentialGuidGenerator Instance = new();
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
public DatabaseType DbType { get; set; } = DatabaseType.SqlServer;
private SequentialGuidGenerator() { }
public Guid Create() => Create(DbType);
public Guid Create(DatabaseType dbType)
{
return dbType switch
{
DatabaseType.SqlServer => Generate(SequenceType.AtEnd),
DatabaseType.Oracle => Generate(SequenceType.AsBinary),
DatabaseType.MySql or DatabaseType.PostgreSql => Generate(SequenceType.AsString),
_ => throw new NotSupportedException($"Unsupported database type: {dbType}")
};
}
private Guid Generate(SequenceType seqType)
{
byte[] randomPart = new byte[10];
_rng.Locking(r => r.GetBytes(randomPart));
long timestamp = DateTime.UtcNow.Ticks / 10_000L; // 밀리초 단위
byte[] timeBytes = BitConverter.GetBytes(timestamp);
if (BitConverter.IsLittleEndian)
Array.Reverse(timeBytes);
byte[] guidBytes = new byte[16];
switch (seqType)
{
case SequenceType.AsString:
case SequenceType.AsBinary:
Buffer.BlockCopy(timeBytes, 2, guidBytes, 0, 6);
Buffer.BlockCopy(randomPart, 0, guidBytes, 6, 10);
if (seqType == SequenceType.AsString && BitConverter.IsLittleEndian)
{
Array.Reverse(guidBytes, 0, 4);
Array.Reverse(guidBytes, 4, 2);
}
break;
case SequenceType.AtEnd:
Buffer.BlockCopy(randomPart, 0, guidBytes, 0, 10);
Buffer.BlockCopy(timeBytes, 2, guidBytes, 10, 6);
break;
}
return new Guid(guidBytes);
}
public enum DatabaseType
{
SqlServer,
Oracle,
MySql,
PostgreSql
}
private enum SequenceType
{
AsString,
AsBinary,
AtEnd
}
}
락 확장 메서드
public static class LockExtensions
{
public static void Locking<T>(this T obj, Action<T> action) where T : class
{
lock (obj)
{
action(obj);
}
}
}
사용 예시
for (int i = 0; i < 100; i++)
{
var id = SequentialGuidGenerator.Instance.Create(SequentialGuidGenerator.DatabaseType.SqlServer);
Console.WriteLine($"{id} {i} {DateTime.Now.ToFileTime()}");
Thread.Sleep(1);
}
주의할 점은 SQL Server의 경우, GUID가 문자열 형태로 출력될 때 순차적으로 보이지 않더라도, 내부적으로 uniqueidentifier 타입으로 저장 및 정렬 시 순차성을 유지한다는 것이다. 이는 SQL Server의 고유한 정렬 규칙 때문이며, 다음과 같은 특징을 가진다:
- 1~8바이트: 왼쪽에서 오른쪽으로 정렬
- 9~10바이트: 오른쪽에서 왼쪽으로 정렬
- 11~16바이트: 오른쪽에서 왼쪽으로 정렬
반면, Oracle은 RAW(16) 타입으로 바이트 배열을 저장하며 .NET의 기본 GUID 정렬과 동일하게 동작하고, MySQL 및 PostgreSQL은 CHAR(36) 형식의 문자열로 저장되므로 문자열 사전순 정렬 규칙을 따른다. 따라서 각 데이터베이스의 저장 및 정렬 방식에 맞춰 GUID의 순차 부분 위치를 조정해야 인덱스 성능을 극대화할 수 있다.