メインコンテンツへスキップ
見出し画像

頑張らない開発フロー(Android)

    この記事は、NOT A HOTELのソフトウェア開発連載「Inside the SOFTWARE TEAM」の第7日目です

    NOT A HOTELでオーナーアプリを開発している@haruです。
    以前NOT A HOTEL Androidの技術構成について話しましたが、今回はNOT A HOTEL AndroidにおけるCI/CD、Git/GitHub周りの取り組みを紹介したいと思います。

    NOT A HOTELのオーナーアプリは元々iOS・Webが先行してリリースしていたアプリで、Androidのアプリ開発は2023年6月に開始しました。必要最小限の機能で初期リリースを行ったのが2024年2月。その後、2024年12月にはiOSの実装に追いつき、同等な機能になりました。2025年現在、Androidエンジニアは3人です。

    画像
    これまでの開発の軌跡

    ブランチ戦略を頑張らない


    NOT A HOTEL Androidでは、GitHub Flow/トランクベース開発を基本としたシンプルなブランチ戦略を採用しています。mainブランチから各自が短命なfeatureブランチを切って開発し、Pull Request(PR)を出してmainにマージする、という流れです。

    画像

    参考までに直近100件のPRの行数を調べたところ、中央値は80行でした。

    $ gh pr list --state all --limit 100 --json additions,deletions | jq '[.[].additions + .[].deletions] | sort | .[length/2]'
    80

    リリースは3週間に1回のサイクルで行っています。リリース時には審査提出時のmainブランチにタグを打つだけで、リリースブランチは作っていません。審査に落ちることは滅多にないと仮定し、投機的にリリース作業を進めることでシンプルな運用を実現しています。

    このように、基本的にはmainとfeatureブランチしか存在しないのですが、唯一の例外として、hotfixブランチを作ることが稀にあります。
    重大な問題が発生した場合には、直近のリリースタグからhotfixブランチを切り、hotfixブランチからリリース作業を行います。そして審査に提出した後、hotfixブランチはmainブランチにマージします。

    画像

    また、こうしたブランチ戦略で開発するためには、開発途中のコードや、リリースに含めない変更を常にmainブランチにマージできる必要があります。
    これはFeature Flagによって実現しています。

    Feature Flagを頑張らない


    Feature FlagはフラグのON/OFFによって、機能の有効/無効を切り替える仕組みです。

    if (FeatureA.isEnabled()) {
        // 新機能
    } else {
        // 旧機能
    }

    モバイルアプリではFirebaseのRemoteConfigを使って実装することが多いかもしれません。一方、我々はBuildConfigで静的に出し分けるだけの単純な仕組みを使っています。

    // 定義
    object FeatureFlag {
        const val FOO: Boolean = BuildConfig.FLAVOR == "dev"
        const val BAR: Boolean = BuildConfig.FLAVOR == "dev"
        // ...
    }
    
    // 利用時
    if (FeatureFlag.FOO) {
        // 新機能
    } else {
        // 旧機能
    }

    これには次のような利点があります。

    1. リアルタイムに値を更新しないので、考慮すべきことが減る

    これはメリットであり、デメリットでもあります。

    デメリットは、任意のタイミングで機能をON/OFFできないことです。しかし、我々のアプリではアプリのリリースと機能のリリースを同時に行っているため、あまり影響を受けていません。

    メリットは、実装がシンプルになる点です。動的な出し分けを実現する場合、「アプリ動作中にフラグが変化したとき、現在の画面はどうなるべきか?」といったことを考慮する必要があります。これを見落として「Feature Flagの出し分けにバグがあった」といった問題も起こりがちで、静的なFeature Flagではこうしたリスクを回避することができます。

    2. どこからでもフラグの値を参照できる

    これはコードの書きやすさに直結します。実装はシングルトンなFeatureFlagクラスが一つあるだけなので、UI層やデータ層といったレイヤを問わず、ViewModelのような典型的なクラスからEntityのような単純なクラスまで、自由に参照することができます。また、RemoteConfigのような非同期に値を取得する仕組みでは、値を更新する前(起動直後など)にフラグを参照することで意図しない挙動になる可能性があります。こういった複雑さを排除し、簡単に扱えるのは大きなメリットです。

    3. 未使用コードの最適化が効く

    細かい話ですが、静的なFeature Flagでは、出し分けのif文の結果が静的に定まるので、R8による不要コードの削除が期待できます。Feature Flagは新機能の実装に使うことが多いはずで、新機能に関連したコードがバイナリに含まれずに済むのもメリットと言えるでしょう。

    コードレビューを頑張らない


    コードレビューの依頼は不規則に発生し、コーディングしていたエンジニアの集中を妨げる原因になります。かといってレビューを放置した場合、今度はレビューを待つ人の作業を止めてしまいます。大規模開発においてほぼ必須ともされるコードレビューというプロセスは、ある意味では正当化された時間泥棒とも言えて、これを削減できるに越したことはありません。

    AIがコードレビューをする

    生成AIによるコードレビューは明らかにゲームチェンジャーです。NOT A HOTEL Androidでは、Claude Code GitHub ActionsとGitHub Coplilot code reviewを併用することで、レビューの負荷を軽減しています。(最近はCodexも試しています)

    全てをAIに任せているかと言うとそうではなく、複雑なコンテキストを要求しない、一般論から推測できることを中心に見てもらっています。例えば、修正漏れやtypoの検出など、ヒューマンエラーの検出は生成AIの得意とする領域で、かなり効果が出ていると実感しています。

    画像
    コピペの誤りを見つけてもらった例
    画像
    リネームの作業漏れを見つけてもらった例

    一方、AIは誤った指摘をすることもあり、特に失敗しやすいものについては定期的にプロンプトを見直すことで改善を続けています。

    Claude Code GitHub Actionsのコードレビューについてはこちらでも解説しているので、興味があればご覧ください。
    https://zenn.dev/notahotel/articles/cf90b6eff72272

    コードレビューの外側で問題を解決する

    コードレビューを楽にするために任意で設計レビューを行っています。

    例えば、Androidチームでは毎日朝会を行っており、全体に影響が出る変更や、大規模な実装については朝会で方針を議論することが多いです。

    画像
    ある日の議事録

    実装者の視点で見ると、レビューの前に一定の合意が取れているので、手戻りを減らすことができます。レビュワーからすると、実装方針などのコンテキストが頭に入っているので、コードを読み解く負荷を減らすことができます。

    レビューのしやすいPRを心がける

    各自でやっている取り組みとして、レビューしやすいPRを心がける、というものがあります。一般論として、コードを読み解くのはコードを書く以上に大変な作業です。冒頭で述べたようにPRの粒度を小さく抑えたり、PRの説明を簡潔に、要点を絞って書くのはもちろんですが、セルフレビューでコメントを追加するなどして、レビューする人が理解しやすいように工夫しています。

    画像
    数分の気遣いがレビュワーの数十分を節約する

    また、最近ではAIによるコードをレビューする機会も増えてきました。
    残念ながら現時点でのAIが出力するコードには、本来その人なら到底しないであろうミスが含まれていたり、バグが巧妙に、それっぽく埋め込まれることが多く、要注意なコードであると言わざるを得ません。そのため、AIが書いたコードをコミットする時にはコミットメッセージに Co-authored-by を付与して、AIが書いたコードであることを明示するようにしているほか、
    PRの説明についても、AIが書いた文章は区別できるようにしています。

    画像
    コミットをAIによるものか判別できるようにする
    画像
    Claudeが生成した文章にはアイコンが入る

    そもそも、なんのためのコードレビューなのか?

    コードレビューには次のようなメリットがあると言われています。

    ・教育的効果、知識の共有
    ・バグの発見
    ・セキュリティリスクの発見
    ・責任の分散、属人化の防止
    ・一貫性の維持

    このうち、バグの発見やセキュリティリスクの発見、一貫性の維持については、AIによるコードレビューである程度担保できるようになりました。これはAIの進化に伴い、より精度が向上していくでしょう。教育的効果については、我々のチームは全員Android歴が10年以上という特徴もあり、あまり意味をなしていないように思えます。そうなってくると、いよいよコードレビューをやる意味は?という疑問が湧いてきます。とりわけ我々のようなチームに関して言えば、責任の分散、属人化の防止、知識の共有といった要素の重要度が高く、品質の担保というよりも、レビューする側にもオーナーシップを持ってもらい、今後の開発で困らないようにコードを見ておく、といった側面が強いのかもしれません。

    CI/CDを頑張らない


    CI/CDにはGitHub Actionsを使い、主に以下のジョブを実行しています。

    ・lintやテストの実行
    ・アプリのビルド及び配布

    よくある設定は「PR毎にlintやテストを実行し、ビルドを行う」といったものですが、我々はそうしていません。
    最大の理由は時間がかかることです。コードレビューは数分で終わることもあるため、数十分かかるCI/CDの実行は高速なマージを阻害してしまいます。これは我々の開発フローにおいて致命的でした。また、CI/CDは実行時間に比例して費用もかかります。運用的に言っても、リリースまでに全ての問題が修正されていれば問題ないので、頻繁な実行は我々には不必要なものでした。

    実際のところは次のように運用しています。

    ・lintやテストは毎朝自動実行する
    ・ビルドは特定のタイミングで半自動的に実行する

    事件は寝ている間に発生し、起きる頃にはAIが解決している

    lintやテストは毎朝決まった時間帯に実行しています。
    ジョブが失敗したときには、新たにClaude Code GitHub Actionsが実行され、自動的に修正PRが作成されます。

    画像
    朝起きると修正PRができている

    lintやテストの失敗は単純な修正で済むことが多く、我々が起きる頃にはClaude Codeが全てを解決していて、人間の作業は最終確認をするだけ、というフローになっています。

    ラベルによる柔軟なトリガー

    ビルドについては半自動的な運用をしています。
    特定のラベルをGitHub Actionsと紐づけて、必要になったタイミングで簡単にビルドが用意できるようになっています。例えば、これで機能Aの開発は完了、と思ったら、そのPRに特定のラベルをつけてマージします。すると自動でビルドが始まり、完了したらSlackに通知が飛ぶ、といった仕組みになっています。

    まとめ


    NOT A HOTEL Androidチームにおける頑張らない(ために頑張っている)開発フローを紹介しました。

    世間に広く浸透しているテクニックは万能に見えることがありますが、万能であるがゆえに、不必要な複雑さを生むこともあります。Androidチームではそれらを盲信せず、自分たちにあった開発フローを適宜選択していくことで、シンプルで効率的な開発フローを追求しています。

    また、こうした開発フローは一度決めて終わりではなく、環境の変化に合わせて定期的に見直すことも重要です。
    例えば、以前は全員でコードレビューを行っていましたが、AIによるコードレビューの台頭や実装のパターン化、各自のコードベースの理解などが進んだ結果、今は1人の承認をもってマージしても良いことにしています。
    現在の運用は少人数チームを前提にした性善説的なものですが、チームが拡大した場合には、どこかで破綻してしまうかもしれません。こうした際にルールを増やして物事を解決するのは簡単ですが、その先にあるのは複雑で楽しくない運用になってしまうので、本当に必要なものを日々振り返りながら、頑張っていきたいと思っています。

    現在、NOT A HOTELのSoftwareチームでは採用強化中です。 カジュアル面談も受け付けておりますので、お気軽にご連絡ください。