2023/09/20の「ZOZO Tech Talk #8 - Go」にて発表した登壇資料です。 イベント詳細: https://zozotech-inc.connpass.com/event/293668/
事業成長を加速させるGoの コード品質改善の取り組み ZOZO Tech Talk #8 - Go 株式会社ZOZO ブランドソリューション本部 バックエンド部 FAANSバックエンドブロック バックエンドエンジニア 田島 太一Copyright © ZOZO, Inc.1
View Slide
© ZOZO, Inc.株式会社ZOZO ブランドソリューション開発本部バックエンド部FAANSバックエンドブロック 田島太一 2020年4月中途入社 MLOpsチームでZOZOTOWN, WEARのML機能の開発・運用に従事後、2022年6月からFAANSの開発チームでバックエンド開発に従事 最近はトマトにハマって毎日2玉食べている
© ZOZO, Inc.3ショップスタッフの販売サポートツール「FAANS」とは● WEAR, ZOZOTOWN, Yahoo!ショッピング, アパレルブランドの自社 ECへのコーディネート投稿やその成果の可視化など、ショップスタッフのオンライン接客を支援する業務支援ツール● ZOZOTOWN上でブランド実店舗の在庫取り置きを希望した際に、ショップスタッフが FAANS上での簡単操作で取り置き対応を完結できるといった実店舗業務のサポートにも対応
© ZOZO, Inc.4FAANSのバックエンドシステムは全てGoで書かれている● WebAPIサーバやバッチ処理も含め全て Goで実装● WebAPIサーバはClean Architectureのポリシーに則った、ドメイン層をレイヤー間の依存関係における最上位に置くレイヤードなアーキテクチャとなっている
© ZOZO, Inc.5FAANSはまだローンチまもない新規プロダクト● FAANSは正式ローンチからまだ 1年の新しいプロダクト● 0 → 1の立ち上げフェーズから 1 → 100の成長フェーズに移行● 新機能の開発や既存機能の改善に取り組む日々● 一方で、技術的負債も溜まってきているため、負債を解消していく取り組みも行っている
© ZOZO, Inc.6新規事業でも技術的負債と向き合っていくことが大事https://speakerdeck.com/shinden/living-with-technical-debt
© ZOZO, Inc.7機能開発の傍、様々な技術的負債の解消に取り組んでいます● アプリケーションのドメインモデル● データベースのデータモデル● Web APIのスキーマ定義● データベースや外部 WebAPIといった外部コンポーネントとの連携部分の処理の実装● レイヤー構造やパッケージ構成、それらの依存管理といったサーバサイドのアプリケーションアーキテクチャ● コード品質 ← 今日はこのお話● …
© ZOZO, Inc.8本日お話しすること● 今日は技術的負債の中で Goのコード品質に関する部分にフォーカスしてその改善の取り組みとその際の工夫についてお話します● Goでチーム開発する上で入門向けの優しい内容となっています
© ZOZO, Inc.9本日お話ししないこと● 我々が採用しているレイヤードなアーキテクチャに関する解説● テストコードの品質改善の取り組み● データベースなどの特定技術を扱う処理の Go実装に関する工夫や技術的負債の改善の取り組み
© ZOZO, Inc.10コード品質とは● 可読性● 保守性● 安全性● 再利用性● パフォーマンス● …
© ZOZO, Inc.11コード品質は事業の成長スピードにとって重要な因子● 開発の生産性の向上● メンテナンスの容易性の向上● 長期的なバグの減少
© ZOZO, Inc.12Goでも書き方にばらつきは生まれる● Goはシンプルな言語仕様● とはいえ、(むしろだからこそ?)開発者によって書き方にばらつきは生まれる● コーディングの一般的な作法、 Goらしい書き方を学ぶ必要はある
© ZOZO, Inc.13Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善※ 他にも色々やってますが時間の都合上割愛
© ZOZO, Inc.14Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.15Linterで縛れるものは縛る● Pull RequestのCIでバグ検知や不適切な書き方を自動検知できるので Linterで縛れるものは縛ってコード品質を仕組みで担保する● Goの代表的なLinterツール○ govet○ Staticcheck○ golangci-lint○ …
© ZOZO, Inc.16golangci-lint● Goの代表的なLinterツールの一つ● govetのようなgo標準のLinterやStaticcheckのようなサードバーティの linterをまとめて実行できる runner● yamlファイルでどのLinterツールを使うか、使わないかといった細やかな設定が可能● ホワイトリスト型、ブラックリスト型の設定どちらにも対応
© ZOZO, Inc.17当初の弊チームのgolangci-lintの設定● golangci-lintはプロダクト立ち上げ当初から CIに導入済していた● yaml上では実行するLinterツールは何も指定されていない、つまりデフォルトで有効化されている Linterのみを実行する設定となっていた● golangci-lintでデフォルトで有効化されている Linter一覧○ errcheck○ gosimple○ govet○ ineffassign○ Staticcheck
© ZOZO, Inc.18Linterの設定を徐々に厳しくしていく手順● golangci-lintの設定ファイルにおいて disable-all: trueの上でenable配下にLinterを列挙していくホワイトリスト型を採用● まずは、enable配下にデフォルトで有効化されていた Linterを明示的に指定して実質的に設定に差分がない状態とした● デフォルトで有効ではない、つまり未導入の golangci-lintのLinter一覧をコメントアウトする形で列挙しておく
© ZOZO, Inc.19Linterの設定を徐々に厳しくしていく手順● その上で有効化したい Linterをコメントアウトして1つずつPull Requestを分けて有効化していく● 有効化したlinterにより変更していない既存コードの部分で CIがコケるようになると開発体験が下がるため導入時にプロジェクトコードの全範囲においてその Linterでエラーが出る箇所を修正することが重要
© ZOZO, Inc.20徐々に設定を厳しくしていった結果● golangci-lintでデフォルトで有効化されていて既に導入済みだった Linterを除いて新たに20個以上のLinterが導入された● 今後も優先度を精査しながら適切なタイミングで Linterの導入作業を進めていく予定
© ZOZO, Inc.21Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.22Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.23スタイルガイドの導入● コーディング規約はドキュメントにまとめ、チームで認識合わせできるようにしておくことはチーム開発の基本○ コミュニケーションコストの削減○ 新メンバーのオンボーディングの円滑化● 0から自分たちでコーディング規約を定めていくのは開発組織の規模に見合わない労力がかかり非現実的なので一般に公開されていて信頼できるスタイルガイドにできる限り乗っかる戦略● 導入したGoのスタイルガイド○ Effective Go (公式)○ Go Code Review Comments (公式)○ Google Go Style Guide (Google社製)
© ZOZO, Inc.24Google Go Style Guide● 2022年11月に公開されたGoogle社によるGoのスタイルガイド● Effective Goを前提としているのでスタイルガイド間のバッティングもない● 読みやすく慣用的な Goのコーディングスタイルを示している● 可読性が高いコードを書く上での迷いを最小化して初心者にありがちなミスを避けられるようにすることが目的● https://google.github.io/styleguide/go/
© ZOZO, Inc.25Google Go Style Guideは3章構成規範的(canonical): 規範的かつ永続的なルール標準的(normative): 一貫性を持たせるためのルール章 主な対象者 規範的 標準的Style Guide EveryOne Yes YesStyle Decisions Readability Mentors Yes NoBest Practices Anyone Interested No No● 3章全てをチームが従うべきスタイルガイドとして一先ず導入した
© ZOZO, Inc.26「Style Guide」の5原則● 個別ケースだけでなく全体に通底する考え方も説明されていて Good● 5原則 (上から重要度順)○ Clarity (明白さ) : 目的と根拠が読み手にとって明白か○ Simplicity (シンプル) : できる限りシンプルなやり方で目的を達成しているか○ Concision (簡潔さ): 高いS/N比を有しているか○ Maintainability (保守性): 保守は容易か○ Consistency: 広範なGoogleのコードベースと一貫しているか↓5原則という優先度付きの判断軸をチームの共通認識として持つことでスタイルガイドに具体的な書き方が記載されていないようなケースにおいても実装時やレビュー時に判断しやすくなり、チームとして合意形成もしやすくなった
© ZOZO, Inc.27Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.28Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.29Goにおけるエラーハンドリング● 業務アプリケーションでは関数 /メソッドの呼び先で発生したエラーをそのまま呼び出し元には返さずにfmt.Errorf() (Go 1.13〜)を使って呼び先の関数 /メソッドの名前や引数の値といった情報を付与する形でエラーをラップして上流に伝播させるのが一般的○ そのまま返してしまうとエラーログからエラーの発生箇所が分かりにくくなりトラブルシューティングの難易度が上がる○ サードパーティライブラリを使ってスタックトレースを出す方法もある ※標準パッケージにスタックトレースの機構は現状存在しない
© ZOZO, Inc.30Before: 適切にラップせずにそのままのエラーを伝播させていた● 我々のシステムでは呼び先の関数やメソッドで発生したエラーを適切にラップすることなく呼び元にそのまま返してしまっていたため、エラー調査の難易度が上がっていた
© ZOZO, Inc.31After: 関数/メソッドの”呼び先側”でエラーをラップするようにした● エラーが発生した呼び先の関数 /メソッドの情報を呼び元側ではなく呼び先側で fmt.Errorf()を使ってエラーをラップして上流に伝播させるようにした● errwrapperというpackageを用意してエラーのラップ処理が関数 /メソッドの内部実装の文頭 1行で済むように簡易化した
© ZOZO, Inc.32errwrapperパッケージの実装 ※一部抜粋
© ZOZO, Inc.33errwrapperパッケージの実装● https://github.com/golang/pkgsite/blob/master/internal/derrors/derrors.go を参考に実装した● 全体の実装は↑をご参照ください
© ZOZO, Inc.34errwrapperパッケージのPros/Cons● Pros○ 呼び先側の関数/メソッドの先頭で一度だけエラーラッピングのロジックを記述するだけで、その関数/メソッド内で発生するエラーは自動的にラッピングされるのでラク○ 呼び先の関数/メソッドの情報以外の情報を付与したい時を除けば呼び元ではエラーをラップする必要がなくなったのでコードがシンプルになった● Cons○ 標準的なやり方ではないため外部ライブラリの関数 /メソッドでは呼び元でエラーを適切にラップする必要があり、その分の認知負荷がかかる
© ZOZO, Inc.35Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.36Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.37凝集度と結合度● 保守性と拡張性が高いソフトウェアには高い凝集度と低い結合度が重要○ 凝集度■ モジュール内の機能がどれだけ密接に関連しているかを示す尺度○ 結合度■ 二つのモジュール間の依存関係の強さを示す尺度
© ZOZO, Inc.38① ファクトリ関数による構造体(struct)の初期化● Goの構造体にはJavaのクラスコンストラクタのような初期化用メソッドは用意されていないし初期化時にそのようなメソッドによる初期化処理を強制することもできない● GoではNewXXX()という命名でファクトリ関数を用意して初期化するのが慣例
© ZOZO, Inc.39Before: ファクトリ関数を使わずにDomain層のEntityの構造体を初期化していた● アプリケーション固有のビジネスロジックを実装する UseCase層でDomain層に実装されたEntityを表す構造体をファクトリ関数を使わずに初期化していた○ 属性値のバリデーションチェックなどのドメイン層の知識が Domain層ではなくUseCase層で実装されて凝集度が下がってしまう → ドメインモデル貧血症○ 属性値の指定忘れやバリデーションチェックのし忘れによる中途半端に初期化された状態の構造体の存在を許容してしまう恐れ → バグの温床
© ZOZO, Inc.40After: Entityを表す構造体のファクトリ関数を定義して凝集度を高める(Domain層の実装)属性値のバリデーションやドメイン固有の変換処理はファクトリ関数内で実行
© ZOZO, Inc.41After: Entityを表す構造体のファクトリ関数を定義して凝集度を高める(UseCase層の実装)テストしやすいように日時は外から渡す
© ZOZO, Inc.42After: Entityを表す構造体のファクトリ関数を定義して凝集度を高める● ファクトリ関数内で属性値のバリデーションや変換処理が行われるため、不完全な状態の構造体が生成されることがなくなった● ドメイン固有の知識が UseCase層に散らからずにDomain層に集約されたことで凝集度が高まった● 時間の都合上説明は割愛するが、例えば複数の Entity間で共通の属性などは Defined Typeで適切に「値オブジェクト」化し、その属性のバリデーション処理を値オブジェクトの生成処理内で行うことで凝集度を高めることも有効
© ZOZO, Inc.43② Defined Typeによるファーストクラスコレクション● ファーストクラスコレクション○ 配列などのコレクションをラップする専用のクラスを持ち、そのコレクションのビジネスルールや振る舞いをそのクラス内に実装するオブジェクト指向プログラミングの実装パターン● Defined Type○ GoではDefined Typeを使用することで独自の新しい型を定義可能○ Slice型に対してDefined Typeで新たな型を定義しその型でメソッドを定義することで簡単にファーストクラスコレクションが実現できる
© ZOZO, Inc.44Before: Entityのコレクション操作がUseCase層に実装されていた
© ZOZO, Inc.45After: ファーストクラスコレクションを使ってDomain層にコレクション操作のロジックを持たせる (Domain層)
© ZOZO, Inc.46After: ファーストクラスコレクションを使ってDomain層にコレクション操作のロジックを持たせる (UseCase層)データアクセス時にentity.DocumentのSlice型ではなくentity.DocumentList型で返すようにする
© ZOZO, Inc.47After: ファーストクラスコレクションを使ってDomain層にコレクション操作のロジックを持たせる● ファーストクラスコレクションによってコレクション操作が Domain層の振る舞いとして実装されて凝集度が高まった
© ZOZO, Inc.48Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.49Goのコード品質改善の取り組み1. 徐々に厳しくするLinter設定2. スタイルガイド「Google Go Style Guide」の導入3. エラーハンドリングの改善4. 凝集度を高める改善5. ボーイスカウトルールによる既存コードの継続的改善
© ZOZO, Inc.50コード品質向上のためのチーム方針● Linterで検知できるものは Linter導入時に一括で修正する● 新規で書くコードはLinterやチームの規約に沿って実装する● 機能開発に追われる中で既存コードはいつ、どのように改善していくか → ボーイスカウトルール
© ZOZO, Inc.51ボーイスカウトルール● 「キャンプ場は、自分が訪れた時よりも綺麗にして去るべきだ」● ソフトウェアのコードに変更を加える際、関連する部分のコードを少しでも改善してからコミット・マージするソフトウェア開発のプラクティス→ 時間とともにコードベース全体の品質が徐々に向上していく
© ZOZO, Inc.52ボーイスカウトルールによるコード品質改善のチーム方針● 既存のコードに変更を加える際はその周辺がチームの規約に沿ってなければついでに直す● 利用する既存の定数 /変数/構造体/関数/メソッドも規約に沿ってなければついでに直す● ただし、既存コードの品質改善のための変更により Pull Requestのレビュアーの負荷が高くつく場合は、改善部分を適切な粒度で別 Pull Requestとして切り出して出す
© ZOZO, Inc.53ボーイスカウトルールは実効的なアプローチ● 新規事業で機能開発に追われる中で継続的に既存コードを改善していく上でボーイスカウトルールは実効的なアプローチ● ただし、ボーイスカウトルールだけに頼るのではなく、既存コードにおいてセキュリティリスクや後からの負債回収コストが高くつく負債は独立したタスクとして切り出して優先度を上げてしっかり工数を割いて取り組むことも大事
© ZOZO, Inc.54まとめ● スピーディーな事業成長にとって高いコード品質は重要な因子● 新規事業で機能開発に追われる中でも途中からでも着実に改善していける● 仕組み化やルール化によって高品質で Goらしいコードをチームで実践していきましょう