Golangを用いたサーバーサイド:技術選定と現状の小さな課題
前書き
話題を絞りたいので、アーキテクチャ(DDD)とインフラ(AWS)に関しては、記載を省略します。
現在の技術選定
採用しているOSSは、下表の通りです。Golangでの開発に焦点を当てるため、インフラ寄りの技術(Docker、AWS、Mackerelなど)は省略しています。
OSS名 | 役割 | 説明 |
---|---|---|
WEB Framework | goa独自のDomain Specific Languageをインプットとして、Web APIホスティングに必要なベース処理(ルーティング、コントローラ、Swaggerなど)を生成する | |
DDLおよびCRUDコード生成 | Golangの構造体をインプットとして、DDL(DBテーブル定義)と当該DBテーブルを操作するための簡易なCRUDコードを生成する | |
/ | RDBMS | データを永続的に保存するためのDB |
ER図の生成 | 稼働中のDBにアクセスし、ER図を生成する | |
DBマイグレーション |
MySQLのDBスキーマを比較し、差分を生成する |
|
テストカバレッジ計測 | GitHub Actionsのworkflowとしてテストカバレッジを計測し、カバレッジ結果をPR内にコメントする(ローカル実行も可能) | |
静的解析 | GitHub Actionsのworkflowとしてコードを静的解析し、指摘があればPR内にコメントする | |
負荷試験 |
APIを連続実行し、その結果をブラウザで表示する |
|
DIツール |
DIを用いた初期化処理を自動生成する |
大まかな開発の流れを示します。
-
NotionでAPI仕様案を作成し、iOS/Androidエンジニアと合意を取る
-
goaを用いて、APIのモックを作成する。ここでのモックとは、ダミーレスポンスを返すAPIのこと。goaの生成するswaggerは、API仕様の認識をエンジニア間で合わせるドキュメントとなる
-
-
実装を行う。ORMを採用していないため、SQLはGolangコード内に直接書いている。
-
ユニットテストを作成する。基本的に、カバレッジを通せる部分は全て通す。境界値チェックや網羅テストが必要な場合は、各々の判断でテストを追加する。
-
GitHub Pull Requestを作成し、2 approvalを得てからマージする
順番が前後する部分がありますが、基本的な新規開発では上記の流れの繰り返しです。
Web Frameworkは、移行タイミングを失った
(個人的な意見というより、会社都合の観点が多いです)
goa version 1は、非常に便利です。実装とドキュメントが乖離せず、自動生成されるswaggerを用いてエンジニア間でコミュニケーションが取れます。普段の開発でgoaに不満はありません。goaのDSL学習コストを充分にペイできています。
しかし、goa のメインラインはversion 3であり、goa ver 1の開発は下火です。業務では、会社のテックリードがforkしたgoa ver 1を使っています。goa ver1とver3は別物のなので移行コストが大きく、2023年現在では完全にver 1 –> ver 3の移行タイミングに乗り遅れました(移行時期、私は部外者だったので、様々な判断根拠を把握していません)
フレームワーク移行が進まない理由は、「移行しなくても困らない」および「開発体験が変わらない案が存在しない」「goaで作られたソフトが大量に存在する(全てを別フレームワークに移行できない)」あたりが挙げられます。
様々な事情で、goaの代わりにgorilla/muxやgin-gonix/ginを採用したサーバーアプリも開発しました。しかし、これらのフレームワークはswaggerを生成しないため、APIドキュメント管理に問題を抱えました。gorillaに至っては採用直後にアーカイブされ、頭を抱えました。フレームワーク毎の実装方法やセキュリティ問題を調査するコストも増え、3種類のフレームワークを導入したことは大きな判断ミスだったと感じています(この判断ミスは、私が局所的に犯したものです)
「gin(labstack/echoでも良い)とswaggo/swagを組み合わせれば、実装からswaggerを生成できる」や「go-swaggerを用いてswagger駆動開発にしよう!swaggerからGolangコードを生成すれば良い」という代案は思いつくのですが、それを実行するほどの不満がgoaに無い。それが問題です。
SQL駆動開発をするにはsqlcがベスト?
前提条件として記載しておくと、私はORM(Object Relational Mapping)ツールがあまり好きではありません。
例えば、Androidアプリでローカルストレージ操作をRoom(ORM)で行うぐらいであれば問題ないと思いますが、サーバーサイドのような大量のデータを高速に取り扱いたい場合は複雑なSQLを書きたい需要が生まれます。
ORMは「オブジェクト指向で設計されたレイヤー」と「DBスキーマ」とのインピーダンスミスマッチを減らすツールなので、複雑なSQLを書こうとするとどうしても不満が出てきます(あと、ビックリするバグが稀に話題になるので、印象が良くない)
つまり、「SQLは手書きがベスト」という結論になります。私はSQLを手書きしています。SQL駆動開発バンザイ……とはなっておらず、少し課題感があります。
Golang内にSQLを書くと、”+=”で文字列連結をすることになりますが、この書き方ではSQLを簡単にコピペできません。検証済みのSQLをペタッとGolangコードにコピーすることも出来ません。ヒアドキュメントを使えばいいじゃん!と考えても「ヒアドキュメントのバッククォート」と「MySQLのエスケープ文字」が競合します。
この煩わしさを解決しようとすると、SQLファイルからGolangコードを生成するkyleconroy/sqlcが輝いて見えます。sqlcに関しては過去に別記事でまとめましたが、sqlcは型安全なCRUDコード + DBテーブルに対応したモデル(構造体)を自動生成します。
DDLとCRUDコード生成に利用しているmyddlmakerは、現職のテックリードが開発したものであり、不満はありません。必要であれば機能追加も柔軟に行われています。しかし、私の「SQLをキレイに書きたい(割と見栄えの問題)」の解決を図ると、myddlmaker –> sqlc に置き換えたい欲求も生まれます。
sqlcぐらいに採用実績のあるOSSであれば、CRUDコードをテストコード以外に勇気を持って使える点もsqlcの評価ポイントです。
課題:E2Eテストの実現
サーバーサイドの開発では、多くの現場でDDDが採用されていると思われます。DDDのおかげで、各レイヤー単位のテストは容易に実施できますが、E2Eテストを実施するには戦略が必要です。
私が実装している範囲内では、サーバーサイドで完結するE2Eテスト(自動)は、実現できていません。E2Eテストを”go test”の文脈で実行できるk1Low/runnを選定したところまでは良かったのですが、途中でコード修正が必要なことに気づきました。
コード修正が必要な部分は、外部サービスを利用する処理です。コードを修正せずにE2Eテストを実行すると、高速に外部サービスへ通信します。下手をすれば外部サービスへの攻撃とみなされてしまいます。
この課題を解決するには、外部サービスのモックOSS(例:localstack/localstack)を選定するか、モックを自作する必要があります。さらに言えば、テスト時は外部サービスAPIを実行している処理をモックAPI実行に差し替えることも必要です。
理想は、接続先情報を変更すればコード変更することなく、外部サービスモック環境(ローカル環境やCI環境で建てたDockerなど)に接続するようにできる状態です。しかし、モック環境を準備するのは労力がかかるので、ダミーレスポンスを返すモックメソッドで誤魔化す事もあるでしょう。
「外部サービスを利用する部分 ≒ 外部ライブラリに定義されたクライアント構造体を利用する部分」でもあります。つまり、外部ライブラリのクライアント構造体をDI(依存性の注入)できるようにすると、E2Eテスト時に都合が良いことが多いと想定できます。
この案を採用するには、外部ライブラリのクライアント構造体(+ メソッド)をインタフェース化する必要があります。メソッドが沢山あるとインタフェースを作るのが大変なので、対象のクライアント構造体をインプットとしてvburenin/ifacemakerでインタフェースを生成するのが無難だと感じています。
課題:高速なテストコードの実現
サーバーサイドのテストコードでは、DB関係のテストが遅くなりがちです。DBテーブルに挿入したデータを削除する部分(Truncate)が致命的に遅いです。
この問題を解決するために、テスト開始時にCPUコア数だけDB接続インスタンスを作成し、DB接続インスタンスをチャネルから受け取る方法を導入しました。この方法によってテストが並行実行できるようになり、テスト実行時間が30〜40%ほど高速化されました。
高速化した後であっても、全テストを実行すると2〜3分かかります。このテストを30秒〜1分程度の実行時間に改善するのが目標です。
検討した結果、駄目だった案を以下に記載します。他の案をご存知な方はコッソリ教えていただきたいです。
- In-memoryにデータを保存する案
- MySQLはTEXT/BLOBをメモリに保存できないため断念
- DATA-DOG/go-sqlmockを使う案
- DBを使用しないため、高速化されるのは確実
- なるべくDBを動かしてテストしたいため劣後
- 既存テストの修正量が多いことも難点
- proullon/ramsqlを使う案
- メモリにデータを保存するため、高速化されると思われる
- PostgreSQL寄りなので、MySQL派は使いづらい
- 開発が活発ではない
- cockroachdb/copyistを使う案
- PostgreSQLのみをサポート(MySQLでも部分的に使える)
- Parallelでテストできない
課題:地味にモックを書くのが面倒
DI(依存性の注入)を実現するためにはインターフェースを作成し、そのインターフェースに適合するモックを書きます。今現在は温かみのある手作業でモックコードを作成してますが、誰がどう考えてもモックを自動生成できます。
matryer/moqとgolang/mockが自動生成ツールの候補なのですが、私の周りではどちらも良い評判を聞かず。悪くはないが良くもない、という評価を彷徨っている印象があります。これらのツールの使い方を覚えるぐらいなら、手でモックを書いた方が楽という派閥もあります。
どなたか最強のモック生成ツールを教えてください。
課題:ベンチマーク計測
DBに大量のデータが投入されると、動作が想定より遅くなる可能性があります。現状のDB設計で問題がないか、SQLクエリがメモリを過度に浪費しないかなどは、ベンチマーク計測を行わなければ把握できません。
「推測するな、計測せよ」という言葉があるように、DBやコードの最適化を想像で行うのは素人です。しかし、今の私は素人です。大量のデータを扱ったケースを想定した計測が実現できていません。
何故計測しないのか。理由の一つは、大量のデータを投入するテストをどのようにメンテナンスするかが結論付けられていないからです。計測自体は、今すぐ実現できます。
ベンチマーク計測をどのように行う想定かを説明します。Golangには、ベンチマーク計測するパッケージが標準で存在します。そのため、ダミーデータを生成するbrianvoe/gofakeitを使ってDBに大量のデータを挿入してから、ベンチマーク計測を行うテスト関数を実行する方法が考えついています。
しかし、実装量がそれなりにあり、DBスキーマやコードが変化した時のメンテナンスコストがどの程度になるのか(受け入れられるのか)で迷いが生じています。
しかし、最近は履歴形式でデータを扱うようになったため(一つのレコードを更新する形でデータを保持せず、都度追記を行う形式になったため)、大量の履歴データを操作する実行時間を計測しないと痛い目を見る、と感じています。2023年の終わりぐらいにはベンチマーク計測を導入し、その結果どうなったかの感想文が書けようにしたいと考えています。
最後に
他にやりたいこととして、「ファジングテスト」「securego/gosec 以外でのセキュリティ対策(コードベースでの対策)」がありますが、未調査です。ファジングテストはGolang 1.18から標準で導入されているので導入は難しくないのですが、私がコードを書く機会が減って試すことが出来ていません。
腰を据えて技術調査したいのですが、業務では新規開発が激しく、プライベートでは息子との格闘が激しく、なかなか上手く行かないものです。
ロシア人と国際結婚した地方エンジニア。
小学〜大学院、就職の全てが新潟。
大学の専攻は福祉工学だったのに、エンジニアとして就職。新卒入社した会社ではOS開発や半導体露光装置ソフトを開発。現在はサーバーサイドエンジニアとして修行中。HR/HM(メタル)とロシア妻が好き。サイトに関するお問い合わせやTwitterフォローは、お気軽にどうぞ。
2件のフィードバック
[…] 追記:サーバーサイドの方向けの情報を追記します。別記事に、私が個人的に抱えている技術的な課題を書いています。「こんな課題、簡単に解決できますよ」という方は是非応募していただきたいです。 […]
[…] Golangを用いたサーバーサイド:技術選定と現状の小さな課題 […]