GoSuda

Go と OpenAPI エコシステム

By iwanhae
views ...

서론

Go 言語で Production Backend サーバーを開発する際、ほとんどの開発者が最初に直面する難題の一つは次のとおりです。

API ドキュメント化、どうすればよいか…?

これについて少し調べると、OpenAPI の仕様に沿ったドキュメントを作成することが有益であると気づき、自然と OpenAPI と連携するライブラリを探すことになります。しかし、この決定を下しても、次に問題が生じます。

OpenAPI 関連のライブラリがたくさんあるが、どれを使えばよいか…?

この文書は、このような状況を経験されている Go 初心者の方々のために作成した、簡潔なライブラリ紹介記事です。2024年末時点での文書であり、言語エコシステムは常に流動的に変化するため、参考にしつつ常に最新の動向も確認することをお勧めします。

OpenAPI に対応するライブラリの戦略

既にご存知かもしれませんが、OpenAPI は REST API を明確に定義し、ドキュメント化するための仕様です。API のエンドポイント、リクエスト、レスポンス形式などを YAML または JSON 形式で定義することで、開発者だけでなく、フロントエンド、バックエンドのコード生成を自動化し、無意味な繰り返し作業を減らし、些細なヒューマンエラーを減らすのに大きく貢献します。

このような OpenAPI をプロジェクトに自然に組み込むために、Go エコシステムのライブラリは主に次の3つの戦略をとります。

1. Go のコメントを OpenAPI 仕様ドキュメントとして組み合わせる

OpenAPI に合わせて API を開発する際の難しい点の一つは、実際のドキュメントとそれを実装したコードが別のファイルで全く異なる場所に存在するため、コードを更新したのにドキュメントを更新しなかったり、ドキュメントは更新したのにコードを更新できなかったりする状況が意外と頻繁に発生することです。

簡単な例を挙げると、

  1. ./internal/server/user.go というファイル内で API のロジックを修正したが、
  2. 実際のドキュメントは ./openapi3.yaml に存在し、この変更を誤って忘れてしまう可能性があります。
  3. このような変更に関する問題を認識せずに Pull Request を発行し、同僚からレビューを受けた場合、
  4. レビュー担当者も ./openapi3.yaml の変更が目に見えないため、 API 仕様はそのままなのに、実際の API 実装が変更されてしまうという不都合が生じる可能性があります。

Go のコメントの形で API ドキュメントを作成することで、このような問題をある程度解消できます。コードとドキュメントが一箇所に集まっているため、コードを修正しながらコメントも一緒に更新できます。このようなコメントに基づいて、OpenAPI 仕様ドキュメントを自動的に生成するツールが存在します。

代表的なプロジェクトとしては Swag があります。Swag は Go コードのコメントをパースして OpenAPI 2 形式のドキュメントを生成します。使用方法は簡単です。ハンドラー関数の上に、各ライブラリで定められた形式に従ってコメントを記述するだけです。

 1// @Summary ユーザー作成
 2// @Description 新しいユーザーを作成します。
 3// @Tags Users
 4// @Accept json
 5// @Produce json
 6// @Param user body models.User true "ユーザー情報"
 7// @Success 200 {object} models.User
 8// @Failure 400 {object} models.ErrorResponse
 9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11    // ...
12}

このようにコメントを記述すると、Swag という CLI はこれらのコメントをパースして OpenAPI 2 ドキュメントを生成します。一般的に、CI プロセスでこのような作業が行われ、生成された OpenAPI 仕様のドキュメントは Git リポジトリ、最終ビルド成果物、または別途の外部 API ドキュメント管理システムにデプロイされ、他のプロジェクトとの連携時に使用されます。

長所:

  • コメントがコードと共にあるため、実際のコードとドキュメントの整合性が崩れる可能性が低減します。
  • 別途のツールや複雑な設定なしに、コメントだけで手軽かつ自由にドキュメント化できます。
  • コメントが実際の API ロジックに影響を与えないため、ドキュメントとして公開するのがためらわれる一時的な機能を追加するのに適しています。

短所:

  • コメントの行数が増えることで、単一コードファイルの可読性が低下する可能性があります。
  • コメントの形式で全ての API 仕様を表現するのが難しい場合があります。
  • ドキュメントがコードを強制するわけではないため、OpenAPI ドキュメントと実際のロジックが一致するという保証はできません。

2. OpenAPI 仕様のドキュメントから Go コードを生成

Single source of Truth (SSOT) を Go コードではなくドキュメント側に置く方法も存在します。それは、OpenAPI 仕様をまず定義し、定義された内容に基づいて Go コードを生成する方式です。API 仕様がそのままコードを生成するため、開発文化として API 設計を先にすることを強制でき、開発順序として API 仕様の定義が最初に始まるため、開発が完了してから初めて見落としを認識し、API 仕様の変更とともに全体コードが修正されるという不都合を早期に防止できるという強みがあります。

