メインコンテンツにスキップ

4件の投稿件の投稿が「kotlin」タグ付き

すべてのタグを見る

[システムデザインインタビュー] URL短縮サービスをゼロから実装する

· 7分の読み時間
Haril Song
Owner, Software Engineer at 42dot

banner

情報

コードはGitHubで確認できます。

概要

URLの短縮は、もともとメールやSMSでURLが分断されるのを防ぐために始まりました。しかし、現在ではTwitterやInstagramなどのソーシャルメディアプラットフォームで特定のリンクを共有するためにより積極的に使用されています。URLが冗長に見えないことで可読性が向上し、リダイレクト前にユーザーの統計情報を収集するなどの追加機能も提供できます。

この記事では、URL短縮サービスをゼロから実装し、その仕組みを探ります。

URL短縮サービスとは?

まず、結果を見てみましょう。

この記事で実装するURL短縮サービスは、以下のコマンドで直接実行できます。

docker run -d -p 8080:8080 songkg7/url-shortener

使用方法は簡単です。短縮したい長いURLをlongUrlの値として入力します。

curl -X POST --location "http://localhost:8080/api/v1/shorten" \
-H "Content-Type: application/json" \
-d "{
\"longUrl\": \"https://www.google.com/search?q=url+shortener&sourceid=chrome&ie=UTF-8\"
}"
# ランダムな値(例:tN47tML)が返されます。

次に、http://localhost:8080/tN47tMLにアクセスすると、

image

元のURLに正しくリダイレクトされることが確認できます。

短縮前

短縮後

では、どのようにしてURLを短縮するのか見てみましょう。

大まかな設計

URLの短縮

  1. longUrlを保存する前にIDを生成します。
  2. IDをbase62にエンコードしてshortUrlを作成します。
  3. ID、shortUrl、およびlongUrlをデータベースに保存します。

メモリは有限で比較的高価です。RDBはインデックスを通じて迅速にクエリを実行でき、メモリに比べて比較的安価なので、URLの管理にはRDBを使用します。

URLを管理するためには、まずID生成戦略を確保する必要があります。ID生成にはさまざまな方法がありますが、ここでは長くなるため省略します。今回は単純に現在のタイムスタンプを使用してIDを生成します。

Base62変換

ULIDを使用すると、タイムスタンプを含む一意のIDを生成できます。

val id: Long = Ulid.fast().time // 例:3145144998701、プライマリキーとして使用

この数値をbase62に変換すると、次のような文字列になります。

tN47tML

この文字列はshortUrlとしてデータベースに保存されます。

idshortlong
3145144998701tN47tMLhttps://www.google.com/search?q=url+shortener&sourceid=chrome&ie=UTF-8

取得プロセスは次のように進行します:

  1. localhost:8080/tN47tMLにGETリクエストを送信します。
  2. tN47tMLをbase62からデコードします。
  3. プライマリキー3145144998701を取得し、データベースをクエリします。
  4. リクエストをlongUrlにリダイレクトします。

これで大まかな流れを見たので、実装して詳細を掘り下げていきましょう。

実装

前回の記事「Consistent Hashing」と同様に、自分で実装します。幸いなことに、URL短縮サービスの実装はそれほど難しくありません。

モデル

まず、ユーザーからのリクエストを受け取るモデルを実装します。短縮するURLのみを受け取るように構造を簡略化しました。

data class ShortenRequest(
val longUrl: String
)

POSTリクエストを処理するためのコントローラーを実装します。

@PostMapping("/api/v1/shorten")
fun shorten(@RequestBody request: ShortenRequest): ResponseEntity<ShortenResponse> {
val url = urlShortenService.shorten(request.longUrl)
return ResponseEntity.ok(ShortenResponse(url))
}

Base62変換

最後に、最も重要な部分です。IDを生成した後、base62にエンコードして短縮します。この短縮された文字列がshortUrlになります。逆に、shortUrlをデコードしてIDを見つけ、それを使用してデータベースをクエリし、longUrlを取得します。

private const val BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

