845 Views
October 31, 25
スライド概要
2025/10/31 hatena.go#2 https://connpass.com/event/371918/
はてなではマンガビューワ「GigaViewer」を開発しています。GigaViewerは10年近い歴史を持ったマルチテナントのプロダクトです。月日を経て、サービスの成長・人の入れ替え・エコシステムの変化などによって、いくつかの課題が生じてきました。
そこで、新しいアーキテクチャでリプレイスする計画が進行しています。新しいアーキテクチャではGoを利用しています。 この発表では、リプレイスにあたってGoを選択した理由、Goを使ってどのように開発しているかをメインにお話します。
こんにちは
マンガビューワ「GigaViewer」を Goでリプレイスしようとしている話 id: momochi29 2025/10/31 hatena.go #2
自己紹介 ● id: momochi29 ● 所属: Mackerelチーム ○ 最近まではマンガチームに所属していた ● 職種: Webアプリケーションエンジニア ● 好きなマンガ: ワールドトリガー
目次 ● リプレイスしようとしている背景 ● 言語選定 ● Goをどのように利用しているか ● 持続可能にするために ● まとめと今後
リプレイスしよう としている背景
GigaViewerについて ● マンガを快適に読んでいただけるように各種機能を提供 している ● 2017/1のリリースから成長を続けている
GigaViewerについて GigaViewer - 株式会社はてな より引用
GigaViewerの現在の構成 ● 1つのコードベースで異なるサービス(以降ではメディア と呼ぶ)を提供するマルチテナントの構成 ● 基本的にはPerl、部分的に最適な言語を使用 ● バックエンドのAPIは主にGraphQLを利用
月日を経て課題が出てきた ● 事業が成長して、メディアごとのカスタマイズに対応す るための処理が増加し、影響範囲の把握・理解が難しい ● 人の入れ替えによって、アーキテクチャの意図が忘れら れたコードが出現している ● エコシステムの変化によって、Perlコミュニティの勢いが 減っていたり、SDKのサポートがなかったりする
全社的にもPerlから移行する機運が高まっていた ● 組織としての技術スタックを均質化することで、会社と しての技術の蓄積や人的リソースの共有によってレバ レッジを効かせたい ○ 新しいプロダクトは Go/TypeScript を利用することが推 奨されている
リプレイスしよう ● GigaViewerは今も盛んに開発されている ○ 崩壊しているわけではない ■ 1機能追加するのに数ヶ月かかると言った状況ではない ■ 動的な型制約は普通にあるし、テストも充実している ● 数年後も安定して開発できるようにリプレイスしよう
言語選定
言語選定にあたって意識したこと ● 積極的な技術選定 ○ 選定される(あるいはさ れない)技術そのものが 原因となる意思決定 ● 消極的な技術選定 ○ 技術そのもの以外に原因 がある意思決定 積極的な技術選定と消極的な技術選定 - uhyo/blog より引用
まずはサービスが満たすべき特性を知る ● チームの全エンジニアでブレストし、ディレクターに確 認を取って特に重要にしたい特性を明らかにした ○ ○ ○ ○ パフォーマンス スケーラビリティ メンテナンス容易性 マルチテナント対応容易性(独自定義)
積極的な技術選定 ● ● ● ● ● 変なことをコンパイル時に気付ける静的型付け言語にしたい GraphQLのパフォーマンスが良いのが望ましい メディアごとの差異をうまく表現できる表現力があると望ましい コミュニティが盛り上がっていてSDKのサポートが手厚いのが望ましい AIのサポートを手厚く受けられるのが望ましい
積極的な技術選定 ● ● ● ● ● 変なことをコンパイル時に気付ける静的型付け言語にしたい GraphQLのパフォーマンスが良いのが望ましい メディアごとの差異をうまく表現できる表現力があると望ましい コミュニティが盛り上がっていてSDKのサポートが手厚いのが望ましい AIのサポートを手厚く受けられるのが望ましい Rust, Go, TypeScriptが候補か
消極的な技術選定 ● 組織として技術スタックを均質化したいので Go/TypeScriptに揃えたい ● Rustの知見が社内にほとんどない
選定 ● 積極的/消極的のバランス ○ 積極的、つまり技術的に明らかに優位な理由があるなら技 術以外に問題があっても説得する価値はある ● 今回は、Goが最もバランスの良い選択肢だと判断した
Goで行けそうか? ● GraphQLのパフォーマンスが良いのが望ましい ○ → GCによるオーバーヘッドはあるが、PoCを作って現状 よりはパフォーマンスが良いので大丈夫だろうと判断した ● メディアごとの差異をうまく表現できる表現力があると 望ましい ○ → かっこよくは書けなくとも、DIによって愚直に書けると 判断した
初めて言語選定した感想 ● 「情熱」が決め手になりそう ○ ○ 大枠の要件を満たした言語による差は情熱によって覆る範疇だと思う 例えばTypeScript大好きチームなら多少の困難があってもTypeScriptを 選んでなんとかするだろう ● チームとしての言語に対する情熱と組織としての方針の バランスで決まると感じた
Goをどのように利用して いるか
リプレイスで目指すこと これまでの課題に対処するために主に3つ定めた 1. 静的型付けを活かすための型の活用 2. マルチテナント対応を容易にするためのDI指向 3. テスト容易性を獲得するためのアーキテクチャ
リプレイスで目指すこと これまでの課題に対処するために主に3つ定めた 1. 静的型付けを活かすための型の活用 2. マルチテナント対応を容易にするためのDI指向 3. テスト容易性を獲得するためのアーキテクチャ 1と2について話します。 3は付録にあります。
静的型付けを活かすため の型の活用
思想 ● なるべくコンパイル時に問題を発見したい、ただ複雑す ぎる型で型パズルすることになるのは避けたい
Scott Wlaschin 著, 猪股 健太郎 訳, アスキードワンゴ — 関数型ドメインモデリング
型の使い所 ● 型でビジネスルールを表現する ● 状態ごとに型を作成して、その状態に必要なものだけを もたせる
例: アクティブなユーザにのみメールを送信したい
これでも要求は満たせているが・・・ 呼び忘れてもコンパイルエラーには ならない
これでも要求は満たせているが・・・ 未アクティベートユーザ は常にゼロ値が入る
これでも要求は満たせているが・・・ 検証されていないメールアドレス でも送ってしまう
型でガードする
型でガードする アクティブなユーザしか受け付けない ことを表現
型でガードする 状態ごとに型を分け、ActivatedAtは ActiveUserにしかないことを表現する
型でガードする 検証されたEmailAddress型だけを 受け付ける
直和型が欲しくなってくる ● 型でビジネスルールを表現していく と直和型が欲しい場面が出てくる ● Goには直和型が組み込まれていない 関数型ドメインモデリング p106より引用
直和型を表現する
Goらしさからかけ離れてない? ● はい
まだベストな解は持てていないが ● いつ直和型を使う? ○ 「直和型を使わなくてもどこかで分岐が必要な場合」に使 うのがいいと考えている ○ 分岐が必要なら型レベルで別れている方がわかりやすい ● 不要な直和型は作らない ○ EmailAddress = UnvalidatedEmailAddress | ValidatedEmailAddress は本当に必要か?
マルチテナント対応を容 易にするためのDI指向
現在のマルチテナント対応 機能(例: コメント) メディアごとの差異 メディアに応じてサニタイズ処理を切り替 えている
現在のマルチテナント対応 機能(例: コメント) 1機能に対して、メディアごとの分岐が多 メディアごとの差異 数存在 → メディア単位で見たときに何が設定され ているのか把握するのが困難 mediaに応じてサニタイズ処理を切り替え ている
メディアごとに設定したい 機能(例: コメント) メディアごとの差異 メディアの設定 機能(例: コメント) こうしたい コメントの設定 メディアごとの差異 注入 メディアの設定を見れば、そのメディアの機能 がどういう挙動をするのか一目でわかるように なりたい
interfaceが活きる ● > Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. https://go.dev/wiki/CodeReviewComments#interfaces ● 機能の実装時にメディアごとに処理が異なる箇所を interfaceにする
メディアごとに設定をwireでDIする DI コメント機能内部のvalidation あるメディアのコメント機能に 対する設定をwireで記述
DIによる恩恵 ● Flaky Testが起きづらい ○ テストではメディアが乱択され、メディア間の処理の差異 によってテストが失敗しやすいという課題があった ○ テスト時に、テストしたいものをDIすることでメディアに 依らず決定的になる 課題について詳しく書いている記事: GigaViewer for Web におけ る Flaky Test に対する取り組み - Hatena Developer Blog
完成した構成
完成した構成 メディアごとの依存を設定する mediaAの設定例 ● コメント数をValkeyに保存する ● サニタイズする mediaBの設定例 ● コメント数をMySQLに保存する ● サニタイズしない
完成した構成 リクエストしてきたメディアに応じて DI済みオブジェクトを切り替える
持続可能にするために
設計意図を守ってほしい ● 元々のアーキテクチャも狙いを持って設計されていた ● 月日が経つと狙いが忘れられがち ● 同じことを繰り返したくない・・・
開発フローを定義した
ガイドラインを整備した ● いろんなガイドラインを作った
読まれない 😇
AIに読んでもらう ● AIにガイドラインを読み込ませて おく ● レビュープロンプトを用意して、 違反箇所をガイドラインと一緒に 提示してもらう ● 人間は違反している場合のみガイ ドラインを読むという運用
AIどう? ● AIによるレビューは偽陽性/偽陰性ともにあるが、ある程 度は指摘してくれて役立っている ● AIに書いてもらうのはまだ活用できていない ○ 正しいかを人間が判断する必要があるが、まだ新アーキテ クチャの浸透フェーズになっていないので判断できる人が 少ないと考えたため ● AIに聞きながら開発できるようにはなっていそう
まとめと今後
まとめ ● メディアの特性、組織的な方針などを勘案してGoでリプ レイスすることにした ● Goはシンプルで、自分が書くときは若干物足りなさは感 じるものの、自分がよく知らないコード(例えばライブラ リのコード)を読むときにはシンプルさの恩恵を多大に感 じる ● 持続可能な仕組みづくりをできたのではないかと思って いる
GigaViewerの今後 ● 現在、GraphQL Federationによる段階的な移行を進め るためにWunderGraph Cosmo導入と周辺整備が進行中 ● 整備が完了次第、随時移行していく ○ 定量評価も公開していく予定 ● 続報にご期待ください!
付録
使用ライブラリ一覧(全部ではない) ● ● ● ● ● ● ● ● Webフレームワーク: Echo データベースアクセス: GORM/Gen GraphQL: gqlgen, graph-gophers/dataloader テスト: stretchr/testify, gomock, DATA-DOG/go-txdb エラー: cockroachdb/errors 計装: opentelemetry-go DI: wire 便利グッズ: samber/lo, k0kubun/pp
テスト容易性を獲得する ためのアーキテクチャ
思想 ● 最もテストしたいロジックを最もテストしやすくしたい ● 各層でのテスト内容を定めて、重複したテストをなくし たい
Vladimir Khorikov 著, 須田智之 訳, 単体テストの考え方/使い方 | マイナビブックス
ロジックを最もテストしやすくする ● ドメインを中心に据える ● プロセス外依存(DBアクセスなど)をモック ○ gomockはよくできている ■ 入力が違ったり、呼ばれる回数が違ったりするとコケる
曖昧さをなくすためにテストサイズを定義
層ごとのテスト内容を定義
層ごとのテスト内容を定義 中心ではモックしてロジック をテスト
層ごとのテスト内容を定義 モックすると意味のあるテスト ができないのでモックしない
層ごとのテスト内容を定義 外側ではモックせず、全体と して動くことをテスト
高速! ● Smallのテストは200個のテストケースがある パッケージで0.04s ● Mediumのテストは1s未満 ○ 正常系のテストケースが少ない ○ テストごとにロールバックしているのでデータが残らない
感覚として ● 一発で動くことが多い ○ テストが通れば、繋ぎ込んで動かしたときに一発で動 くことが多い