Slide 1

Slide 1 text

Goバックエンド 標準化プロジェクトの 取り組み 株式会社QualiArts 島田 裕司

Slide 2

Slide 2 text

島田 裕司 / Shimada Yuji - 2013年 株式会社サイバーエージェント入社 - 現在はQualiArtsにてスマートフォン向けゲーム 開発を担当 - Go歴は5年程度 - 社内のGoコミュニティ「SGE.go」を運営 The Go gopher was designed by Renée French.

Slide 3

Slide 3 text

ゲーム・エンターテイメント事業部(SGE) 子会社制をとっており、 ゲーム・エンターテイメント事業に 携わる9社の子会社が 所属しています。 ゲーム・エンターテイメント事業部(SGE)

Slide 4

Slide 4 text

Contents 1. バックエンド標準化プロジェクトとは 2. 標準化プロジェクトにおけるADRの活用 3. まとめ

Slide 5

Slide 5 text

1 バックエンド標準化 プロジェクトとは

Slide 6

Slide 6 text

QualiArtsにおける状況 ✔ 複数プロジェクトの開発を経てGoでの基盤・ツール開発が安定 ・Goにおけるノウハウが一定蓄積された ・基盤やツールの実装方法が成熟 ✔ 開発が進むと基盤に手を入れるのが難しい ・影響範囲が大きくなる ・機能開発と比較して優先度が下がってしまう ✔ 運用管理ツール開発には時間がかかる ・0から作ると半年以上かかる ・ある程度開発が進んだ段階でも、管理ツールの改善が必要になるケースもある

Slide 7

Slide 7 text

バックエンド標準化プロジェクトとは? ✔ 新規プロジェクト用のテンプレートリポジトリを構築する取り組み ✔ 実装が安定した開発基盤や運用管理ツールをテンプレート化 ✔ ゲーム仕様を意識すること無く実装することが可能

Slide 8

Slide 8 text

テンプレートリポジトリの提供機能 • 認証 • 管理操作ログ • ダッシュボード • 開発環境管理 • マスターデータCRUD • スプレッドシート インポート/エクスポート • マスターデータ同期 • マスターデータタグ • トランザクションデータ CRUD • ユーザーデータ • ユーザー検索 • IAMロール • マスターバリデーション • Webviewエディタ • テーブル定義 / Enum定義 / API定義 参照 • アセットマネージャー • 課金残高履歴参照 • クライアントマスター参照 • サーバーマスター参照 • ユーザースナップショットマネージャー • APIテスター 運用管理ツール

Slide 9

Slide 9 text

テンプレートリポジトリの提供機能 共通基盤 • エラー • トレース・メトリクス • ロギング • コンテキスト • CORSミドルウェア • ファイルサーバー • バリデーションインターセプター • アクセスログインターセプター • 認証トークンキャッシュ • ランキング • ロック • 報酬 / 消費 • 条件 API基盤 • マスタ取得 • 認証 • ユーザー作成 • ユーザーログイン デバッグ基盤 • コマンド一覧 • コマンド実行

Slide 10

Slide 10 text

テンプレートリポジトリによる恩恵 ✔ QualiArts新規プロジェクトもテンプレートリポジトリをベースに開発 ✔ およそ6人月程度の工数削減を実現 ✔ 利用者からFBを収集し、テンプレートリポジトリの機能や運用を改善

Slide 11

Slide 11 text

なぜライブラリ/FWではないのか ✔ 過去には多くの共通ライブラリ/FWを輩出 ✔ 同時にその衰退も経験 ・プロジェクト固有仕様に対応する無理な拡張 ・技術トレンドの変化 ・メイン開発者の不在 ✔ メンテナンスのコストが肥大化 ✔ ゲーム開発の大規模化 ✔ 開発期間は2,3年以上、開発人数も100人を超える ✔ 並行して複数プロジェクトを進める機会が減少

Slide 12

Slide 12 text

なぜライブラリ/FWではないのか ✔ 過去には多くの共通ライブラリ/FWを輩出 ✔ 同時にその衰退も経験 ・プロジェクト固有仕様に対応する無理な拡張 ・技術トレンドの変化 ・メイン開発者の不在 ✔ メンテナンスのコストが肥大化 ✔ ゲーム開発の大規模化 ✔ 開発期間は2,3年以上、開発人数も100人を超える ✔ 並行して複数プロジェクトを進める機会が減少 → 共通ライブラリ/FWに縛られるより、良い部分をテンプレートに入れて次に活かす

Slide 13

Slide 13 text

テンプレートリポジトリの運用 ADR(Architecture Decision Record)をもとに週に一度議論を行う 決定事項の反映やその他必要機能の拡充

Slide 14

Slide 14 text

2 標準化プロジェクトに おけるADRの活用

Slide 15

