Slide 1

Slide 1 text

1 社内フレームワークとその依存性解決 Go Far. #2 2025/01/28

Slide 2

Slide 2 text

2 Merpay Solutions team vvakame ♥🐈

Slide 3

Slide 3 text

3 社内フレームワークを作っている話 本日のお話 依存性解決で困った話 depmanというパッケージを作った話 技術書典とかで使ってみた話 02 03 04 01

Slide 4

Slide 4 text

4 社内フレームワークを作っている話

Slide 5

Slide 5 text

5 社内フレームワークを作っている話 ● メルペイではmonorailという社内フレームワークを作っています ○ v1 → 大昔ある人物によって生み出された ○ v2 → メルコインのマイクロサービスでガッツリ使われている ○ v3 → 現在3つのマイクロサービスで試用中→さらなる拡大の野望 ● vvakameは去年の5月末くらいから開発に参加 ○ おもしろそうなところの開発をサポートする気持ちだったが チームメンバーの離職もありTech Leadっぽい感じに… 🙀 歴史とかの話

Slide 6

Slide 6 text

6 社内フレームワークを作っている話 ● マイクロサービスなので疎結合!それぞれが自由にできるぞ! ○ 組織規模がでかくなるにつれ設計や運用の乖離がキツくなってきた ○ チーム内やチーム間で知見が共有できたほうがいいじゃん ○ 統一的な設計や運用への要求の高まり ○ そんなこんなで社内フレームワークが求められるようになった ○ 制約やベストプラクティスの集積や横展開など ○ “Learn Once, Write Anywhere” という標語が生まれた ■ 一回学べば色々なチームで働ける ■ 他のチームのマイクロサービスを容易に移管できる 経緯

Slide 7

Slide 7 text

7 ● Productチームのためのフレームワーク ○ ゆるいLayered Architecture などProductチーム各所で育てられてき たベストプラクティスをゆるく集約 ○ Productチームの要望を最重要視する ■ forkが生まれるのが最悪パターンなので避けたい ○ monorailの機能はどこかのProductチームで使われていたもの ● ArchitectチームやPlatformチームに詳細を移管(できるといいな) ○ ベストプラクティスはそれぞれ最適なチームで管理したい ○ Productチームの工数をなるべく使わずに土台の改善を行いたい 社内フレームワークを作っている話 ねらい

Slide 8

Slide 8 text

8 社内フレームワークを作っている話 Updatable ● テンプレートからforkした後のアップデートの追従にも課題が ● Productチームから見て低コストにアプデに追従したい ● MakefileやGitHub Actions Workflowやlintなどを共有したい ● Kubernetes Manifestのベストプラクティスをパッケージ化したい ● terraformの設定項目をパッケージ化したい ● このあたりを解決し、持続的に発展・共有可能な仕組みがほしかった ● 一部出来てるけど難しい箇所も多い ○ Goコードのアップデートは難しい ○ すでにガッツリできてるインフラにアドオンで何かするのが難しい

Slide 9

Slide 9 text

9 社内フレームワークを作っている話 それ以外にも … ● Architecture Decision Recordを導入しました ○ 技術書典 Unleash Mercari Tech! Vol.5 に収録 ● monorail-todo というテンプレMSがあり、みんなでこれを改善する ○ go get でmonorailを更新するとGitHub Actions上で `monorail update` が実行されファイル更新PRが自動作成される ● などの工夫が無数にあり、それぞれ面白いと思うけど社外公開は… ○ 社内インフラにべったり依存しているので公開しても誰も使えない ● 興味がある人がいれば懇親会で深堀りしにきてください

Slide 10

Slide 10 text

10 依存性解決で困った話

Slide 11

Slide 11 text

