전역 고유 ID 생성기 성능 개선기: Go와 Kotlin 비교

저는 주로 Kotlin으로 백엔드를 개발합니다. Go의 특성을 직접 확인하기 위해 제가 관리하는 코드에서 두 언어의 구현을 비교할 만한 대상을 찾았습니다.
비교 대상은 전역 고유 ID 생성기로 정했습니다.
- ID 생성기는 호출 빈도가 높다.
- 전역에서 고유해야 한다.
- 동시 생성에서도 깨지면 안 된다.
처리 속도만으로는 충분하지 않습니다. 고유성과 동시성 안전성도 함께 만족해야 합니다. 구현 범위는 작아 보여도 요구사항이 명확하고 실패 비용이 커서 벤치마크 대상으로 적합합니다.
처음 목표는 단순했다. bluetape-go에 idgenerators를 구현하고,
기존 bluetape4k-idgenerators와 비교하는 것이었습니다. Go 구현이 원시값과 작은 구조체를 주로
사용하므로 여러 항목에서 앞설 것으로 예상했습니다.
그러나 첫 벤치마크 결과는 예상과 달랐습니다. Go도 호출할 때마다 무작위 엔트로피를 읽고 있었고,
Kotlin도 Snowflake에서 불필요한 중간 객체를 만들고 있었습니다. ULID, KSUID, KsuidMillis도
각각 다른 병목을 갖고 있었습니다. 결국 작업은 세 번의 비교로 확대되었습니다.
- Go 구현을 만들고 1차 비교를 수행했습니다.
- Go의 호출당 메모리 할당과 엔트로피 병목을 줄이고 2차 비교를 수행했습니다.
- Kotlin의 Snowflake, ULID, KSUID, KsuidMillis 병목을 줄이고 3차 비교를 수행했습니다.
이 글은 그 과정을 기록합니다. 결과를 “Go가 빠르다” 또는 “Kotlin이 이겼다”로 요약할 수는 없었습니다. 생성기 계열마다 병목이 달랐고, 한 병목을 제거하면 다음 병목이 드러났습니다.
bluetape-go의 ID 생성기 구현
섹션 제목: “bluetape-go의 ID 생성기 구현”bluetape-go의 구현은 id 패키지에 모았습니다.
- UUID v4/v7은 바로 쓰는 함수와 재사용 가능한 생성기를 모두 제공합니다.
- 무작위 ULID와 단조 증가 ULID 생성기를 제공합니다.
- KSUID는 초 단위 타임스탬프를 쓰는 기본 KSUID와 JVM 구현에 맞춘 밀리초 변형을 제공합니다.
- Snowflake는 64비트 정수 ID를 제공합니다.
현재 Go API는 생성기 구성과 ID 생성 과정에서 발생할 수 있는 오류를 반환합니다. 다음 예제는
현재 develop의 호출 형태이며, 뒤에서 설명하는 성능 수치는 2026-06-10~11에 보존한 벤치마크
리비전의 결과입니다.
ulidGenerator, err := id.NewMonotonicULIDGenerator()if err != nil { return err}ulid, err := ulidGenerator.NextString()
snowflakeGenerator, err := id.NewSnowflakeGenerator(1)if err != nil { return err}snowflake, err := snowflakeGenerator.NextInt64()소스는 bluetape-go/id와
id_benchmark_test.go에서
확인할 수 있습니다. 첫 벤치마크의 조건과 해석은
ID 생성기 1차 벤치마크 보고서에 정리했습니다.
1차 비교: 예상보다 단순하지 않았다
섹션 제목: “1차 비교: 예상보다 단순하지 않았다”1차 벤치마크에서 Go가 모든 항목을 앞서지는 않았습니다. 합성 시계로 핵심 생성 경로를 측정한 Snowflake 항목은 Go가 크게 앞섰습니다. 반대로 Kotlin의 UUID v7, 단일 스레드 단조 증가 ULID, 초 단위 KSUID는 더 낮은 지연 시간을 보였습니다.