Slide 15 text

Architecture Decision Record(ADR) ✔ ソフトウェアアーキテクチャにおける重要な意思決定を記録する文書 ✔ コンテキスト、決定内容、理由を明記し、透明性と将来の参考に役立つ ✔ プロジェクトの全体像を明確にし、チーム内で共有するために使用される

Slide 16

Slide 16 text

Architecture Decision Record(ADR) ✔ ソフトウェアアーキテクチャにおける重要な意思決定を記録する文書 ✔ コンテキスト、決定内容、理由を明記し、透明性と将来の参考に役立つ ✔ プロジェクトの全体像を明確にし、チーム内で共有するために使用される → 標準化プロジェクトではADRを用いて方針の議論、決定を行っている ADRの管理にはGitHub Discussionsを利用

Slide 17

Slide 17 text

標準化プロジェクトにおけるADRの一例 ✔ 技術選定 ・通信フレームワークにconnect-goを使用する ・DIツールにuber-go/fxを使用する ・定義とコードジェネレーターにProtocol Buffersを使用する ✔ パッケージ構成・コーディング規約 ・パッケージ名は標準パッケージと被らない名前にする ・ユースケース層のファイル配置を /usecase/xxx/usecase.go とする ・パッケージ名・ディレクトリ名はスネークケースにする ✔ テンプレートリポジトリの運用 ・技術選定時には過去プロジェクトの資産も考慮に入れる ・GitHub Issuesには明確にやることのみを起票する ・IssueとPRをReferencesで紐づける

Slide 18

Slide 18 text

ADR事例. テストコードの記述フォーマット

Slide 19

Slide 19 text

背景1. Go経験が浅くテスト実装が手探り ✔ はじめてのGoプロジェクトでGo経験が浅く各メンバーが手探りで記述 ✔ テストケースごとにテスト関数を分割 or テーブルドリブンテスト(TDT) ✔ TDTで記述する場合も何をテーブルに含めるがバラバラ ✔ テスト対象関数/メソッド戻り値のチェック粒度が異なる

Slide 20

Slide 20 text

背景1. Go経験が浅くテスト実装が手探り ✔ はじめてのGoプロジェクトでGo経験が浅く各メンバーが手探りで記述 ✔ テストケースごとにテスト関数を分割 ✔ テスト対象関数/メソッド戻り値のチェック粒度が異なる ✔ テーブルドリブンテスト(TDT)で記述する場合も書き方がバラバラ → ✔ TDTで記述を統一する機運が高まる ✔ 最低限のフォーマットも合わせる

Slide 21

Slide 21 text

背景2. TDTと相性が良くないテスト ✔ ゲームのコアロジック ✔ 1ケースのモック呼び出しで数百行に及ぶ ✔ モックの制御を含めたTDTの記述が難しい ✔ ケース毎に処理の分岐を行うテストが爆誕 ✔ util関数等のシンプルな処理 ✔ 1テストケースが1行で済むのでTDTで記述するまでもない

Slide 22

Slide 22 text

背景2. TDTと相性が良くないテスト ✔ ゲームのコアロジック ✔ 1ケースのモック呼び出しで数百行に及ぶ ✔ モックの制御を含めたTDTの記述が難しい ✔ ケース毎に処理の分岐を行うテストが爆誕 ✔ util関数等のシンプルな処理 ✔ 1テストケースが1行で済むのでTDTで記述するまでもない → ✔ 無理にTDTとしない ✔ モック呼び出しが複雑な場合、テスト関数内で複数サブテストを羅列 ✔ 処理がシンプルな場合、サブテストを使わず各ケースを羅列

Slide 23

Slide 23 text

