こんにちは、アドプラットフォーム事業部で広告エンジニアをしているhirocyです。
本記事は、2024年9月20日に開催された「PIXIV DEV MEETUP 2024」で行ったLTをもとに作成しました。ぜひ資料と併せてご覧ください。 speakerdeck.com
はじめに
ピクシブの広告プラットフォームは、広告管理サーバーと広告配信サーバーに分かれており、それぞれRuby on RailsとGo言語で実装されています。現在、広告管理サーバーをGo言語でリプレイスするプロジェクトが進行中で、可能な限りクリーンアーキテクチャを採用して開発を進めています。
本記事では、広告管理サーバーにおけるクリーンアーキテクチャの実践について、考慮したポイントを共有します。 まず、クリーンアーキテクチャの概要およびconnect-goやsqlboilerの概要を説明します。その後、実際に広告管理サーバーを設計した際のディレクトリ構成を紹介し、connect-goとsqlboilerをクリーンアーキテクチャに取り込む際の工夫について詳しく解説します。
クリーンアーキテクチャとは
クリーンアーキテクチャは、Robert C. Martinが2012年に提唱したアーキテクチャの概念です。このアーキテクチャは、以下の4つのレイヤー構造を持つことが特徴です。
- Enterprise Business Rules(domain層)
- Application Business Rules(usecase層)
- Interface Adapters(interface層)
- Frameworks & Drivers(infrastructure層)
本記事では、それぞれをdomain層、usecase層、interface層、infrastructure層と呼びます。
connect-go・sqlboilerとは
connect-goの特徴
connect-goは、Connectという通信プロトコルのGo実装であり、Protobufという形式で記述された通信の定義に基づいてGoのパッケージを生成してくれるライブラリです。
sqlboilerの特徴
sqlboilerは、実際のDBスキーマからGoのORM(Object-Relational Mapping)をパッケージとして生成してくれるライブラリです。
広告管理サーバーのディレクトリ構成
広告管理サーバーの最終的なディレクトリ構成は以下のようになりました。
. └── internal ├── domain │ └── user.go ├── infrastructure │ ├── db │ └── grpc ├── interface │ ├── controller │ │ └── grpc │ │ ├── interceptors │ │ ├── (connect-goの生成物) │ │ ├── presenter │ │ └── user.go │ └── gateway │ ├── (sqlboilerの生成物) │ ├── presenter │ └── user.go └── usecase ├── repository │ └── user.go └── user.go
クリーンアーキテクチャの各レイヤーごとにディレクトリを分けて実装されていることがわかります。
パッケージはinterface層の中に生成しています。
interface層に生成を行った理由
クリーンアーキテクチャに基づいて実装を進める中で、例えばconnect-goに関連する部分については以下のように理解しました。
- interface層
- 通信の中身を扱うレイヤー
- リクエストの内容をusecase層が求める形に変換
- usecase層の結果を変換し、レスポンスとして返す
- 通信の中身を扱うレイヤー
- infrastructure層
- 通信そのものを扱うレイヤー
- 例:gRPC通信の具体的な処理
- 通信そのものを扱うレイヤー
このため、今回のケースでは、ハンドラーやDBクエリはinterface層に記述するのが適切だと判断しました。それに伴い、必要となる生成物もinterface層に生成することにしました。
connect-go・sqlboilerを扱う上での工夫
最後に、クリーンアーキテクチャの中でconnect-goとsqlboilerを扱う際に工夫した点について紹介します。
sqlboilerを扱う上での工夫
sqlboilerの特徴として、クエリの結果が独自の型で返ってくることが挙げられます。これをクリーンアーキテクチャに取り込む際には、domain層との間でデータの変換が必要となります。この変換をスムーズに行うために、converterをgateway内に作成して対処しました。
また、sqlboilerの実行には*sql.DBが必要です。そのため、infrastructure層で接続を確立した*sql.DBを返すように実装し、それをsqlboilerに渡してクエリを実行できるようにしています。
connect-goを扱う上での工夫
connect-goに限らず、Echoなどのライブラリでも共通して言えることですが、connect-goでは決められたハンドラーの型に従ってメソッドを実装する必要があります。これは、クリーンアーキテクチャにおけるusecaseからpresenterを呼び出す流れと相性が悪く、そのままでは実装が難しくなります。
そこで、controller層から直接presenterを呼び出す形にアーキテクチャを変更しました。
func (h *UserHandler) GetUser( ctx context.Context, req *connect.Request[pamv1.GetUserRequest], ) (*connect.Response[pamv1.GetUserResponse], error) { u, err := s.userS.GetUser(ctx, presenter.ToDTOUserInput(req.Msg)) if err != nil { if errors.Is(err, domain.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, err) } return nil, errors.Wrap(err, "failed to get user") } return connect.NewResponse(presenter.ToGRPCUserResponse(u)), nil }
このメソッドはcontroller層に属しており、presenterを直接呼び出していることがわかると思います。このような工夫により、クリーンアーキテクチャにおける制約に対応しながら、実装を進めています。
最後に
今回の記事では、広告管理サーバーにおけるクリーンアーキテクチャを実践したなかで、考慮したポイントを紹介しました。
本記事で紹介したアーキテクチャは現段階のものであり、これからもリファクタリングを行いながら、より良いアーキテクチャを目指していきます。