entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

connect-goとsqlboilerで構築するクリーンアーキテクチャ

こんにちは、アドプラットフォーム事業部で広告エンジニアをしている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つのレイヤー構造を持つことが特徴です。

  1. Enterprise Business Rules(domain層)
  2. Application Business Rules(usecase層)
  3. Interface Adapters(interface層)
  4. 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を直接呼び出していることがわかると思います。このような工夫により、クリーンアーキテクチャにおける制約に対応しながら、実装を進めています。

最後に

今回の記事では、広告管理サーバーにおけるクリーンアーキテクチャを実践したなかで、考慮したポイントを紹介しました。

本記事で紹介したアーキテクチャは現段階のものであり、これからもリファクタリングを行いながら、より良いアーキテクチャを目指していきます。

hirocy
2023年新卒入社。広告なんでもチームとpixiv Adsチームを兼務し、ピクシブの様々なサービスにおける広告開発を担当。