はじめに

 初めまして!2024年11月にCATechJOBプログラムで株式会社AbemaTV BD本部開発局(以下BD)で1ヶ月間インターン生として参加した中尾一心です。

九州大学大学院システム情報科学府の修士1年生でフェイクニュースに関する研究を行っています。

 

インターン先のチームについて

私が配属された株式会社AbemaTV BD本部開発局では主に広告に関する事業を行っています。

その中でも私は純広告の案件管理〜配信設定業務のDX化を主務とするDX_Groupでシステムの改善業務に取り組みました。

 

インターンでやったこと

インターンでは以下の2つのタスクに取り組みました。

  1. 広告配信用のマスタデータをAPIで管理するためのアーキテクチャ設計
  2. 広告の入稿設定を再実行するためのSlackAPI

 

広告配信用のマスタデータをAPIで管理するためのアーキテクチャ設計

背景

Abemaの広告チームでは現在、Abemaの広告配信設定のためのマスタデータをスプレッドシートで管理しています。しかしながら、運用上ミスが起きることもあり、容易に変更されたくない項目も追加されるなど、ある程度制限したいことが多くなっていました。そこで、本インターンではこのマスタデータをAPI経由で管理できるようにアーキテクチャの設計から実装まで一通り行うことを目標としました。

 

開発設計

技術選定

技術選定にあたって

  • プログラミング言語
  • リクエストを受け取るAPI
  • データを保存しておくDB
  • DBを操作するためのライブラリ
  • ローカルでの開発環境

の5つを考える必要がありました。

結論としては

  • プログラミング言語: Go言語
  • API通信:gRPC
  • DB: mySQL
  • DB操作: sqlc
  • 開発環境:docker + air(ホットリロード)

となりました。

これからこのように選んだ理由を説明します。

 

プログラミング言語

今回のインターンではGo言語の勉強を目標にしていたためGo言語を使用しました

 

API

BDではそれぞれのマイクロサービス単位でgRPCを使用しており、フロントからのリクエストもgRPCで行われていることからAPIとしてはgRPCを採用しました。

 

gRPCとはRemote Procedure Callという遠隔地にあるサーバーでもローカルにある関数のように呼び出すことのできる仕組みでgoogleがオープンソースとして公開している技術です。

既存のRESTAPIに比べ

  • HTTP/2で実装されている
  • データフォーマットがバイナリ形式であるためハイパフォーマンスである
  • 双方向に通信が可能でありストリーミング通信などが可能である
  • 他言語に対応したコード生成がある

などのメリットがあります。

[What is gRPC?]

 

gRPCはprotoと呼ばれるスキーマを定義するファイルを作成することで自動でGoのコードを生成することができます。[詳細]

 

DB

スプレッドシートと同期が取られているデータベースがすでに存在しており、MySQLで構築されていたためそのまま流用することにしました。

 

DB操作用のライブラリ

sqlcを採用しました。BDでは主にgormが使われていたのですがgormではメソッドチェーンを使ってGoコードからSQLコードを生成できる一方で柔軟にSQLを書くことが難しいというデメリットがありました。そこで本インターンでは私が新たなDB操作ライブラリ開拓の第一歩としてsqlcに挑戦することにしました。

 

sqlcはgolang向けのDB操作ライブラリで一般的に有名なgormやSQLBoilerと違ってGoコードからSQLを生成するのではなくSQLコードからGoコードを生成します。

  • クエリビルダーがないので処理速度が速い
  • SQLがわかればよく、学習コストが低い

などのメリットがあります。

また、これは結果論なのですがdomain層をなくしたことによってORMが本来の機能(DBとソースコード内での型のマッピング)を果たさなくなったため、DB志向で自動でデータベース側の型まで作ってくれる点はとてもよかったと思います

例として以下のようなUserテーブルを考えます。

sql

CREATE TABLE IF NOT EXISTS User (

  `id` bigint NOT NULL AUTO_INCREMENT,

  `name` varchar(255) COLLATE utf8mb3_bin NOT NULL,

  `created_at` datetime(3) DEFAULT NULL,

  `updated_at` datetime(3) DEFAULT NULL,

  PRIMARY KEY (`id`)

);

これにUserをGetするSQLクエリを書くと

sql

-- name: GetUser :one

SELECT * FROM users

