Java Spring-BootとGridDBを使って短縮URLを生成するAPIを構築する

長くて面倒なURLにうんざりしていませんか？友人や同僚とURLを共有するのは難しいと感じませんか？もしそうなら、あなただけではありません。長いURLはイライラさせるし、管理も難しいです。幸いなことに、この問題を解決する方法があります。この記事ではJava Spring-BootとGridDBを使って簡単なURL短縮ツールを作る方法を紹介します。

## URL短縮ツールとは？

URL短縮ツールは、非常に長いURLから短いリンクを作成するサービスです。短いURLをクリックすると、ユーザーは元のURLにリダイレクトされます。例えば、長いURL https://developers.googleblog.com/2018/03/transitioning-google-url-shortener.html を goo.gl のように短縮することができます。短縮されたURLは共有しやすく、スペースも取りません。

## なぜURL短縮ツールを作るのか？

URL短縮ツールを作りたい理由はいくつかあります。 1. 長いURLをより効率的に管理できます。 2. クリック数やユーザーのエンゲージメントを追跡するのに役立ちます。 3. URLのブランディングに役立ちます。例えば、短縮URLにブランド名を使用することができます。

## 特徴

技術的な詳細に入る前に、URL短縮サービスに実装したい機能のいくつかを見てみましょう： 1. REST APIにアクセスできるサービスであること 2. 長いURLが与えられた場合、ユニークな短縮URLを生成すること 3. 短縮URLが与えられた場合、元のURLにリダイレクトすること 4. サービスは分析機能を提供すること（最も訪問されたリンク、URLが何回訪問されたか）

## システム設計

ここで留意すべき最も重要なことは、我々のシステムはread-heavyであるということです。読み込みリクエストの数は、書き込みリクエストの数の1000倍になります。

### 実際のURLのエンコード

*   与えられたURLの一意なIDを計算する。このチュートリアルではTSIDを使用します。
*   Base62エンコーディング（[A-Z, a-z, 0-9]）を使用して、IDを一意の文字列にエンコードします。最初の7文字を短いURLとします。

## SQL データベースか NoSQL データベースか

URL短縮サービスを構築する際に決定しなければならない重要なことの1つは、どのデータベースを使用するかということです。データベースには主に2つのタイプがあります。SQLデータベースとNoSQLデータベースです。

SQLデータベースは、あらかじめ定義されたスキーマを持つテーブルにデータを格納するリレーショナルデータベースです。構造化データやトランザクションの処理に適しています。

一方、NoSQLデータベースは、柔軟でスキーマのない形式でデータを格納する非リレーショナルデータベースです。非構造化データを扱い、水平方向に拡張するのに適しています。

私たちのURL短縮サービスでは、NoSQLデータベースを使用する予定です。私たちのユースケースでは、データ間の関係は必要としておらず、一方で高速な読み取りと書き込み速度が必要なため、NoSQLデータベースが適しています。

このチュートリアルでは、GridDBを使用します。GridDBは、IoTやビッグデータ・アプリケーション向けに最適化された、拡張性の高いインメモリーNoSQL時系列データベースです。

キー・コンテナ・モデルにより、JavaやCのAPIを通じてデータに高速にアクセスできます。また、GridDBのデータは、カスタムSQLライクなクエリ言語であるTQLを通じてクエリされます。WHEREコマンドによる基本的な検索や、インデックス作成による高速な条件検索操作は、高速な検索を必要とするアプリケーションに大きなメリットをもたらします。GridDBは、アプリケーションからの複数レコードを含むトランザクションをサポートしています。GridDBのトランザクションは、コンテナレベルでACID（Atomicity、Consistency、Isolation、Durability）を保証します。

このチュートリアルでは、URLを格納するテーブルとメトリクスを格納するテーブルの2つだけが必要です。

## Javaアプリケーションのセットアップ

このチュートリアルでは、以下のソフトウェアスタックを使用します。

*   Java OpenJDK 17 
*   Docker 23.0.1 
*   Spring Boot 3.0.5 
*   apache-maven-3.8.7

### GridDBインスタンス

APIエンドポイントを作成する前に、アプリケーションとGridDBのクラスタデータベースとの接続を取得する必要があります。ここでは、GridStoreクラスのインスタンスを作成します。

```java
    @Bean
    public GridStore gridStore() throws GSException {
        // Acquiring a GridStore instance
        Properties properties = new Properties();
        properties.setProperty("notificationMember", "griddbserver:10001");
        properties.setProperty("clusterName", "dockerGridDB");
        properties.setProperty("user", "admin");
        properties.setProperty("password", "admin");
        GridStore store = GridStoreFactory.getInstance().getGridStore(properties);
        return store;
    }
```

### URLを格納するためのデータセット作成

idカラムを主キーに設定します。

```java
    @Data
    public class UrlModel {
        @RowKey
        Long id;
        String shortUrl;
        String originalUrl;
        int clicks;
    }
```

### URLのメトリクスを格納するためのデータセット作成

時系列データには、タイムスタンプ形式の主キーが必要です。

```java
    @Data
    public class UrlMetricModel {
        @RowKey
        Date timestamp;
        String shortUrl;
        String userAgent;
        String ipAddress;
    }
```

### 行を管理するコンテナの作成

データベース接続を設定したら、それを使ってコンテナを作成します。ここでは、Collection コンテナを使用して URL を格納します。このフィールドは WHERE クエリの条件で使用されるため、shortUrl フィールドをインデックスとして使用します。

