Android 개발/Kotlin

Kotlin 2.4 UUID 안정화로 안드 데이터 클래스에서 라이브러리 두 개 지운 후기

stackD 2026. 6. 21. 18:00
반응형

 

어제 저녁 build.gradle.kts 열어두고 의존성 두 줄을 지웠습니다. com.benasher44:uuid, 그리고 정렬용으로 붙여놨던 ulid-kotlin. RC2 시절부터 만져보던 코틀린(Kotlin) 2.4를 지난주 풀린 2.4.0 정식판으로 올렸을 뿐인데 모듈이 가벼워지더라고요.

 

KMP 공통 모듈에서 ID 타입을 고민하시던 분이라면 이번 릴리즈가 꽤 반가울 거예요. @OptIn(ExperimentalUuidApi::class) 어노테이션을 클래스마다 박아두던 자리가 그대로 사라지고, 외부 정렬 ID 라이브러리도 표준 함수 한 줄로 옮겨갈 수 있게 됐습니다.

 

Kotlin 2.4 stable UUID로 사라진 com.benasher44:uuid

 

kotlin.uuid.Uuid 클래스 자체가 Stable로 승격됐습니다. 클래스 레벨 @OptIn 이 더 이상 필요하지 않아요. 다만 한 가지 짚어둘 게 있는데요, 생성 팩토리인 Uuid.generateV4()Uuid.generateV7() 은 여전히 Experimental 상태입니다.

 

저는 ID 생성을 한 군데 유틸 객체에 몰아둔 구조라, 거기에만 @OptIn(ExperimentalUuidApi::class) 를 남겼습니다. 도메인 데이터 클래스에서는 opt-in 어노테이션이 전부 빠지니까 import 줄이 시각적으로 깔끔해져요.

 

build.gradle.kts 에서 빼낸 라인은 이거 한 줄입니다.

 

// 삭제
implementation("com.benasher44:uuid:0.8.4")

 

KMP 공통 모듈에서 com.benasher44.uuid.Uuid 였던 import 경로가 kotlin.uuid.Uuid 로 옮겨갔고, 안드와 iOS 사이드의 ID 타입이 표준 라이브러리 하나로 통합됐어요.

 

kotlin.uuid Comparable로 단축한 정렬과 ulid-kotlin 제거

이 부분이 개인적으로 가장 마음에 들었어요. UuidComparable<Uuid> 를 구현하기 때문에, 예전처럼 문자열로 변환해서 비교할 필요가 없습니다.

 

// Before
items.sortedBy { it.id.toString() }

// After
items.sortedBy { it.id }

 

기본 비교는 128비트 MSB-우선 unsigned 비교라, 결과적으로 hex 문자열의 사전식 정렬과 일치하는 구조예요. Uuid.generateV7() 로 생성한 시간 정렬 UUID는 이 순서가 생성 순서와 자연스럽게 맞물리기 때문에, 여기서 정렬 전용 ID 라이브러리를 들어낼 수 있는 자리가 나옵니다.

 

// 삭제
implementation("com.aallam.ulid:ulid-kotlin:1.3.0")

 

v7 UUID의 단조성은 단일 프로세스 라이프사이클 동안만 보장됩니다. 프로세스를 재시작하거나 멀티 노드 환경에서는 동일 밀리초 안의 순서가 흔들릴 수 있으니, 분산 환경에서 강한 순서를 요구한다면 그건 따로 설계하셔야 돼요.

 

 

Room TypeConverter와 kotlinx.serialization 자리

Room 은 Uuid 를 자동 매핑하지 않습니다. kotlin.uuid.Uuid 가 Room 의 기본 타입 목록(원시·String·ByteArray 등)에 포함되지 않은 클래스라, 컨버터를 한 번 박아주는 손이 필요해요.

 

class UuidConverter {
    @TypeConverter
    fun fromUuid(value: Uuid?): String? = value?.toString()

    @TypeConverter
    fun toUuid(value: String?): Uuid? = value?.let { Uuid.parse(it) }
}

 

KMP 호환성까지 같이 생각하면 16바이트 바이너리 저장보다 문자열 변환 쪽이 무난한 편입니다. Uuid.toString() 결과 자체가 java.util.UUID.toString() 과 동일한 8-4-4-4-12 포맷이라, 기존 데이터를 다시 마이그레이션할 필요 없이 그대로 호환됐어요.

 

kotlinx.serialization 쪽은 내장 시리얼라이저(Uuid.serializer())가 이미 제공되고, 1.7.2 버전부터는 직렬화 컴파일러 플러그인이 Uuid 필드를 자동으로 인식해 시리얼라이저를 끼워줍니다. @Serializable 만 박아도 컴파일이 그대로 통과돼요. 다중 모듈에서 컨텍스트 주입이 필요한 케이스라면 @Contextual 로 명시해두는 쪽이 깔끔합니다.

 

java.util.UUID 점진 이전과 두 가지 함정

레거시 모듈을 한 번에 다 갈아엎을 필요는 없어요. JVM 한정으로 toJavaUuid(), toKotlinUuid() 확장 함수가 제공되니까, 신규 모듈부터 kotlin.uuid.Uuid 로 받고 경계에서 변환하는 식으로 점진 이전이 가능합니다.

 

다만 common 모듈에서 java.util.UUID 를 직접 만지는 코드가 있다면 expect/actual 로 풀어야 해요. iOS 사이드에는 java.util.UUID 자체가 없으니까요.

 

마이그레이션할 때 부딪힌 함정 두 가지만 짚자면,

 

  1. v7 UUID는 타임스탬프를 그대로 품고 있어서 외부 노출 시 생성 시각을 역산할 수 있습니다. 공개 API ID는 v4, 내부 정렬용 ID는 v7로 분리하는 이중 ID 전략을 깔아두는 쪽이 안전해요.
  2. Proto/CBOR 같은 바이너리 직렬화 포맷을 쓰는 모듈에서는 16바이트 레이아웃이 미묘하게 다를 수 있습니다. 같은 ID라도 인코딩 결과가 안 맞을 수 있으니, 마이그레이션할 때 덤프 비교를 한 번은 돌리시는 게 좋아요.

 

 

라이브러리 두 줄을 지운 뒤 남은 것

라이브러리 두 줄 지우는 일이 거창한 작업은 아니지만, KMP 공통 모듈에서 ID 타입을 표준 라이브러리 하나로 통일했다는 점이 저한테는 더 컸습니다. import 경로가 단일해지고, opt-in 어노테이션이 빠지고, 정렬 코드가 한 줄 짧아지는 자잘한 정리들이 모이면 모듈 한 개의 인지 부담이 꽤 줄어들더라고요.

 

이번 글에서는 단일 프로세스에서 v7 UUID의 단조성이 어떻게 정렬을 받쳐주는지까지만 다뤘는데요, 노드별 시퀀스 비트를 끼워 넣어 분산 환경에서 단조성을 다시 세운 시도는 다음 글에서 따로 풀어볼 생각입니다. 멀티 노드 사이에서 동일 밀리초가 겹쳤을 때 어느 비트로 순서를 잡았는지, 그 자리에서 v7의 표준 레이아웃을 어디까지 건드려도 호환이 깨지지 않는지를 같이 짚어볼게요.

 

반응형
LIST