【第1回】型安全な通信について

さまざまな方法で型安全な通信を実現する方法を紹介します。

Homeブログ一覧【第1回】型安全な通信について

はじめに

今月末まで少しだけカテゴリフリーで講習をすることになりました。日程が増えてしまって申し訳ないですがよろしくお願いします。

型について

型はプログラムの中で扱うデータの種類を表すものです。例えば、数値や文字列などがあります。型があることで、プログラムが正しく動作するかどうかを保証することができます。C++を書いてる人はもう馴染みがあるかもしれませんが、JavaScriptやPythonなどの動的型付け言語を使っている人は少し違和感があるかもしれません。

let a = 1;
let b = "1";
console.log(a + b); // 11

JSは動的型付け言語なので、数値と文字列を足してもエラーをおこさないために基本的に暗黙的にキャスト(データ型の変換)を行います。このような挙動はバグの原因になりやすいです。 それを防ぐために型安全な言語を使うことがあります。

TypeScriptはJavaScriptのスーパーセット(上位互換)であり、型安全な言語です。TypeScriptはJavaScriptに型注釈と少しの構文を追加したものです。TypeScriptはコンパイル時に型チェックを行うため、型の不一致を事前に検出してバグをある程度防ぐことができます。

let a: number = 1;
let b: string = "1";

console.log(a + b); // Error: Operator '+' cannot be applied to types 'number' and 'string'.
console.log(a + Number(b)); // 2
console.log(a.toString() + b); // 11

このように、TypeScriptは型の不一致を検出してくれるため、バグを防ぐことができます。 逆にそういうJSでの暗黙的なキャストによる挙動を再現するためには、NumbertoStringなどの明示的なデータ型変換を行うことで実現することができます。

型安全な通信とは

Web研でREST APIを書いた人ならわかると思いますが、近年のサーバークライアント型アプリケーションでは基本的にクライアント - サーバ間の通信はJSON形式で行われます。 JSONで通信するのはもちろん便利ですが、型の不一致によるバグが発生しやすいです。

例えば https://hoge.api.maximum.vc/user からユーザー情報を取得するAPIがあるとします。このAPIは以下のようなJSONを返します。

{
  "id": 1,
  "name": "sor4chi"
}

次にこの情報を取得するためのクライアントを書いてみます。

type User = {
  id: number;
  name: string;
};