この方式を採用する代表的なプロジェクトとしては、oapi-codegenOpenAPI Generator が存在します。使用方法は簡単です。

  1. OpenAPI 仕様に沿って yaml または json ドキュメントを作成し、
  2. CLI を実行すると、
  3. それに対応する Go stub コードが生成されます。
  4. この stub が使用できるように、個別の API の詳細ロジックのみを直接実装すればよいのです。

以下は oapi-codegen が生成するコードの例です。

1// StrictServerInterface represents all server handlers.
2type StrictServerInterface interface {
3	// ...
4	// Returns all pets
5	// (GET /pets)
6	FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7	// ...
8}

上記の interface を介して oapi-codegen が生成したコードは、クエリパラメータ、ヘッダー、ボディのパースおよびバリデーションなどのロジックを実行し、interface に宣言された適切なメソッドを呼び出す構造です。ユーザーは上記の interface の実装のみを行えば、API 実装に必要な作業が完了します。

長所:

  • 仕様が先行し、開発が進むため、複数のチームで協力する場合、作業を並行して進めるのに有利です。
  • 反復的な手作業で行っていた部分のコードが自動的に生成されるため、業務効率が向上しつつもデバッグには依然として有利です。
  • ドキュメントとコードの整合性が常に一致することを保証しやすいです。

短所:

  • OpenAPI 仕様自体に不慣れな状態である場合、初期の学習コストが多少存在します。
  • API をハンドリングするコードの形式がプロジェクトによって自動生成されるため、カスタマイズが必要な場合に対応が難しい可能性があります。

著者のコメント。 2024年10月現在、OpenAPI Generator が生成した Go コードは、API ロジックだけでなく、プロジェクト全体の構造を強制し、プロジェクトの構造が硬直しており、実際の Production 環境に必要な多様な機能を追加するには不適切な形式のコードを生成しています。この方式を採用される方々には、oapi-codegen の使用を積極的に推奨します。著者は、oapi-codegen + echo + StrictServerInterface を使用しています。

3. Go コードから OpenAPI 仕様ドキュメントを生成

数十、数百人の人々が同じサーバーについて開発を進める場合、個別の API ごとに統一性が崩れるという問題が必然的に発生します。直感的な例として、100を超える API Endpoint の仕様を1つの OpenAPI yaml ファイルに宣言する場合、そのファイルは1万行を超える怪物となり、新しい API Endpoint を宣言する際に必然的に同じモデルを重複して宣言したり、いくつかのフィールドを漏らしたり、規約に合わない Path ネーミングが誕生したりするなど、全体的な API の統一性が崩れ始めます。

このような問題を解決するために、OpenAPI yaml のオーナーを別に置いたり、Linter を開発して CI プロセス中に自動的に検出できるように措置を講じることもできますが、Go 言語で Domain-specific language (DSL) を定義することで、全ての API が一貫した統一性を持つように強制することができます。

このような手法を使用する代表的なプロジェクトは Kubernetes であり(別途ライブラリなしで独自に構築)、go-restfulgoa などのプロジェクトを使用して試すこともできます。以下は goa の使用例です。

 1var _ = Service("user", func() {
 2    Method("create", func() {
 3        Payload(UserPayload)
 4        Result(User)
 5        HTTP(func() {
 6            POST("/users")
 7            Response(StatusOK)
 8        })
 9    })
10})

上記のようにコンパイル可能な Go コードを作成すると、POST /users API の実装とドキュメントの定義が同時に完了するという利点が得られます。

長所:

  • コードから全てが生成されるため、プロジェクト全体の API の一貫性を維持しやすいです。
  • Go の強力な型システムを活用することで、OpenAPI3 の全ての機能を活用した場合よりもより正確で議論の余地のない仕様を得ることができます。

短所:

  • 各フレームワークで定義された DSL を習得する必要があり、既存のコードに適用するのは難しい場合があります。
  • フレームワークが提案する規則に強制的に従わなければならないため、自由度および柔軟性が低下する可能性があります。

まとめ

各方法は長所と短所があり、プロジェクトの要件とチームの好みによって適切な方法を選択することが重要です。常に最も重要なのは、どのような方法を使用するのが良いかではなく、現在自分が置かれている状況に最も適したソリューションは何であるかを価値判断し、開発生産性を高く保ち、早期退勤満足のいくワークライフバランスを享受することです。

現在2024年10月を基準に記事を作成しましたが、Go と OpenAPI のエコシステムは継続的に発展しているため、この記事を読む時点との時間差を考慮し、各ライブラリやプロジェクトの動向とその変更された長所と短所も継続的にフォローアップすることをお勧めします。

幸せな Go ライフを!😘