측정 방식이 서로 다르다는 점에 주의해야 합니다. Go Snowflake 항목은 합성 시계 훅으로 핵심 생성
경로를 측정했습니다. Kotlin Snowflake 항목은 실제 시스템 시계와 kotlinx-benchmark 배치 방식을
사용했습니다. 따라서 이 수치만으로 두 언어의 운영 성능 차이를 판단할 수 없습니다.
1차 비교는 병목을 찾는 데 유용했습니다. Go 프로파일에서는 생성기 재사용만으로 차이가 크게 줄지 않았고, 무작위 엔트로피 읽기와 문자열 인코딩 비용이 더 두드러졌습니다.
Go 개선: 메모리 할당보다 엔트로피 비용이 먼저 보였다
섹션 제목: “Go 개선: 메모리 할당보다 엔트로피 비용이 먼저 보였다”Go 개선은 두 갈래로 진행했습니다.
먼저 기본 엔트로피 공급 경로를 바꿨습니다. UUID, ULID, KSUID가 ID를 생성할 때마다
crypto/rand를 직접 읽으면 시스템 호출 비용이 누적됩니다. 그래서 패키지 내부에 잠금으로 보호한
버퍼 판독기를 두고 기본 생성기가 공유하도록 했습니다. 엔트로피 원천은 계속 crypto/rand를
사용합니다. 이 최적화는 ID를 인증·인가 토큰으로 사용할 수 있다는 의미가 아닙니다.
KSUIDMillis 문자열 인코딩에서는 불필요한 임시 슬라이스를 줄였습니다. 27자 접두사를 고정된
지역 버퍼에 바로 인코딩하여 전체 문자열을 만든 뒤 잘라내던 경로를 제거했습니다.
최종 benchstat에서 기하 평균 지연 시간은 58.34% 감소했습니다. 재사용한 UUID v4 생성기는
224.10 ns/op에서 45.57 ns/op로, 밀리초 KSUID는 316.80 ns/op에서 122.80 ns/op로 낮아졌습니다.
반면 단조 증가 ULID는 65.80 ns/op에서 65.31 ns/op로 거의 변하지 않았습니다.
단조 증가 ULID는 엔트로피 버퍼링보다 단조 증가 상태 관리와 인코딩의 비중이 더 컸습니다. 하나의 최적화가 모든 ID 생성기의 병목을 제거하지는 못한다는 사실을 보여줍니다.
자세한 Go 개선 기록은
ID 생성기 성능 개선 보고서에서 확인할 수 있습니다.
2차 비교: 병목이 이동했다
섹션 제목: “2차 비교: 병목이 이동했다”Go 개선 뒤의 2차 비교에서는 양상이 달라졌습니다. Go는 UUID v4, 밀리초 KSUID, 동시 실행 ULID·KSUID 항목에서 앞섰습니다. Kotlin은 UUID v7과 단일 스레드 단조 증가 ULID에서 계속 더 낮은 지연 시간을 기록했습니다.

2차 차트의 핵심은 “Go가 빨라졌다”보다 “병목이 이동했다”는 점입니다. 무작위 엔트로피 읽기 비용을 줄이자 Go는 UUID v4와 밀리초 KSUID에서 앞서기 시작했습니다. 반면 UUID v7과 단일 스레드 단조 증가 ULID는 Kotlin이 계속 빨랐습니다. 항목별 측정 경로가 달라 한 런타임의 일반적인 우위로 해석할 수는 없습니다.
Go의 병목을 제거하고 나자 Kotlin 구현에서도 줄일 수 있는 객체 생성 경로와 엔트로피 처리 단계가 보였습니다.
Kotlin 개선: Snowflake, ULID, KSUID, KsuidMillis
섹션 제목: “Kotlin 개선: Snowflake, ULID, KSUID, KsuidMillis”Kotlin 작업은 bluetape4k-idgenerators에서 진행했습니다. 공개 API를 바꾸지 않고 생성 경로의
불필요한 객체와 무작위 페이로드 처리 단계를 줄이는 것이 목표였습니다.
Snowflake는 SnowflakeId 중간 객체를 만든 뒤 다시 Long 값을 꺼내는 경로가 있었습니다. 내부
시퀀서에 Long 값 생성 경로를 추가하여 공개 API를 유지하면서 nextId()와 nextIds(size)가
가능하면 값을 직접 받도록 바꿨습니다.
internal interface SnowflakeValueSequencer { fun nextValue(): Long}이 변화는 처리량 표에서 크게 드러나지 않았습니다. 벤치마크는 한 연산에서 ID 65,536개를 생성하지만, Snowflake 시퀀스는 밀리초마다 4,096개로 제한됩니다. 실제 시계를 사용하면 시퀀스 한계가 먼저 나타납니다. 반면 메모리 할당 프로파일에서는 차이가 명확했습니다.
| Snowflake 프로파일 | 기준값 | 후보 3 | 변화율 |
|---|---|---|---|
gc.alloc.rate.norm | 14,579,592.553 B/op | 6,970,033.101 B/op | -52.19% |
gc.alloc.rate | 868.813 MB/s | 415.382 MB/s | -52.19% |
gc.count | 6 | 3 | -50.00% |
ULID는 ByteArray(10)을 만들고 Random.nextBytes로 채우는 단계를 제거했습니다. 80비트 난수를
nextBits(16)과 nextLong()으로 직접 받아 값과 문자열 생성 경로에 전달합니다.
KSUID와 KsuidMillis는 타임스탬프를 먼저 기록한 뒤 무작위 페이로드 부분만 채우도록 바꿨습니다.
초 단위 KSUID는 4바이트 타임스탬프 뒤의 16바이트 페이로드만, 밀리초 변형은 8바이트 타임스탬프
뒤의 12바이트 페이로드만 무작위 값으로 채웁니다.
Kotlin 후보 3에서 가장 크게 개선된 항목은 동시 실행 ULID였습니다.
concurrent.ulidMonotonicString은 18.48%, 고유성까지 확인하는 항목은 29.02% 개선되었습니다.
KSUID와 KsuidMillis는 페이로드 무작위화 범위를 줄인 만큼 제한적으로 개선되었습니다.
불리한 수치도 그대로 해석해야 합니다. single.ksuidMillisDefaultString은 후보 3 전체 실행에서
낮게 나왔지만, 같은 JVM에서 반복한 표적 실행에서는 기준값보다 1.74% 낮은 수준이었습니다.
후보 2와 3 사이에 밀리초 KSUID 코드 경로가 바뀌지 않았으므로 전체 실행의 하락은 벤치마크
잡음으로 기록했습니다. 재현되지 않은 이상치를 성능 결론으로 사용해서는 안 됩니다.
3차 비교: Go와 Kotlin이 각자 다른 곳에서 앞선다
섹션 제목: “3차 비교: Go와 Kotlin이 각자 다른 곳에서 앞선다”마지막으로 당시 최적화된 Go 구현과 Kotlin 후보 3을 다시 비교했습니다. Kotlin 처리량 항목은
배치 크기 65,536을 기준으로 1e9 / (ops/s * 65_536)을 적용해 ns/id로 정규화했습니다.