const UserDisplay = () => {
  const [user, setUser] = useState<User>({ id: 0, name: ""});

  useEffect(() => {
    fetch("https://hoge.api.maximum.vc/user")
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, []);

  return (
    <div>
      <p>{user.id}</p>
      <p>{user.name}</p>
    </div>
  );
};

さて、ここでAPIの仕様が変更されて、nameusernameに変更されたとします。この場合、クライアントはuser.nameでユーザー名を受け取ることを期待した実装になっているため、user.usernameundefinedになります。さあサーバー側の変更によって名前が表示されなくなってしまいました。大変だね。

このようなバグを防ぐために、あらかじめ通信で何が受け取れるかを型として管理し、サーバー側とクライアント側でその型情報を共有することで、もしレスポンス仕様に対して何らかの変更があった場合にコンパイルエラーを発生させ、バグを本番環境(実際のユーザーが使う環境)に持ち込まないようにすることができます。

型安全な通信の実現方法

サーバーとクライアントがどちらもTypeScriptで書かれている場合

Monorepoと言われる、複数のパッケージを1つのリポジトリで管理する方法を使うことで、サーバーとクライアントで共通の型情報を参照することができます。 これはフロントエンドではアーキテクチャを整理する上で必要な技術で、フロントエンドの上達に必須な技術なのでぜひ触れてみてください。(sor4chi個人の意見です)

Hono

Web研に一番身近なフレームワークはおそらく Hono (Client) でしょう。HonoはTypeScriptで書かれた型安全なHTTPクライアントライブラリを持っており、型安全な通信を実現することができます。

Hono作者によるHono Client (Hono RPC) の紹介記事

Hono

スキーマパッケージを共有する

もう一つの方法として、サーバーとクライアントで共通のスキーマパッケージを作成し、それを使って型情報を共有する方法があります。 これは Web Speed Hackathon 2024で用いられた方法です。

共通のインターフェースを定義するためのスキーマ(型定義、雛形)を作成してモノレポの1パッケージとして配置、それをサーバーとクライアントで共有することで、型情報を共有することができます。

Shared Schema

サーバーとクライアントが別言語の場合

サーバーとクライアントが別言語で書かれている場合、共通の型は基本的には参照できません。そもそも言語が違うので、型の定義方法も読み込み方も何もかもが違います。

しかし、これらを解決するとても面白いソリューションがいくつかあるので紹介します。

スキーマ記述言語を使う

サーバーとクライアントで言語が違うならば、どちらにも依存しない中間言語を使えばいいじゃないかという発想で生まれたのがスキーマ記述言語です。

OpenAPIはAPIの仕様を記述するための言語です。OpenAPIはJSONやYAMLといった形式でAPIの仕様を記述することができ、その仕様からクライアントコードやサーバーコードを生成することができます。

openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /user:
    get:
      responses:
        '200':
          description: A user object
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string

このように書けば、/userエンドポイントから返ってくるJSONは

{
  "id": 1,
  "name": "sor4chi"
}

こんな形をしてるよということを表現できます。この仕様を元に、クライアントコードやサーバーコードを生成することができます。

OpenAPI Generatorを使えば、この仕様書から型情報等のコードを生成することができるため、言語が異なっていてもその言語のOpenAPI Generatorがありさえすれば、型情報を共有することができます。 (OpenAPI Generator以外にも色々な生成ツールがありますが、今回はOpenAPI Generatorを紹介します)

これほどの言語に対応しているそうです。

Schema Language

他にも、Protocol BuffersTypeSpecなどもあります。

(sor4chi個人的にはTypeSpecがアツいです)

MelineはProtocol BuffersからTypeScriptの型定義を生成してクライアントへ、Goの型定義を生成してサーバーへとそれぞれ提供することで、型安全な通信を実現しています。 MelineではREST APIの型付けとしてProtocol Buffersを採用していますが、Protocol Buffersは主にgRPCのスキーマ定義として使われることの方が多いかもしれません。

gRPC

余談ですが、gRPCはGoogleが開発したRPC(Remote Procedure Call)フレームワークで、Protocol Buffersを使って通信を行います。gRPCはHTTP/2を使って通信を行うため、低レイテンシで通信を行うことができます。

HTTP/2を使うことで、1つのTCP接続で複数のリクエストを並列に処理することができるため、通信の効率が向上します。また、HTTP/2はヘッダー圧縮やサーバーからのプッシュなどの機能を持っているため、REST APIのようなサーバーからの単方向の通信だけでなく、双方向の通信を行うこともできます。しかし、HTTP/2が必ず必要になるため、ブラウザ-サーバー間の通信には基本的には使えません。 そのため、マイクロサービス間などのサーバー-サーバー間の通信に使われることが多いです。

gRPC

GraphQL

GraphQLはFacebookが開発したクエリ言語です。従来のREST APIのようなサーバーの実装では、クライアントが必要な情報を取得するために、その都度新しいエンドポイントを実装する必要がありました。しかし、GraphQLではクライアントが必要な情報をクエリとして送信することで、サーバーがそのクエリに対して必要な情報を返すことができます。

これはWeb Speed Hackathon 2023で用いられた方法です。

GraphQL

サーバー側で定義したGraphQLスキーマを元にクライアント側へ型情報を提供し、型安全な通信を実現しています。

GraphQL通信はブラウザ側での呼び出しを想定しているため、gRPCとは違いブラウザ-サーバー間の通信に使うことができます。(BodyはJSONです)

スキーマ駆動開発

今まで紹介した中で、しきりにスキーマという言葉が出てきたと思います。 スキーマ駆動開発とは、開発の際にスキーマを中心にして開発を行う手法のことです。 Webアプリを複数人で開発していて、どうしても「サーバーが実装されてないとクライアント側の実装が進められない」という場面に出会ったことはありませんか? これはサーバーの実装が終わるまで提供するAPIの仕様がクライアント側に伝わらないために起こる問題です。 逆に言えば、APIの仕様を先に決めておけば、サーバーはこういうデータをこういう挙動で必ず返すという信頼のもとで実装を進めることができます。 そうすることで、ウォーターフォール式に開発しなければいけなかったフローが、同時進行で開発を進めることができるようになり、アジャイル開発に向いた開発フローを実現することができます。

フレームワーク・言語などプログラミングは一企業もしくはOSSコミュニティに支えられながら運用されていることが多いです。そのため、いつその技術が使われなくなったり、サポートが終了したり、問題が発生したりするかわかりません。 しかし、スキーマはその技術に依存せずAPI仕様として独立した形で存在するため、技術の変更に強いです。長期運用・メンテナンス性を考えると、スキーマ駆動開発は非常に有用な手法だと言えます。

まとめ

型安全な通信は、サーバーとクライアント間の通信において型情報を共有することで、ある程度の疎通に関するバグを防ぐことができます。

スキーマ駆動開発を導入してサービス開発を行うことで、開発をスムーズにしたり、長期運用・メンテナンス性を向上させることができます。

次回の講習では、字句解析・構文解析・抽象構文木・評価など、インタプリタ中心のプログラミング言語のお話をする予定です。お楽しみに!