(私的)GoのWebAPI開発における現状: 論理編
この記事は?
インディー開発をしたり、本を読んだり、勉強会に参加したりして、GoのWebAPI開発における自分なりの実装パターンが固まってきたので現状を整理します。
具体的にはORM、フレームワーク、パッケージ構成あたりの話。
ある程度アンチパターンを踏んできてますが、僕自身プロダクションレベルでのGo開発経験がバリバリあるわけではないので、ここは間違ってるぞ!とか自分はこうしてる!など(建設的な)マサカリや大歓迎でございます。
tl;dr
前提
WebAPIの規模
以下のような小〜中規模程度のJSONで話すWebAPIを想定。
- APIの数は20本前後
- シンプルなCRUDベースのAPIが多いが、ちょっと複雑なリレーションやビジネスロジックも多少あり。
- 外部システムとの連携は(システム内の)DBがメイン。しかし、Push通知など外部のAPIと連携する箇所も。
ちなみにインディー開発中心なので、ミッションクリティカルなシステムであることは想定してません。とは言え、ある程度の規模までプロダクションレベルに耐えうるものを想定。(と言うより、そこを目指しています…!)
テクノロジースタック
このあたりは好みもあると思いますが、僕は以下の構成でやることが多いです。
上記の環境をDockerで作って開発してます。hot reloadの仕組みを入れるために fresh などを使うのはありだと思いますが、僕はIDEとして GoLand を使っており、ファイル保存時に自動でビルドが走るようにしてます。*1
なお、環境というかインフラはAWSやHeroku、VPS想定。GAE/GCPだとDatastoreを使うことが多いと思うので、また違ってきそう。*2
方針
銀の弾丸はないので、何が正しいか正しくないかは状況次第ですが、アーキテクチャやライブラリの選定基準や開発方針としては以下の2つを意識してます。
- やりすぎない(オーバーエンジニアリングしない)
- done is better than perfect!
- 郷(Go)に従う
- Rails way!
現状
前置きが長くなりましたが、ここからGo界隈でよく盛り上がるトピック別に現状を書いていきます。
ORM関連
いろいろ使いましたが、最終的に sqlx に落ち着いています。sqlx自体はORMというよりは database/sql
パッケージをいい感じに使えるようにしたライブラリ。database/sql
パッケージで頑張るのがGoっぽいとは思うのですが、structへのマッピングを自分でやるのは大変なので、メンテもきちんとされているsqlxを使うようになりました。
ActiveRecordに慣れている人は GORM もアリだとは思うのですが…と言うか自分も最初使ってました。しかし、エラーハンドリングが重要なGoの世界観と合わないと思ってるのと便利な分コードが複雑なので、慣れてきたら薄いライブラリを使うのがいいかなと思ってます。
ちなみに、スキーマ管理は schemalex か sqldef がおすすめです。MySQLならschemalexが枯れてて安心感があります。
その他の選択肢
フレームワーク関連
いわゆるWAFは使ってません。フレームワークを採用するのも良いと思いますが、個人的には余計なDSLを覚えずに可能な限りGoだけで書きたいので、現状は使ってません。RubyにおけるRailsみたいなデファクトスタンダードとなるフレームワークが誕生すれば、また別かもしれない。
とは言え、全部自前で実装するのはつらいので、ルーティングは Gorilla/Mux 、ミドルウェアは negroni を利用しています。JSONではなくHTMLをレンダリングするような場合だと、これだけだと辛いかもしれませんが、JSON形式のWebAPIならこれで充分かなと思ってます。
その他の選択肢
パッケージ構成
個人的にはGoに慣れてきて、一番困るのがこれかなと思ってます。フレームワークを利用していればまだいいのですが、使ってないと自由過ぎて困ります。ですので、これは今も試行錯誤していますが、最近はいわゆる?レイヤードアーキテクチャ+ポートパターンを採用しています。*4
概要図
こういうやつです。
基本的に通常のレイヤードアーキテクチャのように上から下に流れるようにしてる(下位のレイヤは上位のレイヤに依存しない)のですが、ドメインレイヤーにインターフェイス(ポート)を置くことで、ドメインレイヤだけどこにも依存しないようにしてます。
なお、破線部分については後述します。
フォルダ構成
こんな感じでやってます。
. ├── Gopkg.lock ├── Gopkg.toml ├── application │ ├── article.go │ └── user.go ├── config ├── domain │ ├── article.go │ └── user.go ├── infrastructure │ ├── db.go │ ├── article.go │ └── user.go ├── main.go ├── presentation │ ├── controller │ │ ├── handler.go │ │ ├── article.go │ │ └── user.go │ └── view │ └── render.go └── vendor
各レイヤの責務
僕がよく使うRailsやCakePHPのようなMVCモデルと比較しながら説明していきます。
プレゼンテーション層
MVCで言うコントローラーとビューにあたる箇所。コントローラーの責務はリクエストを受け取り、それをアプリケーションレイヤで利用出来る形にしてアプリケーションレイヤを操作すること。そして、その結果を(ビューと連携して)レンダリングすること。ここは極力コード量が少なくなるように意識しています。ビューの責務はレンダリングに関すること全般。ただ、JSON形式のWebAPIだとコントローラー側でJSONを出力するだけで事足りることが多いので、あんまり書かないことが多いかも。僕は汎用的なレンダリングのロジックだけ書くことが多いです。
アプリケーションレイヤ
いわゆるサービスクラスっぽい役割をする箇所。アプリケーションレイヤの責務はドメインレイヤとインフラストラクチャレイヤのビジネスロジックを操作すること。ちなみに、DBのトランザクションはここで制御しています。
ドメインレイヤ
みんな大好きモデルを扱う箇所。ドメインレイヤの責務はビジネスロジックの実装を持つことですが、技術的な実装(直接的なDBの操作やPush通知の送信など)は行ってません。また、先述の通り通常のレイヤードアーキテクチャとは異なり、ドメインレイヤとインフラストラクチャレイヤとの間にインターフェイスを設け、技術と実装を分離するようにしています。DBのモデル(struct)はここで定義していますが、インフラストラクチャレイヤで DTO を定義することもあります。*5
インフラストラクチャレイヤ
技術的な実装を扱う箇所。インフラストラクチャレイヤの責務は直接的なDB操作(SQLの実行)やPush通知の送信など技術的な実装を行うこと。通常のレイヤードアーキテクチャでは、インフラストラクチャレイヤはどこのレイヤにも依存しませんが、技術と実装を分離するために必要に応じてドメインレイヤのインターフェイスに依存させています。
オレオレルール
- 厳密なレイヤードアーキテクチャでは直下のレイヤにのみ依存すると思うのですが、2個下のレイヤに依存するのはある程度OKにしています。
- これが上記概要図の破線の意味です。
- 理想は直下のレイヤにのみ依存させるのがベストだと思うのですが、現実問題として厳密にやりすぎると辛いときがあるのである程度は許容してます。
- ただ、3個下は流石にダメ!(明らかに責務を越えてる)
なぜレイヤードアーキテクチャを採用しているのか?
個人的にはGoで小〜中規模のWebAPIを開発する時にクリーンアーキテクチャはちょっとtoo muchかなと思っています。*6 とは言え、薄いライブラリしか使っていないとMVCモデルでは少し辛いケースが多く、また、テスト観点から技術と実装を分離したくなることが多いので、このようなアーキテクチャにしています。ですので、逆に言うと、フルスタックなフレームワークやORMを使っている場合はMVC(+サービスクラス的なもの)で充分なケースも多いと思います。また、テスト観点だけであれば、インフラストラクチャレイヤがDBのみの場合は、インターフェイスを置かず通常のレイヤードアーキテクチャでもいいかなと思ってます。もちろん、単体テスト時のDBアクセスは低速でアンチパターンですが、RailsやCakePHPをこれまで使ってきた身からすると、そこまで違和感ないので。
その他の選択肢
- 縦割りパッケージ構成
- レイヤではなく、機能で分けるパターン。具体的にはuser packageやarticle packageを作るパターン。
- microservies化を想定しているならアリかなと思います。
- Goっぽいなと思いつつ、個人的には共通処理の扱いが難しいなと思ってます。
パッケージ管理ツール
Go1.12から vgo が正式導入されますが、3rd partyの対応状況を鑑みると、まだ(2019年1月現在)は dep が無難かなと思います。とは言え、今後はvgoを使うのが主流になっていくはず。
最後に
書いてみて、コードがないと分かりづらい気がしたので次はコード編を書いてみたい。その時は今回書けなかったログとかエラーハンドリングのことも書くぞ。
あと、冒頭にも述べましたが、Goは導入事例が増えてきたとは言え、RubyやJavaに比べると枯れておらず、ベストプラクティスがそこまで世に出回っていないので、自分なりの考えや現状を書くことで誰かのためになったり、議論のきっかけになったりすればいいなと思ってます。*7
2019年もGoGo!
参考サイト
- Go言語でのORMを色々検討してみた
- SQLで羃等にDBスキーマ管理ができるツール「sqldef」を作った
- https://qiita.com/bussorenre/items/0ec8722a8f0ecd977104
- Goのパッケージ構成の失敗遍歴と現状確認
- Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
- GolangでAPI Clientを実装する
- Goにおけるバージョン管理の必要性 − vgoについて −
脚注
*1:単純にMakefike経由でビルドしているだけ。
*2:『Clean Architecture 達人に学ぶソフトウェアの構造と設計』でも言及されている通り、技術的な実装に引っ張られないアーキテクチャが本当は理想なのでしょうが…。
*3:ただ、フレームワークはそんなにたくさんは試していないので、他にもっと良い選択肢があるかもしれない。
*4:この用語は『pospomeのサーバサイドアーキテクチャ』を参考にしています。
*5:このあたりは現在進行系で悩み中です…リポジトリパターンやDAOをどうやって使うとか…難しい…。
*6:もちろん開発するプロダクトや個人/チームのスキルにも依るとは思います。
*7:翻って、そこからまた自分が成長出来ればいいなとこっそりと思ってます。