WHERE id = $1 LIMIT 1;

 

以下のようなGoコードが生成されます

go

const getUser = `-- name: GetUser :one

SELECT id, name, created_at, updated_at FROM users WHERE id = ? LIMIT 1

`

func (q *Queries) GetUser(ctx context.Context, id int64) (*User, error) {

row := q.queryRow(ctx, q.getUserStmt, getUser, id)

var i User

err := row.Scan(

&i.ID,

&i.Name,

&i.CreatedAt,

&i.UpdatedAt,

)

return &i, err

}

 

SQLを追加するだけでDB操作に必要なGoコードが生成できるため手間がほとんどかからないという点は多くのテーブル、カラムを扱う上で非常に役に立つと思います。

 

開発環境

Dockerを使用して環境を統一した上で、dev環境用にホットリロードツールとしてairを導入しました。

 

ホットリロードという言葉をご存じでしょうか?

ホットリロードとはコードが変更されてもアプリを再起動させることなくリアルタイムに反映してくれる機能のことです。特にフロントエンドやネイティブでUIを開発している人はこの機能をよく活用している気がします。

今回私が担当したプロジェクトではホットリロード環境が整備されておらずエンジニアは毎回コマンドを叩き直してアプリを再実行していました。そこで、golangのホットリロードライブラリとして有名なairを導入することを決意し導入しました。

airでは.air.tomlというファイルにホットリロード対象のファイルや実行する環境、フォルダなどを指定することができ柔軟に対応することができます。

詳しくは公式のGithubを参考にしてください。

また私が今回の環境整備にあたって参考にした記事も載せておきます

[GolangのホットリロードはAirを使おう]

 

dockerと組み合わせて使う場合の注意点としてはバインドマウントでファイルをマウントしておくことです。ローカル環境と異なり、dockerはイメージが構成されるとその中だけで完結してしまうためローカルの変更がdocker側に反映されなくなります。そのためビルド時にはCOPYで必要なファイルを持ってきてビルドする(go mod tidy)と思いますがその後バインドマウントでフォルダを上書きしてあげる必要がある点には注意が必要です。

 

アーキテクチャ設計

層の設計

当初の設計では4層レイヤードアーキテクチャを採用した上でDDDにするかどうかという部分を議論するつもりでした。しかし、実際に検討してみると、1. DBがすでに存在する 2. 主にCRUD処理だけ という点から複雑なビジネスロジックを管理するdomain層をつくるメリットが薄いことに気付き、またそれならば無闇にレイヤーを増やすよりも入力を受け付ける部分(interface)と出力する部分(infrastructure)だけのシンプルな構造にしてしまったほうがコードの変更が少なく、運用面として楽になると考え、最終的に2層のみからなるアーキテクチャ構成にしました。

フォルダ構造

最終的には以下のような構成になっています。

interface層は入力を受け取るだけと言っても多少のバリデーション処理が含まれる可能性があるため意味を広く持たせるためにappというフォルダ名にしています。

.

├── cmd/

│   └── main.go // mainとなる実行ファイル

├── configs/

│   ├── config.go // sqlなどのconfig

│   └── env.go // 環境変数

├── docs/

│   └── sql/

│       ├── init/

│       │   └── **.sql // database初期化用のsqlファイル

│       ├── migrations/

│       │   └── **.sql // migration用のsqlファイル

│       └── [e.g.]query/

│           └── **.sql // sqlc用のコード生成用クエリsqlファイル

├── internal/

│   ├── app/

│   │   └── **.go // データの受け取り/バリデーション/変換を行うファイル

│   ├── logic/

│   │   ├── **.go // バリデーション等のロジックを追加する

│   │   └── **_test.go // ロジックのテストコードなどを書く

│   └── infrastructure/

│       ├── [e.g.]models.go // sqlcによる自動生成

│       ├── [e.g.]db.go // sqlcによる自動生成

│       ├── [e.g.]querier.go // sqlcによる自動生成

│       ├── [e.g.]{table_name}.go // sqlcによる自動生成

│       └── **.go

├── pkg

├── Dockerfile

├── docker-compose.yml // local用DBとか

├── go.mod

├── go.sum

├── Taskfile

└── [e.g.]sqlc.yml // sqlcを使う場合

運用負荷をさらに下げるためのCopierの導入

