実例に学ぶGoのアーキテクチャ - 「開発スピード優先」でGMOペパボが採用したのはMVC
Goを活用した開発の際、どのようなアーキテクチャを採用するか、議論は尽きません。GMOペパボではオーソドックスなMVCを選んだといいますが、その背景にあったものとは。開発現場のお二人に話を聞きました。
2012年のバージョン1.0発表以降、Golang(以下、Go)はさまざまなサービスでの採用事例を増やしています。しかし一方で、Goを活用したサーバサイド開発における設計の大方針に関しては、どのような手法を採るべきか多くの議論が重ねられてきています。
例えば、DDD(Domain Driven Design:ドメイン駆動設計)やクリーンアーキテクチャ、レイヤー化アーキテクチャなど、さまざまな設計手法や概念が提唱されています。
では、Goを活用した開発に注力する企業では、どのようなアーキテクチャを採用しているのでしょうか。GMOペパボ株式会社ではホスティングサービス『ロリポップ!マネージドクラウド』のGoを用いた開発において、MVCモデルを採用しています。多様なアーキテクチャが提唱されているなかで、なぜ基本ともいえるMVCを採用しているのでしょうか。
その背景には「開発スピードを上げる」「Goらしいシンプルさを確保する」「いまある問題を解決する」という、極めて実用的な視座がありました。
- 小田知央さん(写真右)
- 2009年にGMOペパボ株式会社に入社。現在は技術基盤チームに所属し、チームを横断し、インフラ、技術刷新などを担う。2014年からGoを使用するようになり、その後、福岡でのGoのコミュニティ活動も積極的に行う。主宰の一人として関わるGo Conference'19 Summer in Fukuokaは2019年7月13日(土)に開催予定だ。
- 田中諒介さん(写真左)
- 2016年に新卒でGMOペパボ株式会社に入社。入社後は『ロリポップ!レンタルサーバー』のサーバーサイドエンジニアを経て、2017年から現在のGoを使用した『ロリポップ!マネージドクラウド』の開発に携わる。
なぜ、MVCを採用するのか
——まずは担当されているプロジェクトで、サーバサイドのGoはどのような設計になっているのか教えてください。
田中 私が担当しているのはホスティングサービス『ロリポップ!マネージドクラウド』内で、ユーザーさんが使っているサーバがリクエストを受けた際、構成情報を取得するAPIを作成するプロジェクトです。このプロジェクトではMVCを採用しています。誰でもすぐにコミットできる環境にすることで、新たなメンバーが「環境を理解するための時間的コスト」を削減し、開発速度を上げる、というのがMVCを採用している背景にあります。
——なるほど。他にもMVCを選んだ理由はありますか。
小田 2年ほど前、Goを使用したプロジェクトの開始当初、社内にGoのノウハウは蓄積されていない状態でした。一方で、過去Ruby on Rails(以下、Rails)で開発を推進していたこともあり、多くのエンジニアにとってMVCなら親しみがあり理解しやすいと考えたのです。
プロジェクトの立ち上がり速度を上げるために「設計は開発をしながら変えていこう」という意見にまとまり、設計変更を視野に入れつつ、まずはMVCでいくことになったんです。
——ディレクトリ構成はシンプルなMVCを採用しているのでしょうか。
田中 現状では/model
ディレクトリにモデル層を、/controller
ディレクトリにコントローラー層をそれぞれ配置していくMVCの基本的な形になっています。
——その他のGoを使用したプロジェクトでもMVCを採用しているのでしょうか。
田中 マネージドクラウド系のサーバAPIでは、ほぼ共通した構成になっています。ディレクトリ構成はMVCを採用しているプロジェクトがほとんどです。
小田 弊社では他プロジェクトチームとの情報共有に注力していて、ひとつのプロジェクトがうまく走りだすと、それに似た構成が自然と増えていきます。
同じ構成のプロジェクトが多ければノウハウを共有しやすいですし、別のプロジェクトに参加した際、構成をすぐに理解できるという利点があります。とはいえ、弊社ではMVCをディファクトとして設定しているわけではありません。あくまでプロジェクトの立ち上げ時のさまざまな条件に合致するアーキテクチャを検討した結果、MVCを使用しているんです。
「いま直面する問題」を解決するために、シンプルな構成を選ぶ
——DDDやクリーンアーキテクチャなどの設計を取り入れるという判断にはならなかったのでしょうか。
小田 Goの強みは言語のシンプルさにあると思っています。標準機能が強力なので、外部ライブラリの助けを借りる必要もそれほどないので、設計もシンプルに保てます。だからこそ、あえて複雑な設計にならないようにすることが大切だと考えています。
DDDやクリーンアーキテクチャーに関しては、その全てを踏襲するのではなく、参考にする程度にとどめています。DDDやクリーンアーキテクチャを実装するとなると、ディレクトリ構造など、基本に忠実に再現するというアプローチがあると思いますが、大事なのはコアの部分だと考えています。ルールがいくつもあると開発が複雑化してしまうことも予想されます。必要な部分を最低限決めて導入していくのがいいと考えています。弊社で大事にしている
- 循環参照に陥らないようにすること
- 新しくプロジェクトに入ってきた人がすぐに構成を理解できること
- テストがしやすいこと
こうした条件をクリアできれば、既存の設計に強くこだわることはありません。自分たちが直面している問題を設計によって解決できるかどうかを優先します。
田中 言語仕様に則し、Goらしくないことはやらないようにすることを心がけています。ついメタプログラミングぽいことしたくなりますが、書いた人しか理解できないコードはなるべく避けるべきです。
——Goらしさを大切にしているんですね。
小田 ソフトウェア設計で良いとされるものが必ずしもGoにあてはまるとは限らないので、まずはGoらしい書き方を理解し実践することに努めています。失敗しながら学んでいくものなので、ひたすら書いていくしかないでしょう。
複雑なコードを成立させるためには、チーム内での認識合わせが必要になり、どうしても開発スピードが落ちてしまいます。繰り返しになりますが、シンプルなディレクトリ構成で分かりやすいということが大事です。未来に起き得る問題を想定してアーキテクチャを複雑に考えるのではなく、まずは目の前にある問題を解決するための設計を優先したいと考えているのです。
——プロジェクトの途中で設計を変更しなければならない場合、労力が大きくなるというデメリットがありませんか。
小田 将来的に設計変更が必要になるか、といった予測を完璧に行うことはできません。予測できないことに備えた設計をしておく、というのはコストが必要ですし、新たな設計のためにチーム全員の習熟度を上げるのにもコストがかかってしまいます。構成は変わるものだという前提を理解しておき、テスタブルな設計であることを守っていく方が大事です。
テスタブルであれば、設計変更があってもバグのリスクを恐れず移行できる。あとは工数を確保すれば変更 / 改修に対応できるでしょう。先々のことを考えるよりも、いま直面している問題に労力を振り切った方がいいと考えています。
——設計を考える際に参考にしているものはありますか。
小田 golang-standardsというリポジトリにプロジェクトのディレクトリ構成例が書かれています。ここに書かれているものは非常に洗練されていて参考になります。
MVCを採用し感じる課題
——MVCでは「モデル層の肥大化」がよく課題として挙げられます。開発していてこの課題に直面することはありますか。
田中 現状ではそこまで顕在化した課題ではありません。しかし、いずれは直面する問題だと思います。それよりもモデルとコントローラーでロジックの分離がうまくできていないことの方が、大きな課題です。これは元々Railsを経験しているエンジニアが多かったことが原因の一つとしてあります。
RailsはActive Recordがあることで「モデルのロジックはモデルに書く」とルールが明確です。GoでもORマッパーとしてGORMというActive Recordに近い機能を持ったライブラリがありRailsエンジニアにとって使いやすくなっています。メソッドチェーンのような書き方ができ、作業しやすくプロジェクト立ち上げ時にGORMは非常に便利です。
しかし、Railsと同じ要領でGORMをコントローラー層に書いてしまったことが失敗でした。コントローラーにモデルのロジックがある状態になってしまったのです。さらにコントローラーにモデルと同じようなテストを書かなければならないという問題もあります。
小田 GORMの使い方でよくなかったのはメソッドを乱立させてしまったところです。コントローラーのメソッドごとにモデルのメソッドも増えていってしまい、密結合な関係になってしまっていました。
こういったGORMの間違った使い方を改めるべく、過去にGoCon(Go Conference)で発表をしたことがあります。カンファレンスではなく、まずは社内で発表しろ、という感もありますが(笑)。一例を挙げると……
package api func GetUserWithProfile(c echo.Context) error { n := c.Param("username") u, e := model.FindUserByUsernameWithProfile(n) if e != nil { return NewError(http.StatusNotFound, e, "not found") } return c.JSON(http.StatusOK, u)) } package model func FindUserByUsernameWithProfile(n string) (*User, error) { u := new(User) if err := db.Preload("Profile").Scopes(Enabled).Where("username = ?", n).First(u).Error; err != nil { return nil, err } return u, nil }
過去、こうした記述がありましたが非常に冗長です。Goらしいシンプルさを保とうと考えると以下のような記述が望ましいです。
package api func GetUserWithProfile(c echo.Context) error { n := c.Param("username") u := new(model.User) if err := db.Preload("Profile").Scopes(Enabled).Where("username = ?", n).First(u).Error; err != nil { return NewError(http.StatusNotFound, e, "not found") } return c.JSON(http.StatusOK, f) }
田中 使い方の問題だけでなく、GORMはJOIN
する際、パフォーマンスを上げるのに手間がかかるという課題がありました。そしてパフォーマンスを改善するために、どんどん複雑なコードやクエリが増えていってしまう。特殊な使い方をしているうちにGORMの本来の使い方からどんどん離れていってしまっている感覚があり、これはよくないな、と。
——GORM自体がボトルネックになってしまったのですね。
小田 GORMは優れたライブラリなのですが、僕らのプロジェクトにはマッチしていなかったといまは考えられます。パフォーマンスを考慮するフェーズになってくると特にミスマッチが実感できました。本来、DB接続のためには標準パッケージを使用すれば十分でした。
田中 「とりあえずディファクトだからGORMにしておこう」という判断がよくありませんでした。やりたいことに対してGORMの機能が大きすぎたのです。
——そういった問題を解決するために、現在のプロジェクトで設計変更する予定はあるのでしょうか。
田中 ライブラリの使いにくさなど、細かく気になる部分はありますが、まだアーキテクチャレベルの改修をする時期にはなっていません。その時期が来たとしてもテストはカバレッジよくやっているので、設計変更があっても怖くはありません。
小田 リファクタリングや設計変更は利益を生み出すための作業ではないので、どうしても優先度は低くなってしまいます。また、抜本的な設計変更はメンバーが主体的に動きだしてから進むものでしょう。チームの誰かから意見が出たり、実際に設計変更にまつわるプルリクが出た際、自然と動きだしていくものだと思います。
——もし現状で設計変更をしていくのであればどのように改善していきますか?
小田 先ほども言ったようにDB接続はGORMではなく標準ライブラリを使います。また、モデル層は抽象化したいと思います。モデル層がなくなるとDDDに近い構成になりますが、あくまで抽象化するだけです。Goらしいシンプルな構成を保つことは前提として考えているので、DDDを完全に踏襲することはないと思います。
田中 Goらしくない書き方になってしまっている部分が多く、MVCとしても改善すべき点もあります。もっとGo wayに忠実な書き方に直していきたいです。
ディファクトに従うのではなく、必要とされるものを選ぶ
——もしも新しいプロジェクトを始めるとしたらMVCでまた設計をしますか。
小田 僕はしないと思います(笑)。いや、正確に表現すると、直面する問題を解決するために手を動かして「これがいい」と思えるものを選ぶでしょう。ただ、MVCですらGoにとっては冗長だと僕は考えています。悩みを減らすために構成はシンプルにしておき、コード量が増えたときに適切にパッケージを分けるだけでも十分な対応です。そして先ほども言ったようにテスタブルであることを意識しておけば、設計変更時のコストを低くできるでしょう。
——実際に触っていてMVCはGoと相性がいいと思いますか。
小田 MVCを試してみるのはありだと思いますが、やはり完璧に相性があっているとは言えません。同時に、多様なケースにフィットする万能アーキテクチャというものも存在しません。
プロジェクト内容やプロジェクトメンバーの習熟度、どの程度の開発期間が用意できるか、など条件次第で設計は変わると思います。手を動かして自分たちに合うかどうか確認するのが大切です。
——逆にMVCにしておいてよかった点はありますか。
田中 やはり構成が分かりやすく、敷居が低い点は強みです。MVCによる開発スピードの高さは実感できています。しかし、今後の3~4年を考えるとコードの肥大化など、現状の設計では課題が見えてくる可能性があります。
——Goの設計は様々なものが提唱されていますが、これからディファクトは生まれると思いますか。
小田 むしろ、ディファクトがない状態が良いと僕は思っています。ディファクトに従うことが、直面する問題解決につながるとは限らず、ときに僕らの抱えているGORMのボトルネック問題のように、思わぬ課題をもたらすこともあります。
フレームワークやツールが多数投入されることで生じる開発の複雑化は、Goらしいシンプルさを実現するうえで、避けるべき状況です。すぐに書き始めることができるGoの利点を活かすためには、ディファクトがない状態の方が自然なのでは、と感じています。
——今後、プロジェクトで実現したいことはありますか。
田中 書き方がGoらしくないものを書き換えていきたいです。Go wayに沿った書き方をするだけでも、いま抱えている設計上の問題はかなり解消されると思います。
小田 サービスを分離し、「小さく作る」ことを目指したいです。モノリシックになっている部分を、細かくマイクロサービス化していくイメージです。小さく分けることでプロジェクトのディレクトリ構造の複雑化も避けられ、よりシンプルにGoを活用できると思います。
取材:megaya {$image_5}megayaのブログ