func TestUsecase_Login(t *testing.T) { t.Parallel() // モック呼び出しが複雑な場合、テスト関数内で複数サブテストを羅列 t.Run("新規ユーザー", func(t *testing.T) { t.Parallel() ... assert.Equal(t, want, got) }) t.Run("既存ユーザー", func(t *testing.T) { t.Parallel() ... assert.Equal(t, want, got) }) } テスト関数内で複数サブテストを羅列

Slide 24

Slide 24 text

func TestSplitComma(t *testing.T) { t.Parallel() // 処理がシンプルな場合、サブテストを使わず各ケースを羅列 assert.Equal(t, []string{"a"}, SplitComma("a")) assert.Equal(t, []string{"a", "b", "c"}, SplitComma("a,b,c")) assert.Equal(t, []string{}, SplitComma("")) } サブテストを使わず各ケースを羅列

Slide 25

Slide 25 text

背景3. 複数のテスト記述方法が混在 ✔ 新たに参画したメンバーがどのテストを参考にすれば良いか分からない ✔ 既存の機能の改修を行う際にテスト記述を解読する必要がある

Slide 26

Slide 26 text

背景3. 複数のテスト記述方法が混在 ✔ 新たに参画したメンバーがどのテストを参考にすれば良いか分からない ✔ 既存の機能の改修を行う際にテスト記述を解読する必要がある → プロジェクトの初期からテストコードの記述フォーマットを統一したい

Slide 27

Slide 27 text

テストのフォーマット統一 課題/目的 ✔ 対象 テストコード ✔ 課題・理想状態 ・書き手に依るテストコードのブレがなく、フォーマットが統一されている状態 ・コーディング/リーディング の負担を抑える ・新規メンバーがどのテストを参考にするか迷わないようにする ✔ 目的 ・テストコード記述方式の統一 ・TDテストによるデータと処理の分離、処理の共通化

Slide 28

Slide 28 text

テストのフォーマット統一 1. 1つの関数・メソッドに関するテストは1つのテスト関数に集約 ・基本はTDTを用いる ・複雑なモック制御等、非TDTの方が可読性が高い場合、サブテストを羅列する ・util関数等のごく簡単なテストであれば、サブテストを用いずに記述しても良い

Slide 29

Slide 29 text

テストのフォーマット統一 2. 各テスト関数にて次のフィールドを持つtestCase構造体を定義する ・Name: テスト名 ・Args: 引数(引数がない場合も空で定義) ・Prepare: モック呼び出しを記述する関数 ・Returns: 戻り値(戻り値がない場合も空で定義) ・(Updated: 引数の更新後の値)※省略可、破壊的な変更を行う場合に利用

Slide 30

Slide 30 text

// Case テストケースの構造体 type Case[args, mocks, returns any] struct { Name string Args args Prepare func(args args, mocks *mocks) Returns returns } // Cases テストケース群の構造体 type Cases[args, mocks, returns any] []Case[args, mocks, returns] テストケースのジェネリクス定義 testutil.go

Slide 31

Slide 31 text

// mocks モックの一覧 type mocks struct { cardService *card.MockService consumptionService *consumption.MockService rwTx *database.MockRWTx } // newWithMocks テスト対象と各種モックを生成 func newWithMocks(t *testing.T) (Usecase, *mocks) { m := &mocks{ cardService: card.NewMockService(t), consumptionService: consumption.NewMockService(t), rwTx: database.NewMockRWTx(t), } return NewUsecase(m.cardService, m.consumptionService), m } テスト対象と各種モックの生成

Slide 32

Slide 32 text

引数と戻り値の定義 func TestUsecase_LevelUp(t *testing.T) { t.Parallel() type args struct { ctx context.Context userID string cardID string level int } type returns struct { err error } // テストケースの記述とテスト実行 ... }

Slide 33

Slide 33 text

cases := testutil.Cases[args, mocks, returns]{ { Name: "正常", Args: args{ ctx: context.Background(), userID: "userID", cardID: "cardID", level: 10, }, Prepare: func(args args, m *mocks) { m.cardService.EXPECT(). LevelUp(args.ctx, m.rwTx, args.userID, args.cardID, args.level).Return(dto.Resources{}, nil).Times(1) m.consumptionService.EXPECT(). Consume(args.ctx, m.rwTx, args.userID, dto.Resources{}).Return(nil, nil).Times(1) }, Returns: returns{}, }, } テストケースの記述

Slide 34

Slide 34 text

for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { t.Parallel() u, m := newWithMocks(t) tc.Prepare(tc.Args, m) err := u.LevelUp(tc.Args.ctx, tc.Args.userID, tc.Args.cardID, tc.Args.level) testutil.EqualError(t, tc.Returns.err, err) // 戻り値があればアサーション e.g. assert.Equal(t, tc.Returns.xxx, xxx) }) } テストの実行

Slide 35

Slide 35 text

ADRによる恩恵 ✔ プロジェクトとしての方針を明示し認識を統一できる ✔ PRレビューにおいて参照リンクとして利用できる ✔ 新メンバーへのオンボーディングに活用できる

Slide 36

Slide 36 text

ADRのデメリット ✔ ADRの記述コストが掛かる ✔ ADRでの決定に縛られる可能性がある

Slide 37

Slide 37 text

ADRのデメリット ✔ ADRの記述コストが掛かる → 方針やその経緯を都度調べるのもコストは掛かる   それらを一元管理できるADRは資産 ✔ ADRでの決定に縛られる可能性がある → 既存のADRを必ずしも正とはしない   状況に応じて柔軟に変更していく

Slide 38

Slide 38 text

3 まとめ

Slide 39

Slide 39 text

まとめ • 新規プロジェクト用のテンプレートリポジトリを構築している • 運用管理ツールや開発基盤の標準化により開発の効率化を図っている • ADRを活用して方針の議論・記録を行っている

Slide 40

Slide 40 text

ありがとうございました