KsuidMillis는 초 단위 KSUID와 타임스탬프 정밀도가 다른 별도 변형입니다. 따라서 초 단위 KSUID
결과로 성능을 추정해서는 안 됩니다. Go 개선 뒤 2차 벤치마크에서는 Go가 크게 앞섰지만, Kotlin이
페이로드만 무작위화하도록 바꾼 3차 벤치마크의 동시 생성 항목은 사실상 동률이었습니다. 단일 스레드
항목에서는 Go의 ns/id가 더 낮았습니다. 실제 서비스에서 사용할 타임스탬프 정밀도와 동시성 조건으로
다시 측정해야 합니다.
ULID도 단일 결론으로 요약하기 어렵습니다. 단일 스레드 단조 증가 항목은 Kotlin이, 동시 실행
단조 증가 항목은 Go가 더 낮은 ns/id를 기록했습니다. 단조 증가 상태를 보호하는 방식과 엔트로피·
인코딩 비용의 비중이 워크로드마다 다르기 때문입니다.
초 단위 KSUID는 Kotlin 개선 뒤 단일 스레드와 동시 실행 항목에서 모두 Kotlin의 ns/id가
낮았습니다. Snowflake는 여전히 Go 수치가 낮지만 시계 모델이 달라 직접 비교하기 어렵습니다.
Kotlin Snowflake에서 확인한 개선은 처리량 순위가 아니라 메모리 할당 감소입니다.
이번 작업의 진짜 결론
섹션 제목: “이번 작업의 진짜 결론”이번 비교에서 가장 주의해야 할 질문은 “Go와 Kotlin 중 무엇이 더 빠른가”입니다. 이 질문만으로는 벤치마크 결과를 정확히 설명할 수 없습니다.
결론은 다음과 같습니다.
- Snowflake는 시계 모델과 시퀀스 한계를 분리해 해석해야 합니다.
- Go의 UUID·ULID·KSUID 초기 병목은 생성기 재사용보다 엔트로피 읽기 비용에 가까웠습니다.
- Kotlin Snowflake는 처리량보다 메모리 할당 프로파일에서 개선이 크게 나타났습니다.
- Kotlin ULID·KSUID·KsuidMillis는 페이로드 처리 단계를 줄이면 동시 실행 성능이 달라집니다.
KsuidMillis는 별도 변형이므로 초 단위 KSUID 결과로 성능을 추정해서는 안 됩니다.
ID 생성기는 코드가 작아 벤치마크도 단순해 보입니다. 실제로는 시계, 난수, 인코딩, 고유성 검사 집합, 배치 크기가 결과에 서로 다른 영향을 줍니다. 이번 작업의 핵심 성과는 빠른 항목 몇 개가 아니라, 생성기 계열별로 어느 경로부터 측정해야 하는지 확인한 점입니다.
- Go 구현과 벤치마크:
bluetape-go/id,id_benchmark_test.go - 비교 조건과 해석:
ID 생성기 1차 벤치마크 보고서,ID 생성기 3차 비교 보고서 - Kotlin 개선 결과:
ID 생성기 자체 개선 벤치마크
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.