Pairsエンゲージの新規開発に採用したGo言語のサーバーサイドアーキテクチャ🤵👰
はじめに
この記事は「eureka Advent Calendar 2019」22日目の記事です。
こんにちは!APIチームの@Jimeuxです。今夏、エウレカはPairsエンゲージ(以下エンゲージ)という結婚コンシェルジュサービスをリリースしました。本記事ではPairsのサーバーサイドアーキテクチャから学んだことを今回の新規開発にどう活かしたかを紹介していきます。
Pairsのサーバーサイドについて
現在のPairsのサーバーサイドは、PHPからGo言語でフルスクラッチに開発され、2015年にリリースされました。それ以来、複数のマイクロサービスがmonorepoで開発され、今ではGo言語のソースコードの行数が約40万行まで増えてきています。(※空行・コメントを除く)
エンゲージのサーバーサイド開発に着手する前、チームではPairsのアーキテクチャを主にパターンベースで考えていました。
- Controller:HTTPリクエスト・レスポンス周りの管理
- Facade:処理をユースケース別にまとめ、トランザクション管理を行う(複数のサービスを扱うことが多い)
- Service:複数のFacadeに再利用できる処理を実装する
- Repository:MySQL、DynamoDB、Redisなどデータストア操作を行う
これらのパターンは、新規機能を開発する際のフレームワークとなり、チームの効率的な開発を担保してきました。データアクセスならRepository、HTTP周りならControllerというふうに、関心の分離をうまく実現できました。
Pairsのアーキテクチャにあった課題
コードベースを数10万行という規模までスケーリングできたにしても、アーキテクチャをパターンで考えれば、レイヤの責務や依存関係が曖昧になりがちです。その結果、コードやレビューを書く時に迷うことが増えていました。特に頭痛の種となった課題が2点ありました。
① ビジネスロジックの置き場がない
責務としてビジネスロジックを管理するレイヤがなかったため、結局アプリケーションの全体に漏れてしまいました。そのため、以下の問題が発生しました。
- 仕様が正しく実装されているのか確かめづらい
- ロジックを重複して実装するリスクがある
- FacadeやControllerと同じ構築をしないとテストが書けない
② インフラへの依存性
Serviceが直接Repositoryの構造体に依存することで、以下の問題が発生しました。
- テストを書くにはデータストアの構築が必要になる
- 具体的な技術や外部ライブラリの概念が上位のレイヤに漏れ、差し替えが難しい
これを背景に、Pairsのエンジニアがリアーキテクチャを図りながら、エンゲージの設計に取り掛かかりました。まずは「アーキテクチャから何を求めるか」から考え始めました。
アーキテクチャの「WHY」
アーキテクチャからは、マシン上のパフォーマンス向上は期待できませんが、エンジニアがアプリケーションのコードを考えたり整理したりを補助するメンタルモデルになります。
アプリケーションとチームのニーズを想定した設計は、レイヤの責務と依存関係、各修正の影響範囲などが分かりやすくなります。アーキテクチャは、新規機能の開発を指導する論理的なフレームワークとなり、開発の効率向上に大きく繋がります。
Pairsチームが使いこなしていたパターンはこの思想を大いに実現していましたが、「パターン思考」から「レイヤ思考」に切り替え、「責務」を明確にする必要がありました。
レイヤのおさらい
レイヤとはシステムを分割するための技法です。モジュールやパッケージで機能の詳細な実装を隠蔽することで関心の分離を可能にします。
MVCのような、レイヤの定義が決まっているアーキテクチャもありますが、それ自体とその制限は設計者により定義されるものです。レイヤの制限は、プログラミング言語レベル(非公開な変数)でも、チーム内の規約上(レイヤXはレイヤYからインポートしない)でも守られることがあります。
各レイヤには明確な責務があり、その他のレイヤと特定な依存関係があります。責務を明確にすることで影響範囲が分かりやすいですし、依存関係を守ればこの範囲は膨らみません。レイヤ責務を考慮すれば、レイヤ内でどの型、パターン、ライブラリなどを使うべきかも判断できます。
エンゲージのアーキテクチャ
エンゲージでは、Pairsでうまくいっていたパターンを取り入れながら、「レイヤ思考」でビジネスロジックとインフラへの依存性の課題をクリアできる設計にしてみました。その考え方を簡単に紹介します。
ビジネスロジックから考える
ビジネスロジックをアプリケーションの中心に設計を考えるアプローチはドメイン駆動設計から広がり、今ではその概念が色々なプログラミング言語のコミュニティに浸透しています。
エンゲージでは「Domain」レイヤを最初に定義し、業務領域(ドメイン)に関する値と振る舞いの管理を責務にしました。他のレイヤや外部のライブラリとインフラには一切依存しないレイヤです。
Domainを扱ってユースケースを実現するレイヤとして「Application」レイヤがあり、具体的な技術に依存せず、Domainと共にアプリケーションのコアになります。
アプリケーションと技術の分離
ソフトウェア設計には、persistence ignorance(永続化の無知)という概念があり、特に新しいものではありません。
「Persistence Ignorance」とはデータの永続化に対して、特定のテクノロジーやプロダクトに依存しないことをさしています。
これを永続化だけではなく具体的な技術全体に当てはめれば、上に説明した「コア」を実現できます。エンゲージではDomainに必要なインターフェイスがDomain自体に定義され、それを満たす構造体がInfrastructureレイヤに実装されます。
これは依存関係逆転の原則をアーキテクチャレベルで表した設計です。依存性の注入をすべてmain.go
で以下のように行います。結果としてテストの構築がシンプルになりました。
// インフラストラクチャ層
tx := mysql.NewTx(orm)
userRepository := mysql.NewUserRepository(orm)
// ドメイン層
userService := user.NewUserService(userRepository)
// アプリケーション層
userFacade := application.NewUserFacade(tx, userService)
// API層
userController := controllers.NewUserController(userFacade)
最後にレイヤの概要とパッケージ構造を簡単に紹介します。
レイヤの概要
API:APIの入出力に関わる処理
- Controllers
- ミドルウェア
- リクエスト・レスポンスのパース、バリデーション、整形など
Application:ユースケースの実装
- Facades(1メソッド=1ユースケース)
- トランザクション管理
Domain:業務領域に関する値と振る舞い
- 仕様に関わる型(エンティティや値オブジェクト)と値(定数)
- レポジトリのインターフェイス(実装はInfrastructure層にある)
- ドメインサービス(標準Goのビジネスロジックのみ)
Infrastructure:具体的な技術による実装
- レポジトリの実装
- 具体的な技術を必要とするDomainサービスの実装
- 外部ライブラリへの依存性(ORM、AWSのSDK、Redisのクライアントなど)
パッケージ構造
レイヤごとのパッケージに分けられ、必要最低限のものを公開したり、チーム内でルールを作り共通認識を持つことで、依存関係を守っています。
├── api
│ └── controllers
│ └── user_controller.go
│ └── middleware
│ └── routes
├── app
│ └── facades
│ └── user_facade.go // ユースケースの実装
├── domain
│ └── payment
│ └── user
│ └── user.go // エンティティ
│ └── user_repository.go // レポジトリ(インターフェイス)
│ └── user_service.go // サービス(実装)
├── infra
│ └── mysql
│ └── user_repository.go // レポジトリ(実装)
│ └── redis
│ └── s3
まとめ
今回はPairsエンゲージのサーバーサイドの初期設計に関する話を書きました。
リリースしたあとの感想としては、改善したいところはたくさんありますが、今回のアーキテクチャによってチームの生産性が支えられ、今後のリファクタリング、新規開発、テスタビリティを担保できました。
オーバーエンジニアリングよりも、分かりやすくチーム全体に取り入れられる設計が最終的にはメンタルモデルとして機能すると感じました。
参考
- What is a Layer? — Medium
- Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える — SlideShare
- 「実践ドメイン駆動設計」 から理解するDDD (2018年11月) — SlideShare
- DDD x CQRS 更新系と参照系で異なるORMを併用して上手くいった話 — SlideShare
- Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート — BASE開発チームブログ
- Designing a DDD-oriented microservice | Microsoft Docs
- Infrastructure Ignorance — Ayende @ Rahien
Feature image by Serkan Turk from Pixabay