class Base62Conversion : Conversion {
override fun encode(input: Long): String {
val sb = StringBuilder()
var num = BigInteger.valueOf(input)
while (num > BigInteger.ZERO) {
val remainder = num % BigInteger.valueOf(62)
sb.append(BASE62[remainder.toInt()])
num /= BigInteger.valueOf(62)
}
return sb.reverse().toString()
}

override fun decode(input: String): Long {
var num = BigInteger.ZERO
for (c in input) {
num *= BigInteger.valueOf(62)
num += BigInteger.valueOf(BASE62.indexOf(c).toLong())
}
return num.toLong()

}
}

短縮されたURLの長さはID番号のサイズに反比例します。生成されたID番号が小さいほど、URLを短くすることができます。

短縮されたURLの長さが8文字を超えないようにするには、IDのサイズが62^8を超えないようにする必要があります。したがって、IDの生成方法も重要です。前述のように、この記事では内容を簡略化するためにタイムスタンプ値を使用しました。

テスト

curlを使用してランダムなURLを短縮するためのPOSTリクエストを送信してみましょう。

curl -X POST --location "http://localhost:8080/api/v1/shorten" \
-H "Content-Type: application/json" \
-d "{
\"longUrl\": \"https://www.google.com/search?q=url+shortener&sourceid=chrome&ie=UTF-8\"
}"

http://localhost:8080/{shortUrl}にアクセスして、正しくリダイレクトされることを確認できます。

結論

改善の余地がある点:

  • ID生成戦略をより正確に制御することで、shortUrlをさらに短縮できます。
    • トラフィックが多い場合、同時実行性に関連する問題を考慮する必要があります。
    • Snowflake
  • ホスト部分にDNSを使用することで、URLをさらに短縮できます。
  • 永続化レイヤーにキャッシュを適用することで、応答速度を向上させることができます。

[Kotlin] 中置関数

· 2分の読み時間
Haril Song
Owner, Software Engineer at 42dot

Kotlinでは、中置関数と呼ばれる関数の定義方法があります。これは、Javaを主要な言語として使用していた時には想像もできなかった構文です。Kotlinを始めたばかりの方に向けて、これを紹介しましょう。

単一のパラメータを持つメンバー関数は、中置関数に変換することができます。

中置関数の代表的な例の一つに、標準ライブラリに含まれている to 関数があります。

val pair = "Ferrari" to "Katrina"
println(pair)
// (Ferrari, Katrina)

必要に応じて、to のような新しい中置関数を定義することもできます。例えば、Int を次のように拡張することができます:

infix fun Int.times(str: String) = str.repeat(this)
println(2 times "Hello ")
// Hello Hello

to を新しい中置関数 onto として再定義したい場合は、次のように書くことができます:

infix fun String.onto(other: String) = Pair(this, other)
val myPair = "McLaren" onto "Lucas"
println(myPair)
// (McLaren, Lucas)

このようなKotlinの構文により、非常に独特なコーディング方法が可能になります。

class Person(val name: String) {
val likedPeople = mutableListOf<Person>()

infix fun likes(other: Person) {
likedPeople.add(other)
}
}

fun main() {
val sophia = Person("Sophia")
val claudia = Person("Claudia")

sophia likes claudia // !!
}

参考

Kotlin Docs

[Kotlin] 拡張されたループ

· 3分の読み時間
Haril Song
Owner, Software Engineer at 42dot

Kotlinでは、Javaに比べてはるかにシンプルで便利なループを書くことができます。どのように使うか見てみましょう。

1. .. 演算子

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0..fruits.size - 1) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

.. を使うと、1ずつインクリメントする従来のループが作成されます。

2. downTo

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in fruits.size - 1 downTo 0) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

downTo を使うと、期待通りにデクリメントするループが作成されます。

3. step

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0..fruits.size - 1 step 2) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

step キーワードを使うと、特定の数の要素をスキップするループを実装できます。これは downTo にも適用されます。

4. until

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0 until fruits.size) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

until を使うと、最後の数を含まないループが作成され、-1 を使う必要がなくなります。

5. lastIndex

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0 .. fruits.lastIndex) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

lastIndex プロパティを使うと、ループが読みやすくなります。しかし、もちろんまだ他にも方法があります。

6. indices

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in fruits.indices) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