```java
    @Bean
    public Collection&lt;Long, UrlModel&gt; urlCollection(GridStore gridStore) throws GSException {
        Collection&lt;Long, UrlModel&gt; urlCollection = gridStore.putCollection("urls", UrlModel.class);
        urlCollection.createIndex("shortUrl");
        return urlCollection;
    }
```

URL メトリクスでは、発生時刻を持つ行を管理する必要があるため、TimeSeries コンテナを使用します。

```java
@Bean
public TimeSeries&lt;UrlMetricModel&gt; urlMetricContainer(GridStore gridStore) throws GSException {
    return gridStore.putTimeSeries("urlmetrics", UrlMetricModel.class);
}
```

## REST APIエンドポイント

アプリケーション用に以下のエンドポイントを作成します。

1.  POST `/api/urls` : 新しい短縮URLリソースを作成します。

```java
    public ShortUrlResponse create(UrlPayload payload) {
        UrlModel model = new UrlModel();
        model.setId(nextId());
        model.setOriginalUrl(payload.getUrl());
        model.setShortUrl(generateShortUrl(model.getId()));
        try {
            urlCollection.setAutoCommit(false);
            urlCollection.put(model.getId(), model);
            urlCollection.commit();
        } catch (GSException e) {
            e.printStackTrace();
            throw new UrlShortenerException("Please try again");
        }
        return SHORT_URL_MAPPER_INSTANCE.mapEntityToResponse(model);
    }
```

URL データを GridDB に格納するために Java API を使用します。

ペイロードの例：

```bash
      "url": "https://www.javacodegeeks.com/2023/05/what-are-events-relation-to-api-calls.html"
```

成功のレスポンス：

```bash
    {
      "url": "https://www.javacodegeeks.com/2023/05/what-are-events-relation-to-api-calls.html",
      "shortUrl": "HjlQjET"
    }
```

このエンドポイントは、サービスクラスによって生成された短いURLを含むJSON値を返すか、エラーが発生した場合に例外を投げます。

2\.  GET `/api/urls/{shortUrl}` : 元のURLを短縮URLで検索します。

```java
Query<UrlModel> query;
try {
    urlCollection.setAutoCommit(false);
    query = urlCollection.query("select * where shortUrl = '" + shortUrl + "'");
    RowSet<UrlModel> rs = query.fetch(true);
    if (!rs.hasNext()) {
        throw new UrlShortenerException(String.format("%s not found", shortUrl));
    }
    while (rs.hasNext()) {
        UrlModel model = rs.next();
        model.setClicks(model.getClicks() + 1);
        rs.update(model);
        urlCollection.commit();
        return SHORT_URL_MAPPER_INSTANCE.mapEntityToResponse(model);
    }
} catch (GSException e) {
    e.printStackTrace();
    throw new UrlShortenerException("Please try again");
}
```

最初の部分では、shortUrlでURLを照会しようとします。行が見つからない場合は、例外を投げます。行が見つかった場合は、クリック数をインクリメントする必要があります。その後、トランザクションをコミットします。

成功のレスポンス：

```bash
    {
      "url": "https://www.javacodegeeks.com/2023/05/what-are-events-relation-to-api-calls.html",
      "shortUrl": "HjlQjET"
    }
```

3\.  GET `/api/urls` : すべてのURLを、そのURLに何回アクセスされたかの情報とともにリストアップします。

```java
public List<UrlModel> getUrls() {
    List<UrlModel> urls = new ArrayList<>();
    Query<UrlModel> query;
    try {
        query = urlCollection.query("select * from urls");
        RowSet<UrlModel> rs = query.fetch();
        while (rs.hasNext()) {
            UrlModel model = rs.next();
            urls.add(model);
        }
    } catch (GSException e) {
        e.printStackTrace();
    }
    return urls;
}
```

このコードは、コレクション内のすべてのURLをフェッチするためのものです。

成功のレスポンス：

```bash
    {    
        "data":[
            {
              "id": 435224999694707071
              "shortUrl": "GjuWlGRgHH",
              "originalUrl": "https://developers.googleblog.com/2018/03/transitioning-google-url-shortener.html"
              "clicks": 7
            }
        ]
    }
```

4\.  GET `/api/metrics` : すべての url メトリクスをリストアップします。

成功のレスポンス：

```bash
    {
      "data":[
        {
          "timestamp": "2023-05-21T18:56:08.656+00:00",
          "shortUrl": "HgUsO2Cj7d",
          "userAgent": "vscode-restclient",
          "ipAddress": "172.23.0.1"
        }
      ]
    }
```

## APIの拡張

これらの項目は、本番稼動可能なシステムを作るために考慮しなければならないことです。

*   高い可用性: APIは高い可用性を持つべきです。URLリダイレクトとレスポンスタイムは、最小のレイテンシーでリアルタイムに行われるべきです。 
*   待ち時間を改善するためのキャッシュ: 頻繁にアクセスされる短いULRや1日の検索の上位10％をキャッシュすることで、APIの応答時間を改善することができます。 
*   ロードバランシング: どのサーバーがどのリクエストを処理できるかを決定します。ロードバランサーは、全ユーザーの窓口の役割も果たします。 
*   データ容量：システムに挿入するデータ量を把握し、5年または10年間のデータ保存を計算する。 
*   セキュリティ: APIは、悪意のあるユーザーがフィッシングサイトやマルウェアサイトへのショートリンクを生成するのを防ぎ、DDoS攻撃や総当たり攻撃から保護するように設計されるべきです。

## 結論

Java Spring-BootとGridDBを使ってシンプルなURL短縮ツールを作るのはそれほど難しくありません。この記事で説明したステップに従うことで、管理と追跡が簡単なURL短縮ツールを作成できます。