Raw UUID 사용 중단: Rust에서 타입 안전하고 접두사된 ID (Stripe 스타일)
Source: Dev.to
우리는 모두 그런 경험을 해봤습니다. 새벽 2시에 서버 로그를 들여다보며 요청을 디버깅하려고 하는데, 다음과 같은 로그가 보입니다:
Processing request for ID: 550e8400-e29b-41d4-a716-446655440000
그게 사용자 ID일까요? 주문 ID일까요? API 키일까요? 누가 알겠어요—그냥 헥스 문자열 덩어리일 뿐이죠.
더 심각하게는, 다음과 같은 함수를 작성할 수도 있습니다:
fn process_payment(user_id: Uuid, order_id: Uuid) { /* … */ }
실수로 process_payment(order_id, user_id)를 호출하면, 컴파일러는 이를 막아주지 않습니다. 두 인자가 모두 Uuid이기 때문에, 데이터베이스가 “레코드를 찾을 수 없습니다”라는 오류를 내기 전까지는 실수를 알 수 없으며—더 나아가 데이터를 손상시킬 수도 있습니다.
Stripe‑style 솔루션
Stripe의 ID는 자체 설명적입니다 (cus_018…, ch_018…). 읽기 쉽고 타입‑안전합니다.
저는 Rust에서도 같은 경험을 원했지만, 컴파일 타임에 안전성을 확보하고 싶었습니다. 그 결과가 puuid입니다.
puuid(Prefixed UUID)는 표준 uuid 라이브러리를 감싸는 가벼운 크레이트이며 다음을 제공합니다:
- 가독성 – ID가 단순 숫자가 아니라
"user_018c…"와 같이 출력됩니다. - 타입 안전성 –
UserId와OrderId를 혼동할 수 없습니다.
프리픽스 정의하기
use puuid::{Puuid, prefix};
// 1. 프리픽스를 정의합니다
prefix!(User, "user");
prefix!(Order, "ord");
// 2. 강한 타입을 생성합니다
pub type UserId = Puuid;
pub type OrderId = Puuid;
컴파일‑타임 보호
fn delete_order(id: OrderId) {
println!("Deleting order: {}", id);
}
fn main() {
let user_id = UserId::new_v7();
// ❌ COMPILE ERROR: expected `OrderId`, found `UserId`
delete_order(user_id);
}
이제 Rust 컴파일러가 실수로 인자를 뒤바꾸는 것을 방지합니다.
구현 세부 사항
Puuid는uuid::Uuid를 감싸는#[repr(transparent)]래퍼입니다.- 메모리 – 표준 UUID와 정확히 같은 크기(16 바이트)입니다.
- 데이터베이스 –
.into_inner()를 사용해 UUID 그대로 삽입하거나, 프리픽스를 보이게 하려면 텍스트 형태로 저장합니다. - 메서드 –
Deref를 구현해uuid크레이트의.as_bytes(),.get_version()등을 그대로 사용할 수 있습니다.
기본은 UUID v7
아직 v4(무작위)에서 v7으로 전환하지 않았다면, 전환을 권장합니다. v7 UUID는 시간 정렬 가능하므로, 데이터베이스에서 생성 시간 기준으로 자연스럽게 인덱싱되어 인덱스 파편화를 줄이고 대용량 테이블 삽입 속도를 높입니다.
let id = UserId::new_v7();
// Output: user_018c6427-4f30-7f89-a1b2-c3d4e5f67890
JSON API 연동
JSON API(Axum, Actix 등)를 구축할 때, puuid는 (de)serialization을 자동으로 처리합니다:
#[derive(Serialize, Deserialize)]
struct Checkout {
id: OrderId,
customer: UserId,
}
클라이언트 JSON
{
"id": "ord_018...",
"customer": "user_018..."
}
클라이언트가 원시 UUID를 보내거나 프리픽스를 혼동하면, 역직렬화 단계에서 검증 오류가 발생합니다.
링크
- Crates.io:
- Docs:
- GitHub:
API 설계에 대한 의견을 자유롭게 공유해주세요! 🦀