11 依存性解決で困った話 ● monorailはServerという単位でプロセス内に複数の実装を持てる ● Spanner(DB)のClientなど、Server間でシェアしたい要素がある ● シェアしたくないものもある ● 各パーツは別のものに依存している ○ Spanner → OpenCensus → Datadog Agent → 環境変数 ○ など… ● DIといえばgoogle/wireだが… ○ フレームワーク内で実装するので静的に解決するのが難しい ● 必要なものの生成を手動で解決していた ○ 当然だけどメンテが大変 当初の実装

Slide 12

Slide 12 text

12 依存性解決で困った話 ● 最終的にアプリ側から何が要求されるかはわからない ● 余計なものは生成せずに済ませたい ● 複数Serverで生成物をシェアしたりしなかったり制御したい ● インスタンスの生成をブラックボックス化せず追いやすくした ○ wireはそこが優秀! ● 循環参照が発生してもごまかす手段を用意しておきたい 考えたこと

Slide 13

Slide 13 text

13 depmanというパッケージを作った話

Slide 14

Slide 14 text

14 depmanというパッケージを作った話 ● github.com/vvakame/depman ○ 年末年始に実装しなおした私家版 ○ 社内版とは実装の詳細が異なります(私家版のほうが多分便利) ● DEPendency MANager 略して depman ○ ということにしてありますが… ● 発想元はJavaScriptのSymbol ○ symbolの公開範囲はデータの公開範囲制御と等価にできる 作ったもの

Slide 15

Slide 15 text

15 depmanというパッケージを作った話 ● 作りたい実装に対応する ResourceSpec を実装する ○ CreateResource(ctx context.Context) (T, CloseFn, error) を実装 ○ 依存関係が何もなければ、単にそのリソースを作って返す ○ 終了処理が必要ならそのための関数も返せる ● CreateResource の実行中に別のResourceSpecを使ってもよい ○ それぞれが欲しいものを要求すれば最終的に依存性解決できる ○ 同じものは2つ作りたくない キャッシュにあればそれを返す ● depman本体はリソース作成待ちとかをいい感じに調整する ○ 単にロック取ってリソースできたらGoするだけ ○ 循環参照は検出してエラーにする 基本仕様

Slide 16

Slide 16 text

16 depmanというパッケージを作った話 ● 共有範囲を制御しよう ○ 同じものって何? ■ 同じ型でも接続先が違うClientなら別物だよね とか ○ キャッシュ(map)のkeyが同じなら同じ、違うなら別 とする ● ざっくりした比較の仕様 ○ リテラル系は比較可能 ○ struct同士の比較は、全フィールドが同じなら同じ ○ pointer同士の比較は、同じ変数を指してたら同じ ● mapのkeyに使う型は比較可能である必要がある ○ 比較可能であればstructでもpointerでもよい Goの比較演算子の 仕様とかの話

Slide 17

Slide 17 text

17 depmanというパッケージを作った話 Example (go.dev/play/p/RyLTWfpVqYV) type StructType struct { Bool bool } func Test_goComparisonSpec(t *testing.T) { s1 := StructType{Bool: true} s2 := StructType{Bool: true} s3 := StructType{Bool: false} if s1 != s2 { t.Error("unexpected. s1 != s2") } if s1 == s3 { t.Error("unexpected. s1 == s3") } if &s1 == &s2 { t.Error("unexpected. &s1 == &s2") } }

Slide 18

Slide 18 text

18 depmanというパッケージを作った話 Example (go.dev/play/p/RyLTWfpVqYV) m := map[any]any{} m[s1] = "s1" m[s2] = "s2" // s1 == s2. overwrite m[s3] = "s3" m[&s1] = "&s1" m[&s2] = "&s2" m[&s3] = "&s3" specs := []struct { key any value string }{ {key: s1, value: "s2"}, // s2. not s1 {key: s2, value: "s2"}, {key: s3, value: "s3"}, {key: &s1, value: "&s1"}, {key: &s2, value: "&s2"}, {key: &s3, value: "&s3"}, }

Slide 19

Slide 19 text