indices はコレクションのインデックス範囲を返します。

7. withIndex()

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
}

withIndex() を使うと、インデックスと値を同時に抽出でき、コードがシンプルになります。これはPythonのシンプルさに似ています。これでほとんどのループシナリオに対応できますが、もう一つ方法があります。

8. forEachIndexed

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
fruits.forEachIndexed { index, fruit ->
println("$index: $fruit")
}
}

forEachIndexed にラムダ関数を使うと、コードがより簡潔で直感的になります。ニーズに合った適切な方法を選びましょう。


参考

Kotlin Tips: Loops

Kotlinで地球の楕円体を利用する

· 4分の読み時間
Haril Song
Owner, Software Engineer at 42dot

背景

earth 画像の参照1

地球が平らでも完全な球体でもなく、不規則な楕円体であることを考えると、異なる経度と緯度の2点間の距離を迅速かつ正確に計算するための完璧な公式は存在しません。

しかし、geotoolsライブラリを使用することで、数学的に補正された近似値を簡単に取得することができます。

依存関係の追加

geotoolsで地球の楕円体を使用するには、関連するライブラリの依存関係を追加する必要があります。

repositories {
maven { url "https://repo.osgeo.org/repository/release/" }
maven { url "https://download.osgeo.org/webdav/geotools/" }
mavenCentral()
}

dependencies {
...
implementation 'org.geotools:gt-referencing:26.2'
...
}

コードの記述

まず、ソウルと釜山の座標をenumクラスとして定義します。

enum class City(val latitude: Double, val longitude: Double) {
SEOUL(37.5642135, 127.0016985),
BUSAN(35.1104, 129.0431);
}

次に、テストコードを通じて簡単な使用例を見てみましょう。

class EllipsoidTest {

@Test
internal fun createEllipsoid() {
val ellipsoid = DefaultEllipsoid.WGS84 // GPSで使用されるWGS84測地系を使用して、地球に最も近い楕円体を作成

val isSphere = ellipsoid.isSphere // 球体か楕円体かを判定
val semiMajorAxis = ellipsoid.semiMajorAxis // 赤道半径、楕円体の長い半径
val semiMinorAxis = ellipsoid.semiMinorAxis // 極半径、楕円体の短い半径
val eccentricity = ellipsoid.eccentricity // 離心率、楕円体が球体にどれだけ近いかを示す
val inverseFlattening = ellipsoid.inverseFlattening // 逆扁平率の値
val ivfDefinitive = ellipsoid.isIvfDefinitive // この楕円体に対して逆扁平率が決定的かどうかを示す

// 大円距離
val orthodromicDistance = ellipsoid.orthodromicDistance(
City.SEOUL.longitude,
City.SEOUL.latitude,
City.BUSAN.longitude,
City.BUSAN.latitude
)

println("isSphere = $isSphere")
println("semiMajorAxis = $semiMajorAxis")
println("semiMinorAxis = $semiMinorAxis")
println("eccentricity = $eccentricity")
println("inverseFlattening = $inverseFlattening")
println("ivfDefinitive = $ivfDefinitive")
println("orthodromicDistance = $orthodromicDistance")
}
}
isSphere = false
semiMajorAxis = 6378137.0
semiMinorAxis = 6356752.314245179
eccentricity = 0.08181919084262128
inverseFlattening = 298.257223563
ivfDefinitive = true
orthodromicDistance = 328199.9794919944

DefaultEllipsoid.WGS84を使用して地球の楕円体を作成できます。WGS84の代わりにSPHEREを使用すると、半径6371kmの球体が作成されます。

距離の結果はメートル(m)で表示されるため、キロメートルに変換すると約328kmになります。Googleで検索すると325kmと表示されるかもしれませんが、私が選んだ座標とGoogleが選んだ座標の違いを考慮すると、これは悪くない数字です。

他にも多くの機能がありますが、すべてをこの投稿でカバーするのは難しいため、必要に応じて別の投稿で取り上げます。

情報

ビジネス要件によっては誤差が満足できない場合があるため、実際の実装前にgeotoolsの他の方法を十分にテストしてください。


Footnotes

  1. SRIDと座標系の概要