개발
home

Sqids(구. hashids)를 사용하는 방법

Created
2024/08/04
Tags
Sqids
Hashids
Encryption
2024-08-04 @이영훈

개요

Sqids(구. hashids)를 사용하여 보안과 편의성을 높였습니다.
관계형 DB에서 ID를 숫자(bigint) + auto increment로 관리하는 경우가 있습니다. 이 때 특정 계정의 아이디가 10,000이라는 것이 노출되면 다음은 10,001번이라는 쉬운 추측이 가능한 문제가 있습니다.
유튜브는 비디오의 아이디를 string 타입으로 표시하고 있습니다. 그래서 내부에서는 number(Long) 타입으로 관리하고 클라이언트에게 string 타입으로 표현해서 내부의 보안을 높이고, DB에서는 bigint로 인덱스 속도를 빠르게 가져가고 싶었습니다.
이를 위해서 sqids를 도입했습니다. sqids를 Kotlin + Spring Boot에 적용한 방법을 기록으로 남깁니다.

본문

Sqids 알아보기

Sqids 라이브러리는 숫자 ID를 짧고 사람이 읽기 쉬운 문자열로 변환하는 도구입니다.
암호화 라이브러리이기 때문에 숫자 ID와 변환된 문제 ID를 암/복호화 할 수 있습니다.
Javascript 예시 코드
const { encode, decode } = require('@sqids/sqids'); const sqids = new Sqids(); // ✅ 숫자 ID -> 문자 ID로 만듦 (암호화) const id = sqids.encode([12345]); console.log(id); // 출력 예: "NkK9" // ✅ 문자 ID -> 숫자 ID로 만듦 (복호화) const numbers = sqids.decode(id); console.log(numbers); // 출력: [12345]
JavaScript
복사

Sqids를 Kotlin Spring Boot 프로젝트에 적용하기

프로젝트에 다음 정책을 설정하였습니다.
1.
서버 외부로 들어오고 나가는 ID는 모두 String 타입이다
2.
서버 내부에서는 ID는 모두 Number(Long) 타입이다
이를 위해서 다음 전략을 구상하였습니다.
1.
String 타입의 ID를 uid로 명명하였고, Number 타입의 ID를 id로 명명하였습니다
2.
DTO에서 ID를 String으로 만드는 어노테이션을 만들었습니다
3.
Controller에서는 uid를 받아서 id로 변환해서 service로 전달합니다

DTO에서 사용할 @IdUidConverter 만들기

Response/Request DTO에서 id 필드에 적용할 @IdUidConverter를 만듭니다
저는 Sqids의 구버전인 hashids를 사용했습니다 신버전인 Sqids도 코드는 동일합니다
다음과 같이 두 확장함수 toId(), toUid() 를 만듭니다
private val hashids = Hashids(salt = "your-salt", minHashLength = 8) fun String.toId() = hashids.decode(this).getOrNull(0) ?: throw RuntimeException("Incorrect Uid: $this") fun Long.toUid(): String = hashids.encode(this)
Kotlin
복사
이어서 UidToIdDeserializerIdToUidSerializer 를 만듭니다
UidToIdDeserializer 는 RequestDTO에서 uid를 id로 변경해주는 역할을 합니다
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import me.leedo.common.iduid.toId class UidToIdDeserializer : JsonDeserializer<Long>() { // String? -> Long? override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long { return p.valueAsString.toId() } }
Kotlin
복사
IdToUidSerializer 는 ResponseDTO에서 id를 uid로 변경해주는 역할을 합니다
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider import me.leedo.common.iduid.toUid class IdToUidSerializer : JsonSerializer<Long>() { // Long? -> String? override fun serialize(value: Long?, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeString(value?.toUid()) } }
Kotlin
복사
최종적으로 IdUidConverter를 만듭니다
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import me.leedo.common.iduid.serializer.IdToUidSerializer import me.leedo.common.iduid.serializer.UidToIdDeserializer import io.swagger.v3.oas.annotations.media.Schema @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented @JacksonAnnotationsInside @JsonDeserialize(using = UidToIdDeserializer::class) // @JsonSerialize(using = IdToUidSerializer::class) @Schema(type = "string") // swagger에서 string 타입으로 표시되기 위해서 붙입니다 annotation class IdUidConverter
Kotlin
복사
@JacksonAnnotationsInside는 Jackson 라이브러리에서 사용자 정의 어노테이션을 만들 때 사용되는 메타 어노테이션입니다. 이 어노테이션을 사용하면, 사용자 정의 어노테이션이 Jackson의 어노테이션처럼 동작하게 할 수 있습니다.
@JsonSerialize 는 객체를 JSON 형식으로 직렬화할 때 사용자 정의 로직을 지정합니다.
@JsonDeserialize 는 JSON을 객체로 역직렬화할 때 사용자 정의 로직을 지정합니다.
DTO의 id 필드에 IdUidConverter 를 적용하면 자동으로 iduid로 변경해줍니다
예시 코드를 보면서 확인해보겠습니다
Response에 사용되는 UserDTO입니다
User는 식별 아이디와 이름과 이메일, 소속 팀을 가지고 있습니다
class UserDTO( @IdUidConverter // ⭐️ IdUIdConverter가 적용됨 val id: Long, // Number(Long) 타입의 ID val name: String, val email: String, @IdUidConverter val teamId: Long, )
Kotlin
복사
브라우저나 HTTP 클라이언트 (curl, postman 등)에서는 다음과 같이 응답이 json으로 직렬화되어 응답됩니다
{ "id": "ohmH9t28", // ✅ String 타입의 ID로 변환되어 응답됨 "name": "Leedo", "email": "user1@leedo.me", "teamId": "Liom4HYP" }
JSON
복사
Request에 사용되는 UpdateUserDTO입니다
이름과 이메일, 소속팀을 변경할 수 있는 API의 요청 body 값을 정의하고 있습니다
class UpdateUserDTO( val name: String, val email: String, @IdUidConverter val teamId: Long, )
Kotlin
복사
브라우저나 HTTP 클라이언트 (curl, postman 등)에서는 다음처럼 json으로 요청하면 됩니다
{ "name": "Leedo2", "email": "user2@leedo.me", "teamId": "5f0b5b3b" // ✅ String 타입의 ID로 요청합니다 }
JSON
복사