19 depmanというパッケージを作った話 ● 次のような柔軟性を得られる ○ 設定値が同じだったら同じキャッシュを使うよね ■ PublicResourceSpec { Endpoint string } ■ ↑このResourceSpecを使うと接続先が同じなら同じ値が取れる ○ 誰とも共有したくないからkeyを隠すわ ■ &privateResourceSpec{} ■ ↑パッケージ外からはこのリソースにはアクセスできない ● 問題 ○ Goの仕様に依存しているため、一定の熟練度が必要になってしまう ResourceSpecをmapのkeyとする

Slide 20

Slide 20 text

20 depmanというパッケージを作った話 ● Exampleがいくつか用意してあります ○ pkg.go.dev/github.com/vvakame/depman#pkg-examples ■ RequestResource ■ フィボナッチ数を計算するやつ ■ SetResource ● 循環参照を解消(手動で解決)するときに使う 使い方

Slide 21

Slide 21 text

21 depmanというパッケージを作った話 ● monorailの起動プロセスをdepman管理に置き換えてみた ○ Pros ■ 何が何に依存しているかを記述できるようになった ■ Serverが必要としている値のみを生成できるようになった ■ Productの実装側からは見えないように隠蔽できた ○ Cons ■ 仕組みが複雑でメンテナンスにやや属人性を感じている ■ Product側へどの程度カスタマイズ性を提供するか線引が難 ● ないよりはあったほうが断然よいがこの設計で走り続けるかは注視が必要 社内で使ってみた話

Slide 22

Slide 22 text

22 技術書典とかで使ってみた話

Slide 23

Slide 23 text

23 技術書典とかで使ってみた話 ● 技術書典APIはGoで書かれています ○ 技術書典API アーキテクチャ という本に書きました ● google/wire を使っているけど課題があった ○ 循環参照が解決できない ■ ProductInfoRepo → ProductVariantRepo → … ○ 中間成果物が取り出せない ■ REST API, GraphQL Endpoint, IdP Endpoint etc… ■ 共有したいものは多いがほしい形が違う 使ってみた

Slide 24

Slide 24 text

24 技術書典とかで使ってみた話 ● wireのProviderとしてdepmanのResourceSpecを簡素にラップしたものを 登録 ○ 既存Providerをどんどん置き換えていくとwireが生成するコードが減って いく ○ やりきると、最終成果物が一発で出てくる ■ そうなったらwireを外してよい ○ wireはいらなくなったか? ■ 結局GraphQLの巨大のstructのfieldに必要な値をセットするコード を手で書くのが面倒で残すことにした ■ wireはフィールドと型と値のパズルの解決をするだけ wireからの移行

Slide 25

Slide 25 text

25 depmanというパッケージを作った話 実際の利用例 func CreateSearchService(ctx context.Context) (*SearchService, error) { return depman.RequestResource(ctx, SearchServiceSpec{}) } type SearchServiceSpec struct{} func (spec SearchServiceSpec) CreateResource(ctx context.Context) (*SearchService, depman.CloseFn, error) { productInfoRepo, err := depman.RequestResource(ctx, ProductInfoRepositorySpec{}) if err != nil { return nil, nil, err } service, err := NewSearchService(productInfoRepo) return service, nil, err }

Slide 26

Slide 26 text

26 技術書典とかで使ってみた話 ● REST API, GraphQL Endpoint etc で値をそれぞれ生成するとき、 depmanのManagerを共有しておけば出てくるインスタンスも同じなので各所 で値を共有できる ● 循環参照を解決できるようになった ○ 生成経路がコントロールできていないインスタンス生成が撲滅された ● package間の循環参照は相変わらず厳しい ○ 気合で解決可能だけど美しくはない ● デバッガを使ったインスタンス生成の追跡はwireと変わらず可能 ○ さすがにコード生成してno reflectionのwireには劣るが苦ではない 使ってみた

Slide 27

Slide 27 text

27 質問? ご清聴ありがとうございました!

Slide 28

Slide 28 text

28 ご清聴ありがとうございました!