domain層はなくなりましたがそれでも手間のかかる問題が一つだけ残っています。

それはinterface層でのリクエストの型とinfrastructure層でのDBに格納するための型が異なることです。この部分を解消しなければ、例えばカラムを1つ追加したいとなった時にinfrastructure層(sql)、interface層(proto)にそれぞれカラムを追加した後interface層で型をマッピングするためにコードを追加しなければなりません。これはテーブルやカラムの数が増えるほど手間でありエンジニアにとって無駄な時間です。

そこで最初はpythonのアンパッキングやjsのスプレッド構文による代入などに近い操作がないか考えました。しかしgo言語は非常にシンプルに作られており実現できそうにありませんでした。しばらく調べ回っていたあとCopierというライブラリが構造体の型変換に有用であることがわかりました。このライブラリではそれぞれの型で名前が同じであればコピーしてくれるという優れ物です。このライブラリのおかげで、カラムを追加する際にはGoのコードを一切触らなくとも追加できるようになりました。(実際にはバリデーションとかロジックとかあるかもしれませんが型の変換という点においては有用です。)

 

結果

今回フロントエンドまでの実装はリソースの都合上実現できませんでしたが、ローカルでテストを行い、正しくマスタデータをCRUDできるAPIを実装できたことを確認しました。

広告の入稿設定を再実行するためのSlackAPI

背景

BD本部では、受領した広告案件をオペレーションチームが手動で配信設定していた業務をシステムで自動化するDXが進められていましたが、自動設定が一時的な通信障害等で失敗した場合の再実行手段が整っていないことが課題でした。

そこで、本インターンでは広告配信設定を再実行できるシステムを構築しました。

 

開発設計

技術選定

  • 再実行リクエストを送るためのSlack
  • Slackからリクエストを受け取るためのRESTAPIサーバー

 

配信設定を再実行するために、今回はSlack-botを使用しました。理由としては、自動配信設定の成功/失敗通知をSlackで行なっており、その結果を元に再実行を行うため、同じslackで行なった方が運用負荷が小さいと判断したからです。

 

また、システム間の通信は既存の方法を用いてgRPCを使用しておりますが、SlackのリクエストをgRPCとして送るにはリクエストパラメータの数が多く、運用負荷が上がると考えたためそこだけRESTAPIを採用しました。

 

通信イメージ

 

実装部分は一般的なSlackAPIと変わらないため省略します。

内容としてはRESTAPIでSlackBotからのリクエストを受け取った後、gRPCを使用してAdサーバーにリクエストを送るという流れです。

結果

SlackBotにメンションを送ることで広告配信を再設定できました!

インターンでの学び

既存のアーキテクチャに捉われない

今回のインターンで学べた内容として一番大きかったのはこれだとおもいます。

インターンをする前はGoに関するアーキテクチャを調べていてレイヤードアーキテクチャとかDDDとかクリーンアーキテクチャとかどれがいいんだろうというのを考えていました。しかし、実際に参加してみると自分で要件定義から任されてトレーナーなどのBDのエンジニアたちにも最適なアーキテクチャを一緒に考えていこうと言われました。

みなさんと議論していくうちにここは良い、ここはダメ、と自然に最適なアーキテクチャが決まっていったのがとても印象に残っています。

アーキテクチャは設計手法の1つであってそれが全てではない(最適なものはそれぞれにある)ということを認識できたと思います。

 

出社とリモートのメリットデメリット

私は今回出社2週間、リモート2週間でインターンに参加しました。

出社している間のメリットとしては隣にトレーナーがいるためいつでも聞きやすい環境だったこと、リモートのメリットとしては家で作業できるため集中できたことです。逆にデメリットとしてはリモート環境だと誰がいつmtgしてるかわからず、その時誰に聞くのが最適なのかがわからないところです。特にインターンでは全く知らないドメインに飛び込むためわからないことが多く、インターンとしては全日出社できた方がよかったなと思っています。

おわりに

インターンを始める前はGoの基礎文法程度しか知らなかったのですが、1ヶ月でアーキテクチャ設計から実装まで挑戦する機会をくださったAbemaTV BD本部の皆さんにはとても感謝しています。

また、親切に教えてくださったトレーナーの寺坂さんを始め、timesで聞くといつでも返信していただいたBDエンジニアのみなさん、ありがとうございました!