TECH PLAY

株式会社メルカリ

株式会社メルカリ の技術ブログ

261

こんにちは、メルカリのQAエンジニアのFunakiです。今回は品質改善と可視化のための取り組み、特にバグ管理(Bug Management)に焦点を当てて、QAチームがどのような活動を行っているのかをご紹介します。 我々は2018年頃からバグ管理の取り組みを始め、試行錯誤を重ねてきました。製品の品質に関する課題を抱えた方や、品質の可視化を進めたいと考えている方にとって、当ブログが現状を改善するきっかけになれば幸いです。 (出典: https://loosedrawing.com/ ) なぜBug Managementを実施しているのか? 我々はプロダクトの品質を推測するために、バグチケットの管理や可視化するすることを目指しています。品質を推測するために、品質の可視化するための環境構築(ダッシュボード)や、バグのチケット管理ルール(Bug Management Guideline)を作成しています。 もともと、メルカリでは各開発チームが独自にバグの管理をしていました。多くのチームではJIRAを使用してしましたが、JIRA以外で管理をしているチームもありました。 また、チームストラクチャの再編により軽微なバグの担当者がいなくなり、長期間未対応のまま放置されることがありました。 それらの影響でバグチケットの全容が十分に把握できなくなっていました。 これらの問題を改善していくためにBug Mnagement を実施しています。 Bug Management Guideline とは? 我々はバグチケットを健全に管理出来るようにして、品質の見える化をするために、Bug Management Guideline を作成して開発チームへ展開をすることにしました。 ルールを作ると、守らなければならない事項が多くなりがちで、結果として誰もルールを守れなくなることがあり得ます。そうならないよう、私たちは以下の最低限の目標を設定しました。 目標: 1. バグ管理環境をJIRAへ統一 2. バグの発生状況や修正の優先順位が判断できること 3. バグが長期間放置されないこと 目標1を達成するために、まずは各開発チームが使用しているバグ管理ツールを調査し、JIRAを使用していないチームにはJIRAへ変更をお願いしていきました。 目標2を達成するために、バグ修正の優先順位やバグの発生傾向などを分析が出来るように、バグチケットに情報を記載するフィールドを追加しました。 目標3を達成するために、バグチケットの有効期限を設定しました。有効期限が切れたバグチケットが無いか定期的チェックし、期限が切れたチケットはクローズするか、優先順位を上げてすぐにに修正するかを判断するルールを策定しました。 Bug Management Guideline を作成し、各チームが共通の環境とルールを使用することで、Bugチケットの全容が把握するための準備が整いバグチケットの状態の可視化をすることが出来るようになりました。 バグチケットの状態の可視化 JIRAにもチケットの情報を可視化するダッシュボードの機能がありますが、我々がチェックしたい情報を可視化する事は出来ませんでした。そのため、当初はJIRAで管理されたバグチケット情報をLookerで可視化していました。 JIRAのバグチケットの情報は直接Lookerで利用することが出来ないため、JIRAのバグチケット情報を trocco を利用してBigQueryにインポートし、BigQueryの情報からLookerで様々なグラフを作成してダッシュボーを構築していました。 以下の画像は、取り込まれたデータの流れと作成したダッシュボードのサンプルです。 過去の取り組みについて少し紹介した関連記事については、以下のURLをご覧ください。 関連記事: メルカリのQAエンジニアの取り組み2020 可視化(ダッシュボード)の改善 ダッシュボードは開発チーム毎に作成し運用していましたが、メルカリの開発体制やメンバーが頻繁に変動するため維持管理が大変でした。さらに、troccoやBigQueryの環境はQAチームが構築した環境では無かったため、環境のメンテナンスやアクセス権管理、グラフの更新・追加のためのデータ変更が複雑になってしまいました。そのため、メンテナンスがしやすい環境に切り替えることを検討しました。 バグ管理を更に効率化する方法を調査していたところ、データ取得が手軽で、グラフの資料作成も簡単に行える新しい手法を見つけました。具体的には、「 Jira Cloud for Sheets 」というスプレッドシートのアドオンと、「 Looker studio 」 というデータの分析や管理、レポート作成が簡単に行えるBIツールを使うことに決めました。 Jira Cloud for Sheetsは、JIRAの開発元であるAtlassianが提供している拡張機能で、JIRAで管理しているバグ情報をスプレッドシートに直接取り込むことが可能になります。スプレッドシートに取り込んだJIRAの情報は、関数を使い情報を分類したり、集計することで自分たちの知りたい情報を作成することができま。 またJIRAは今の情報しか取得することが出来ません。そこでGoogle Apps Script(GAS)を使用して集計データの履歴を日別に作成しました。履歴を作成したことで、バグチケットの作成や対応件数の傾向を確認することが出来るようになりました。 Looker Studioは、スプレッドシートからデータを直接読み込んでグラフや表を好きなレイアウトでダッシュボードを作成することが出来ました。そのため、他チームに依存することが無くなったため、任意のタイミングでダッシュボードメンテナンスが可能にりました。 また、データの取得や表示データの更新は、アドオンの機能やGASのスケジューリング機能を使って定期的に実行しているため、毎日自動的に情報が更新されるようになっています。 これらの改善により、マニュアルでのメンテナンが最小限になり、バグ追跡と分析もスムーズに行えるようになりました。 今後の Bug Management Bug Management Guidelineを作成し、Looker Studioでの可視化のおかげで、バグチケットが修正されずに残っている場合や、いつ何件のバグチケットが作成され、クローズされたかなどが一目でわかるようになりました。定期的にバグチケット作成からの経過時間をチェックし、優先順位の見直しをすることで、バグチケットが長期間放置されなくなりました。 これらの取り組みにより、適切にバグチケットが管理することができるようになりつつあります。しかし、開発体制の再編や新しいメンバーの増加など影響で、取り組みがリセットされないようにBug Managementの周知が必要です。さらに、バグ発見の傾向や件数から製品の品質を推測し、バグの作り込みを防止する施策の検討などを続けていく予定です。
アバター
はじめに こんにちは、メルカリの日本リージョンのCTOを担当している@kimuras と申します。2023年4月にCTOに就任して現在Marketplace、Merpay、Mercoinの技術的な責任者を担当しています。本稿では、この1年間で注力してきた、Engineering Roadmapの作成についてお話したいと思います。内容によっては、ある程度の組織の規模感にならないと適さない内容となってしまうかもしれませんが、サービスの方向性やそれに合わせたエンジニアリング組織の作成について、今後整理しなければならない局面でご参考にしていただけたら幸いです。 メルカリのロードマップとは メルカリには、グループ全体の指針となるグループロードマップ(以下ロードマップと呼びます)があります。このロードマップのおかげで、私たちは今後進むべき方向が明確になり、社員全員が提供したい価値についての共通の認識を持つことができます。ロードマップは単なる実現したい事項のTODOリストではなく、私たちのミッションやビジョンを正確に理解するための重要なツールです。メルカリのロードマップについては、こちらの メルカンの記事 を参照してください。 Engineering Roadmapの必要性 ロードマップがうまく運用されていることで、わたしたちはこれまでに多くの新しい価値を提供してきました。その中にはメルカードやMerpayのような時間もかかり、難易度も高いプロジェクトも含まれています。しかし、エンジニアリング組織としては、このロードマップに対してより先行して技術的な準備ができていたら、より高速かつ計画的にビジネス展開をできたのではないかと感じることがありました。 事業の未来がロードマップで示されているので、エンジニアリングとしてはその道標に対して、それを実現するための Foundation やPlatformを事前に提供できることが理想的です。しかし、これまでメルカリグループでは各Divisionごとに個別のEngineering Roadmapが存在していたものの、全社横断でのものは存在しませんでした。(※ 12/26 10:30 初稿ではロードマップが一切存在しないようにとれる表現になっていましたが、正しく修正しました) メルカリではビジネスや開発者をスケールさせるためにMicroservices Architectureを導入したり、 インドの開発拠点 を作ったりと、チャレンジングなことを通じて継続的なエンジニアリングの改善を行ってきました。しかし、事業のロードマップに対して、Engineering Roadmapも同時に用意することで、よりエンジニアリングも含めたVisionがクリアーになり、効率性が上がるのではないかと考えました。 Engineering Roadmapがあることのメリット 前提として、私たちの開発のレイヤーは主にProduct、Foundation、Platformの3つのレイヤーに分かれています。Product開発は主にBFF、BackendやFrontendの開発を含めたFeature開発となります。そのひとつ下のレイヤーであるFoundationはLogisticsやTrsansactionやPaymentなどのProductとは疎結合ではあるものの、さまざまなサービスから呼ばれる重要なバックエンドのAPI群となります。そして、Platformはさらに一番下のレイヤーであり、Microserviceを容易に作るためのMicroservices PlatformやCI/CD、Infrastracture、Networkなどのすべてのサービスを支える基盤となっています。したがって、下のレイヤーになるほど支えているサービスが多くなるため、PlatformやFoundationは上位レイヤーのことを考慮しなくてはならないことが多く、開発や変更の時間軸は長くなってしまいます。 このような私たちの状態を前提として、Engineering Roadmapが存在することの意義を以下に述べていきます。なので、序盤にも述べたように、スタートアップのような開発の初期段階のフェーズやFoundation領域が小規模なサービスでは、本稿で述べるEngineering Roadmapの作成する意義や戦略とは違った打ち手の方が良い可能性があることをご容赦ください。 スケジュールに対する期待値調整が容易になる 抽象的な表現となってしまいますが、何か新しい価値提供を実現するためのリアーキテクチャや、新たにFoundation/Platformを開発をするには、想定以上に時間がかかってしまうことが一般的に多くあります。開発を計画的に行わず、間に合わせでライブラリを少し修正するだけですませてしまったり、本来であればアーキテクチャを改修しなければならないところを、改修せずに無理に既存のアーキテクチャに新機能を詰め込んでしまったがゆえに、後のメンテナンス性が落ちてしまったり、リファクタリングが困難になることが起こりがちです。 したがって、エンジニアリングとしては極力新しい要件仕様に対して、適切なFoundation/Platformを新規で開発したり、リアーキテクチャをしたうえで新規機能を実装することが理想的です。しかし、これらの開発には調査や設計、実装方針について関係者とコンセンサスをとるなど、実現するのに数日どころか数ヶ月、あるいは年単位で時間がかかってしまうという問題があります。 このため、新規サービスの開発を始めるタイミングで、Product開発と並行してリアーキテクチャやFoundation/Platform改善をおこなうと、時間軸が合わなかったり、スペックの調整をしながら開発することで要件漏れや大きなバグを作ってしまうことの原因となってしまいます。加えてProduct開発に対してFoundation/Platform側の対応が遅れてしまい、リリーススケジュールに悪影響を与えてしまうこともしばしば発生してしまいます。 ただ、上述のように事業のロードマップが示されている状況においては、エンジニアリングとしてもそれを実現するためのFoundation/Platform開発を事前に計画性をもって行うことができれば、よりスムーズに開発することができるし、メンテナンス性や安全性もより担保された開発を行うことができます。 Engineeringの改善施策のコンセンサスを得ることができる 上述のようにリアーキテクチャやリファクタリング、Foundation/Platform開発などのエンジニアリングに関する改善施策は中長期にわたることがしばしばあります。このため、明確な目的意識を持って施策を実施しなければ、途中経過でプロジェクトの意義を問われることや、プライオリティを下げざるを得ない状況となってしまうことが、残念ながらよく発生します。エンジニアリングには各改善プロジェクトの意義について説明責任はあるものの、事前にコンセンサスがとれておらずに説明の難易度が上がったり、プライオリティが変更されてしまうことは生産性に悪影響があるし、モチベーションにも大きな影響を与えてしまいかねません。 しかし、エンジニアリング主導の改善施策についても、始める前にそれぞれの意義やゴールを明確化して、かつロードマップにアラインできていれば、たとえ中長期な開発であってもステークホルダーからも賛同を得られ、サポートを得ることができるはずです。時には事業のロードマップにアラインすることが難しい中長期の改善施策、例えばMicroservices Architectureの根本的なアーキテクチャの改善や、BCPの改善などについても、ゴール設定と得られるメリットを明確化して、Engineering Roadmapとして事前にステークホルダーや経営から同意を得られていれば、ストレスなく改善プロジェクトを継続することができます。 先を見通したアーキテクチャを作ることができる 基本的にシステムアーキテクチャはビジネスの成長やエンジニアリング組織の規模感、ビジネスの方向性などにあわせて常に改善を続けなければなりません。加えて、極力メンテナンス性や拡張性を高くすることで継続的に新たなニーズに応えられることが理想的です。 しかし、ビジネスの方向性が定まっていなければ、ある程度は想像でシステムの拡張性を担保しなければならず、仮にニーズを満たすことができなれけば、近い将来にリアーキテクチャを実施しなければならなくなります。 一方、事業ロードマップやEngineering Roadmapが作成されていれば、3年ほどの近い将来については概ね方向性がわかっているため、拡張性の観点で確度の高い設計をすることができます。これは、設計を担当するアーキテクトやTech Lead(技術的なリーダーのことであり、以下TLと呼ぶ)に限らず、エンジニアが日々のコーディングでの細かい意思決定を手助けすることができるため、すべてのエンジニアが意識的に将来を見据えた設計を心がけられるようになることが好ましい。 例えばIDに関する設計をしているときに、将来的にどのような事業展開をするのか、またパートナー企業が存在するようなビジネスをするときにパートナーアカウント、あるいはID連携が必要になる事業計画がある、といった計画が事前にわかっていれば、それらのニーズに合わせたアーキテクチャの設計ができます。これはFoundation/Platformやインフラストラクチャなどさまざまな要素技術にとっても重要であり、ビジネス成長には欠かせないことです。 Visionに対する解像度が深まる Visionを作ることは、組織にとってとても大事なことです。Visionを示すことによって、これから先に新たにお客さまに提供したい価値や、組織のありたい姿などを掲げて、組織で一体感を持ってタスクに取り組むことができます。 しかし、Visionだけではそれをどういう手順や手段で実現していくかはわからず、説明される方もうまく咀嚼できないことがあります。ありたい姿をVisionで示し、それに対してどのようにそれを実現していくかをEngineering Roadmapに記載することで、Visionに到達するまでのストーリーが各エンジニアにも伝わり、より理解を得ることができます。 これは説明する側のコストも下がりますし、ミスコミュニケーションを防ぐためにも重要だと考えています。 Engineering Roadmapを作るためのTips Engineering Roadmapの必要性や効果がわかったところで、次に実際にロードマップを作るためのTipsについて説明します。ここでは主に2通りのアプローチを突き合わせる手法について紹介します。 まずは大胆な理想像とVisionを作る 自分の場合は、あまり多くのことを気にしすぎて進められなくなるよりも、まずは実現可能性や周りの考えなどは考慮せずに、大胆な理想像を決めてしまいます。 実際にVisionやEngineering Roadmapを作ることは容易ではありません。理想的なゴールは何なのか、ステークホルダーはどのようにゴールを考えているのか、お客さまは何を求めているのか、それを実現することが可能なのか。それらの多くの関連する要素を考慮すると、なかなかVisionやロードマップを定めることができなくなってしまいます。 ただ自分は、あえて実現することが難しいのではないかと思うくらいの大胆で理想的なゴールを決めます。それから、それを実現するためのロードマップを作りながら実現可能性を考慮して、Visionを少しずつ現実的なものに落とし込んでいくことで、多少難易度が高いが、納得感のある形に落ち着くことができます。万人には当てはまらないとは思いますが、まずはあまり固くならずに、大胆で理想的なVisionを書き出してみると良いと思っています。 TLとのコミュニケーション強化 ある程度の組織規模のCTOの立場になると責務のスコープが広くなり、開発現場での解くべき課題や理想的な状態などが把握しづらくなってしまうことがあります。 普段からVPoEやEMとのコミュニケーションを取ることで、組織課題を把握することができますが、より開発現場に近い課題感を把握するためにはTL(TLを指定していない場合はエンジニアチームをリードしている立場の方が良いでしょう)とのディスカッションをすることで情報を得ることができます。 TLとEMとのディスカッションをすることで開発現場でも納得感の高く、かつ的確に組織課題を捉えたRoadmapを作成することができると、自身の経験から強く感じています。 理想は「現実的でワクワク感」があること ここまで2つのアプローチについて紹介しましたが、進め方としては、まずはフィージビリティを気にせずに、技術的にチャレンジングでかつビジネスに貢献するようなVisionを「トップダウンのアプローチ」で作成してみます。しかし、これだけでは現実離れしすぎてしまうかもしれませんし、本質的な開発現場の課題をとらえられず多くのエンジニアから共感を得られないかもしれません。そこで、各領域での本質的な課題を把握しており、強いVisionを持っているTLや、組織課題を理解しているVPoEやEMからの「ボトムアップ」の意見をぶつけあうことで、チャレンジングでかつ現実的なVisionやEngineering Roadmapを作成できることが理想的です。 このトップダウンとボトムアップの意見をすり合わせることによって、私たちのこれからの開発を一人一人のエンジニアが自分事として捉えて、積極的にコメントをくれるようになり、かつコミットしてくれるようになります。CTOとしては、エンジニアがこれまでに挑戦したかったけど、挑戦できなかったような難しくもおもしろい課題に挑戦するための理詰めを支え、最終的にその挑戦に対してスポンサーとなって一緒に実現しようとする姿勢が大事なのだと思います。このように難しい課題であっても、みんなで同じ方向を見て、一緒に解決していくことで、エンジニアたちのワクワク感が生まれ、結果的に自信を持って自分たちで誇りに思える技術を使い、お客さまに新たな価値を提供できるのだと信じています。 最後に 最後までお読みいただき、ありがとうございました。Engineering Roadmapは作っただけではなくて、今後どのように運用していくか、進捗させていくか、あるいはEngineering Roadmap自体を更新していくかもとても大事だと思います。運用していく中での困難や発見があれば、また記事にしてまとめてお伝えしていきたいと思います。
アバター
この記事は Merpay Advent Calendar 2023 の 24 日目の記事です。 こんにちは、メルコインの @pooh です。 メルカリグループでは金融事業を営んでいるメルペイとメルコインのEngineering Manager(EM)で普段とは別の場所に集まって1日集中して議論をするOffsitesを定期的に実施しています。 この投稿ではOffsitesそのものを紹介するのではなく、Offsitesでよく実施されるワークショップ(参加型作業)についての4つの工夫を紹介します。 複数人が集まって、何かのテーマについて意見を出し合い、意見をまとめて発表するというワークショップはよくあると思います。これから紹介する方法を使用することでより活発な成果が望めます。 本記事では私の経験とメルカリという組織での実践上の知見を書いています。そのため、組織ごとに別のよりよいやり方もあると思いますので、参考程度にそういう考えもある、ぐらいの気持ちで読んでください。 1.付箋に書いてから発表する ワークショップではチームに分かれて、チーム内でディスカッションをして意見やアイディア出しをしていきます。例えば、EM Offsitesでは「2023年10月〜12月を振り返って良かったこと・悪かったこと」といったテーマを設定し意見を出しあいました。このときに思いついた人から口頭で順次発表していくことがあります。ここで1つ目の提案となります。 最初に時間をとって各自で意見やアイディアを手元の付箋に書く 各自で書き出しをした後に発表をしていくとよいかもしれません。最初に付箋に書き出すことによる期待効果は次の通りです。 考えて書き出しているので意見がまとまる 発表に時間がかからない 書いている間は他人の意見が見えないこと 1人ずつ書かずに口頭で発表する場合、2、3人目からは「私もそうなんですけど…」という意見が出やすくなったり、他の人の意見に左右される可能性があります。最初に付箋に書いて発表することで、主体的に自分で考えたアイディアや意見を発表できるようになります。 付箋にあらかじめ書いてあることを読み上げるので、1つの発表が長くなったり、発表の始めと終わりで内容が異なる状態を防ぐことができます。 オンラインとのハイブリッド開催の時 オフラインとオンラインのハイブリッド開催をする時には、付箋ではオンライン参加者には見えずに不便でした。ハイブリッド開催の時にはオンラインホワイトボードを使いました。オンラインホワイトボードを使う場合でも、各自で考えている時には別のファイルやPC上のエディタを使って他の人から見えないようにすると付箋に手書きと同じ効果を得られそうです。 この投稿の本題ではないのですが、ハイブリッド開催の時にはPCのマイクとスピーカーでは音量面でオンライン、オフライン相互に聞き取りにくいことがあります。外付けのスピーカーとマイクの用意をお勧めします。マイクロソフトの「Modern USB-C Speaker」は持ち運びがしやすく、音量も大きく良かったです。 2.順番に1つずつ発表する グループディスカッションでは付箋に書き出して発表します。発表する際には1人ずつ順番に発表していき、最終的に各自で書いたものをまとめてグループの成果として発表する形式を取ります。ここで2つ目の提案となります。 順番に1つずつ自分の書いた付箋を読む。1つ読んだら次の人が読んで、を繰り返して何周か回す。自分の番がきて、もうすべて自分の書いたものを読んでしまった人はパスしていい この方法で発表していくと1人の意見で全体の雰囲気が動くのではなく、みんなの意見が順番に出てくるので発言の機会が均一になります。意見が平均的に出せるようになるため雰囲気が悪くなりません。「けっこういい」とみんなが共感できる意見が色々な人から出てくるため、雰囲気がよくなります。 発表するときには書いたことを発表し時間をかけないようにします。書いた理由や背景などの説明はせずに付箋に書いたことを発表します。小さな付箋を使うと単語しか書けないので大きな付箋を使い単語ではなく発表する内容を書いておきます。 EM Offsitesでは「良かったこと」ではつぎのような発表がありました。 メルペイのTech PRで新しい取り組みができている リリースした口座入金経由で直接メルコイン口座に残高反映する機能が使われている メルコインのEngineer All-Hands(エンジニアメンバーを対象とした毎月開催している全社会)が前回評判よかった気がする 「悪かったこと」ではつぎのような発表がありました。 リソース不足でPJのスケジュールが遅延したり厳しいスケジュールになった プラットフォーム機能開発が進まなかった 情報共有が不足していた 3.問題を「どのようにすれば」に置き換える ワークショップでは「xxxxに関してどんな問題点や懸念点があるか」や「悪い点」などをリストアップすることがあります。それをリストアップしていくと、当然ながら「なかなか難しいね」となることがあります。ここで3つ目の提案になります。 問題を発表した後、それぞれを「どのようにすれば〜〜〜か?」の疑問に言い換える 効果を説明する前に具体例を出してみます。 「プラットフォーム機能開発が進まなかった」という問題を発表した場合、「どのようにすればプラットフォーム機能開発ができるか?」となります。 このように「どのようにすれば」の質問文にすることで、答えを考えられるようになります。「問題なのはわかったから、改善策を言って欲しい」という言葉をたまに耳にすることがあります。課題を質問文に言い換えてもらうことで、自然と答えを考え始められます。 4.もっと面白い質問にする 「どのようにすれば」の質問文にすることで、答えを考えてしまう状況を作れました。もう一歩進められるようにします。 もっと面白い質問のかたちにして、もっといろんな人が考えてくれるようにする もっと面白い質問にするために次のようにします。 「これが起こったらいいな〜」と思うような文章にする 「日本一」「世界一」と言った言葉をいれる 先ほどの質問文「どのようにすればプラットフォーム機能開発ができるか」を「どのようにすれば日本一便利なプラットフォーム機能を開発できるか?」に変えてみます。これでただ単に課題を解決するだけではない、ベストな素晴らしい解決策を多くの人が考え始めたはずです。 まとめ この記事では、つぎの4つの工夫を紹介しました。 付箋に書いてから発表する 順番に1つずつ発表する 問題を「どのようにすれば」に置き換える もっと面白い質問にする これらの工夫のうち、1つ目と2つ目は実際に私がワークショップでファシリテーターをする時にしていることです。EM Offsitesのときにも利用しました。3つ目はOffsitesで使ったわけではないですが、日頃心がけています。自分で使用したり、問題を提起してくれた人に使ったりしてます。4つ目はなかなかできていないです。 実際に1つ目と2つ目について利用したメンバーに感想を聞いてみました。 みんなの意見を聞くことができる & たくさん話したい人は後半話す時間がある、ということで時間効率を最大化できてると思いました 参加者の意見を満遍なく聞けることと、意見の数によってはスキップする自由みたいなところもあったので、意見が出やすくてよかったのでは無いかと思いました ワークショップの目的の認識合わせが不十分だったので、手法以前の改善ポイントがあったように思います。その点からワークショップとしては消化不良でした。 手法としてはポジティブなフィードバックをもらえました。一方でワークショップのゴールが何か、ワークショップが終わった後に何を達成したいのかの認識合わせを最初に実施することの重要性を再認識しました。 ここで紹介したことは、大橋禅太郎氏の「すごい会議」で紹介されていたものになります。この投稿を書くにあたって改めて読みましたが、同僚にも勧めたいと思える書籍です。 書籍では手法も紹介していますが、ワークショップの目的の認識合わせをする「このワークショップが終わったときにどんな成果をあげることを期待しているか」から意見出しをしていました。 会議進行は方法によって効果的になったり非効率になったりします。効果的になる方法については全員で共有していければと思います。 明日の記事は kimurasさんです。引き続きお楽しみください。
アバター
こんにちは。メルカリMarketplace, Foundation EngineeringのDirector, @mtsukaです。日々新しい技術を追い求め、挑戦を続けるMercari Engineeringですが、そんな部門にしては少し毛色の違った部類のチームです。どちらかというと、中長期の視点から、より良いビジネス貢献であったり、より良い開発体験を支える基盤開発を中心に、じっくり腰を据えた仕事をしています。 この記事は、 Mercari Advent Calendar 2023 の23日目の記事です。 メルカリは2021年10月から既存のシステムの解析、改善を大規模かつスピーディに行うという、難易度の高い全社的なリファクタリングプロジェクトRobust Foundation for Speed (RFS) に中期的に取り組んできました。本取り組みは、2023年7月末に各ドメインの改善が無事一段落し、プロジェクトという形は一旦解散としました。こういった取り組みの主要な結果は、具体的な成果として認知されるまでに、数ヶ月数年を要することもままあります。幸運なことに、すでにいくつか具体的に成果として示せることがありますので、昨年に引き続き、うまくいったところ、うまくいかなかったところ、今後の方針など、RFS全体をプロジェクトのオーナーの視点で振り返っていきたいと思います。また、各ドメインについては、ドメイン知識・疎結合化・文化醸成の観点からプロジェクトの成果を解説しています。 プロジェクト発足の背景 改めて、RFSプロジェクトは2021年10月に正式な中期プロジェクトとして発足しました。詳細な説明は 連載:技術基盤強化プロジェクト「RFS」の現在と未来 | メルカリエンジニアリング に譲りますが、事業に関わる共通基盤をうまく抽象化していく、保守性を良くしていくことで、機能実装のリードタイムを一定以下に維持し、これによって間接的に事業貢献をしていこうという取り組みです。このプロジェクトへ参画したいという意思表明が多くのドメインからありましたが、結果的にビジネスインパクトなどを鑑み、Transactions & Checkout(以降Transactionsと呼称)、CSTool, Logistics, ID Platform(以降IDPと呼称)の4ドメインでこの取り組みを行うことにしました。 プロジェクトスコープの設定 リファクタリングなどの改善プロジェクトを発足するときに、改めて気付かされるのはあらゆる資源は有限であるということです。こういったプロジェクトは、ともすれば全てを作り替えてしまいたい衝動に駆られるのが人の性でしょう。特にエンジニアであればそういう気持ちになる方は多いのではないでしょうか。もちろん気持ちとしてはとてもよくわかるのですが、やはりこの取り組みも事業の一環ですから、あれもこれも全てに資源投下というわけには参りません。また、 人月の神話で言われているセカンドシステム症候群 のようなことも避けねばなりません。これらのポイントを考慮して、最終的にはシステムの変更頻度や他システムとの結合度合いを考慮してスコープを決めました。この点については、人によっては不満もあったと思いますし、実際に一部ドメインの調査・分析や関連する議論が収束するまでには半年位の期間がかかってしまいました。個人的には、現場のメンバーが一番知見をお持ちなので、それを尊重しつつ納得感をある程度持ってもらいたかったのですが、正直少し時間をかけすぎてしまったので、明確なしきい値や基準など、もう少し具体的に事前に提示したほうが良かったと感じています。 プロジェクトの定点観測 スコープが決まればロードマップを引いて、OKRを設定し、あとは手を動かしていくだけですが、ビジネスグロースの案件などとバランスをとりながら、うまく説明責任を果たしていく必要がありました。このため、あまり好まれるやり方ではないにせよ、週に一度の定期チェックインミーティングを用意し、CTO/VP同席のもとプロジェクトの進捗を管理しました。原則としてOKRとその進捗をDivision全体で共有し、トラッキングすることでプロジェクトの透明性担保や早期のブロッカー除去に務めました。また、最初期にはカンパニーのOKRとして経営層への定期的な進捗インプットも行いました。 担当チームが解散してしまっていたり、メイン開発者が退職していてドキュメントも存在しないようなコンポーネントを含むドメインでの作業なので、透明性を担保しながら情報を共有し続けることは極めて重要であったと思います。一方で、忘れられた仕様が発見されたりするなど、スケジュールを遵守するという観点では苦労も多かったです。スケジュールマネジメントの観点ではThe Six Week Cycleの Tracking Work on the Hill Chart の考え方を大いに参考にしました。 プロジェクト全体の振り返り さて、このようなプロジェクトでは成果を既存事業への貢献として評価することはとても難しいです。定量的に計測したものは、リードタイムの増減、データベース分離数、マイグレーション数、削除したコードや廃止したAPI, それぞれの費用対効果などを計測しました。その他、定性的にはリファクタリングのマイルストーン達成状況や実際のチームの体感等などを集計しました。このような取り組みを経て、RFSが会社の期待にどのように答えたのか、振り返りを実施しました。かんたんなプロジェクトの総括としては、今後の事業計画・成長を見据えた基盤そのものと基盤維持の仕組みがある程度構築されたので、この取り組み自体は将来へ繋がる意味のある投資であったと確信しています。その旨をMercari Engineering Boardへ報告する形で説明責任を果たしました。個別の詳細については、後述の「各ドメインの振り返り」を参照ください。 各ドメインの振り返り 以下に、各ドメインでの取り組みと振り返りをまとめていますので、ご覧ください。 Transactions Transactionsはお客様が商品を購入してから手元に届くまでの各ステップを司るメルカリのビジネスを構成するAPI郡(以降mercari-apiと呼称)のいちコンポーネントです。複雑化したモノリシックなmercari-apiからこれら関連するコンポーネントを切り離し、保守性を担保しながら抽象化、単純化していくことでプロダクト開発の後押しをするために、チームの組成から着手しました。チームの成り立ちがRFS起因のため、スコープの決定や抽象化のプランの検討は比較的スムースでした。 Transactionsドメインとして mercari-apiから切り離された機能は以下のとおりです: チェックアウト (モジュール化完了) 購入履歴 (モジュール化完了) チェックアウト料金計算機能 (Golangの独立したマイクロサービスとして実装) 配送 (モジュール化完了) これらのモジュール化やリファクタリングを通じて下記のような成果を得ました。 ドメイン知識 綿密なコード解析とリバースエンジニアリングを行い、アプリケーション全体の中で最もビジネス上複雑で重要なドメインに関する知識を、組織として得ることができました。 疎結合化 TransactionsドメインはC2C Marketplaceシステムの中心的なコンポーネントなので、多くの新機能が恒常的にTransactionsドメインの連携を必要とします。このMonolithicな実装のサブコンポーネント郡をモジュラーモノリスとしてリファクタリングすることで、その後の開発に多くの良い影響をもたらしました。例えば機能境界が明確になったので、不具合の発見やリスクのコントロールがしやすくなり、Transactionsドメインに変更を加える成果物の品質が向上しました。また、本取り組みにおける調査の結果が知見として蓄積されたことに加え、認知的負荷の軽減により、オンボーディングも比較的簡単になりました。 上記から派生した効果として、新しい機能や要件実装のリードタイムを大幅に短縮できるようになりました。実例をあげると、チェックアウト料金計算機能によって料金管理が一本化されたため、料率の変更や新しい決済方法の導入などの実装工数が最大3ヶ月から1週間未満に短縮されました。また、CSToolやLogisticsなどの他のドメインとの依存関係を切り離すことができたので、より独立してシステムを維持していくことができるようになりました。 文化醸成 リバースエンジニアリングとリファクタリングと並行して、チームには "reading parties" という独創的で魅力的なコード分析の文化が生まれました。ここから生まれたドキュメントはオンボーディングへ応用されるだけでなく、他のチームとドメイン知識を共有するためにも活用されています。また、今後もTransactionsドメインの変更には多くのチームが関与し続けていくことになるため、将来に渡って意味のある成果になるでしょう。 参考記事 Understanding and Modernizing a Legacy Codebase メルカリの取引ドメインにおけるモジュラーモノリス化の取り組み クライアント・サーバサイドに分散する計算ロジックのマイクロサービス化 Logistics メルカリは多様な配送手段をサポートしています。Logisticsは言葉通りこれらの配送方法を司るコンポーネントです。ご存知の通り、配送方法はメルカリというサービスの成長とともに時間をかけて増えていったものなので、Logisticsコンポーネントも時間の経過とともに複雑さが増してきました。そのため、スコープの確定は早かったものの、他ドメインと比較してゴールの設定難易度が非常に高かったです。最終的にLogisticsドメインでは、重複しているクラスを排除してシンプルにするなど、主にシステムの再設計を行いました。具体的には、メルカリの歴史とともに育ってきた22のコンポーネントの疎結合化を目指したかったのですが、これらすべてを疎結合化するには現実的な時間が足りませんでした。そのため、将来につながるメンテナンスの一環としてインターフェースやデザインを極力共通化することにしました。すでに動いており、しかもビジネスの根幹を担うシステムを改善するわけなので、そういう観点でも難易度は相当なものです。成果自体は次に繋がる良いものでありつつも、残念ながら、この取り組みの見た目的な成果はドメインの中では一番物足りないものでもありました。このあたりは、より良いアプローチを模索していきたいです。 取り組みを通じて下記を達成しました。 ドメイン知識 重複排除などの作業を通じて新しい配送方法の追加、配送料金の変更方法などが統一され、結果としてチームのドメイン知識が増しました。配送手段の仕様はパートナー企業の仕様に依存するものの、社内で扱うインターフェースをある程度共通化することで、全体の把握がしやすくなりました。また、共通化の恩恵として学習コストも格段に下がりました。 疎結合化 Logisticsドメインはパートナー企業との関係もあるので、利用料金の変更や提供プランの変更などが発生した場合、直ちに対応を行わなければなりません。このため、システムの複雑さを解消することは極めて重要な取り組みでした。結果として、疎結合化自体は進みませんでしたが、デザインパターンの適用と再設計を通じて、機能追加や変更が簡単になりました。実際に、配送サービス利用料改定に関わるリードタイムは、チェックアウト機能との連携も完了し、期間も約3ヶ月から1/3の約1ヶ月に短縮されました。今後も新しい配送方法の追加、料金改定、その他将来発生するであろうユースケースについても同じような対応ができることでしょう。 文化醸成 チームがリファクタリング用のバックログを持つようになりました。このバックログは定期的に内容の確認と改善の検討が行われ、必要に応じてメンバーがアサインされるようになりました。 CSTools CSToolsはいわゆる顧客対応ツールです。お客さま対応のためのツールですから、その機能やサポート範囲は多岐にわたり、システムは年々複雑化していく一方でした。このため、スコープの策定議論は一番紛糾したのではないでしょうか。そもそもお客さま対応のためのデータベース数が膨大なため、これをどこまで疎結合化するのかなどが論点になってしまい、スケジュール的にもマイルストーン的にも難しい展開が発生していましたが、最終的に他3ドメインの改善に関わるブロッカー除去を優先するという前提でスコープを設定することで、議論を収束しました。 ドメイン知識 重複排除などの作業を通じて新しい配送方法の追加、配送料金の変更方法などが統一され、結果としてチームのドメイン知識が増しました。配送手段の仕様はパートナー企業の仕様に依存するものの、社内で扱うインターフェースをある程度共通化することで、全体の把握がしやすくなりました。また、共通化の恩恵として学習コストも格段に下がりました。 疎結合化 詳細は後述のブログポストに譲りますが、注力ドメインとの関係性と変更頻度を軸にDBの疎結合化を行いました。これによりCSTools開発に関わる調整相手が減って、クイックに改善活動ができるようになりました。また、追加で古くから一部のQA業務が依存していたシステムのGKEマイグレーションとサービスアウトを実施できました。これにより、QAやテスト環境の統合が進み、システムのコスト面でも貢献することができました。 文化醸成 Post RFSの一環としてCSToolsドメインにFoundationチームが組成されました。このチームはCSToolsドメインの共通基盤やフレームワークをパッケージとして各エンティティに提供することで、個別のエンティティに個別のツールを作らなくても良い状況をもたらすことに責任を負っています。正式にこういった組織を持つことを認められたのも成果の一つと言って良いかもしれません。 参考記事 メルカリCSツールにおけるDBの疎結合化への取り組み GKE 移行を進める上で発見したシステムの問題をどの様に解決したか CS Toolのフロントエンドのリプレイスプロジェクトについて ID Platform ID Platform(以降: IDP)は、メルペイ創業前後にmercari-apiから認証認可の機能を中心にスピンオフしてPlatform化したものです。チームの組成当初からビジネスプランを後押しすべく、然るべきタイミングで然るべきことをやっていくという方針でチームが運営されていたと記憶しています。一方で、どうしても急ぎの実装や設計が先行しがちなことは変わりません。内外各ステークホルダーとのアカウント連携など、常に現状のビジネスを改善する業務に追われている状況で、後々に判明した考慮漏れの修正、リファクタリングなどの時間を確保することが簡単ではない状況でした。RFSでの注力対象にピックアップされたタイミングのIDPチームは、当時メルコインサービスの開発をサポートしていました。もともとチームの思想がRFSに近く、ある程度成熟していたチームなので、RFSとして直接何かの機能改善やリファクタリングをお願いするようなことはせず、メルカリグループ全体の方針などをシェアしながら、現在の設計や実装が今後の抽象化にうまく繋がるように支援しました。 ドメイン知識 IDと認証認可の領域は比較的専門家が少ないため、知見が偏る傾向があります。現在チームはTLの育成と知見の共有などを行いやすい構造になりました。RFSの取り組みと考え方は、この文化醸成の一助になったと信じています。 疎結合化 IDPはもともとはメルカリとメルペイというサービスだけが存在する世界線で実装されたものですから、比較的明確に密結合な場所がありました。日々メルカリを利用してくれるお客さまに新しい価値を提供するためには、この密結合が段々と足かせになりつつあります。今後の取組次第でどうなるかはわかりませんが、この結合度合いをある程度疎に維持できるようになりました。 文化醸成 元来IDPでは、一部の専門家が特定のユースケースを基に知恵を絞って将来を視野に入れたデザインを行う傾向が強いのですが、外部の専門家も交えたドメイン知識の共有や議論などを行えるようになりました。直接は関係ないですがFIDO AllianceのAuthenticate 2023 Conferenceにてメルカリの取り組みを紹介するなどの機会にも恵まれました。 参考記事 Applying OAuth 2.0 and OIDC to first-party services Using the OAuth 2 token exchange standard for managing the identity platform resources まとめ さて、ここまで日々進化するメルカリのアプリケーションを支える基盤開発のエピソードを、長期プロジェクトの振り返りを通じてお伝えしました。 メルカリのFoundation Engineeringチームは、これからもRFSで得られた知見やユースケースを参考に、重要な共通基盤技術の保守性を維持ながらし、プロダクト開発エンジニアがサービス開発を行っていく上で不可欠なコンポーネントを提供し続けます。 明日の記事はQAチームのjyeさんです。引き続きお楽しみください。
アバター
こんにちは。メルコインのバックエンドエンジニアの iwata です。 この記事は、 Merpay Advent Calendar 2023 の23日目の記事です。 私はいまメルコインのCoreチームに属しています。Coreチームでは主にお客さまからの暗号資産の売買注文を受け付ける部分のマイクロサービスを開発運用しています。 メルコインではCI環境として GitHub Actions self-hosted runner を使用しています。またCIだけでなく、さまざまな自動化のためのワークフローの構築もこの環境を用いて実行しています。この記事では私の所属しているCoreチームにおいてGitHub Actions上に構築しているオートメーションについて紹介したいと思います。 PR-Agent PR-Agent はOpenAI APIを使って、PRのコードレビューなどを自動化してくれるActionです。 LayerXさんの紹介記事 を読んで導入しました。 機能はたくさんあるのでここでは詳細は割愛しますが、主に活用しているのはPR作成時にコメントしてくれるコードレビューと /describe コマンドで生成されるPRのタイトルと説明の自動生成です。 PR-Agentによるコードレビュー あまり具体的な例を記事中にだすことができませんが、例えば上記画像のような内容をコメントしてくれます。これによりレビュアーがぱっとこのPRの内容を理解するのに役立つことができます。また /describe を使うと自分のようにSSIAなどで説明文を端折ってしまう面倒くさがりな人でもいい感じのタイトルと説明文をAIが考えてくれて非常に便利です。 /add_docs を使うとコードコメントをSuggestしてくれてこれも便利です。 OpenAIのAPI Keyさえあれば簡単に導入できる点もよいです。一方でGitHub自体にも 似たような機能 がリリースされているので試してみたいなと思っています。 Lint いくつかのLintツールを併用していますが、ここではYAMLで記述されるGitHub Actions(GHA)のワークフローファイルに対するLintについて紹介します。実際のワークフローは以下です。 name: Actions Lint on: pull_request: paths: - ".github/workflows/*.yml" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: actionlint: runs-on: self-hosted permissions: checks: "write" contents: "read" pull-requests: "write" steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: reviewdog/action-actionlint@82693e9e3b239f213108d6e412506f8b54003586 # v1.39.1 with: fail_on_error: true filter_mode: nofilter level: error reporter: github-pr-review ghalint: runs-on: self-hosted permissions: contents: "read" steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup aqua uses: ./actions/setup-aqua with: aqua_version: v2.21.0 - name: ghalint run run: ghalint run このワークフローでは actionlint と ghalint の2つのLinterを実行しています。それぞれセキュリティを含めたベストプラクティスに則っているかをチェックしてくれるので非常に有益です。 ワークフローについては 社内のセキュリティガイドライン に準拠する形で記述しています。3rd Party ActionはFull Change Hashで固定(このフォーマットでも Dependabot および Renovate を使うことで自動更新可能) し、 permissions は最低限の権限を使うようにしています。(以後記載するワークフローファイルはすべてこのガイドラインに則って記述してあります。) また ghalint のバージョン管理には aqua を使っています。 aqua はCLIツールのバージョンマネージャで チェックサムの検証 ができたり、Lazy Installなど便利な機能もあるため使用しています。 ghalint 以外にも golangci-lint や gci など開発に必要なさまざまなCLIツールを aqua で管理しています。( aqua についてより詳しく知りたい方は aqua CLI Version Manager 入門 をご参照ください) したがって aqua は他のさまざまなワークフローで利用することになるため、以下の Composite Action を作って再利用しやすいように工夫しています。 name: Setup aqua with caching describe: Install tools via aqua and manage caching inputs: aqua_version: required: true description: | aqua version for installer, e.g. v2.9.0 aqua_opts: required: false default: -l description: | aqua i's option. If you want to specify global options, please use environment variables policy_allow: required: false default: "" description: | If this is true", the aqua policy allow command is run. If a Policy file path is set, aqua policy allow "policy_allow" is run require_checksum: required: false default: "true" description: | Set an environment variable as `AQUA_REQUIRE_CHECKSUM` cache_version: description: The prefix of cache key required: false default: "v1" runs: using: "composite" steps: # ref. https://aquaproj.github.io/docs/products/aqua-installer/#-caching - name: Restore aqua tools uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 id: restore-aqua with: path: ~/.local/share/aquaproj-aqua key: ${{ inputs.cache_version }}-aqua-installer-${{hashFiles('.aqua/*.yaml')}} restore-keys: | ${{ inputs.cache_version }}-aqua-installer- - name: Aqua install uses: aquaproj/aqua-installer@928a2ee4243a9ee8312d80dc8cbaca88fb602a91 # v2.2.0 with: aqua_version: ${{ inputs.aqua_version }} aqua_opts: ${{ inputs.aqua_opts }} policy_allow: ${{ inputs.policy_allow }} env: AQUA_REQUIRE_CHECKSUM: ${{ inputs.require_checksum }} - name: add path shell: bash run: | echo "$HOME/.local/share/aquaproj-aqua/bin" >> "$GITHUB_PATH" Auto Correct Lintとともに goimports などのコードフォーマッタの活用も重要です。 golangci-lint によって goimports などのフォーマッタのかけ忘れを弾くことは可能ですが、GHA上でフォーマットしてあげて自動でコミットをしてあげるとさらに便利です。コードフォーマッタだけでなく、 wire など自動生成ツールも使っているのでそれらもあわせて実行し、差分があればGHA上でコミットするようにしています。使っているツールをまとめると以下のようになります。 コードフォーマッタ goimports gofumpt gci Linter golangci-lint (auto fix) 自動生成 wire gomockhandler yo GitHub Workflow pinact これらのツールはすべて aqua でバージョン管理しています。 pinact はワークフローファイル内のバージョンをFull Change Hashに自動で固定してくれるツールでとても有用です。Auto Correctのワークフローは以下になります。 name: Correct codes by auto generation on: pull_request: paths: - ".github/**/*.ya?ml" - "**.go" - "**/go.mod" - "**.sql" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true defaults: run: shell: bash jobs: auto-correct: runs-on: self-hosted permissions: contents: "read" steps: - name: Check out uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version-file: "go.mod" cache-dependency-path: "**/go.sum" - name: Setup aqua uses: ./actions/setup-aqua with: aqua_version: v2.21.0 - name: Auto generation run: make gen # make taskでformatter, linter, code generationを実行 - name: pinact run run: pinact run - name: Generate token id: generate_token uses: suzuki-shunsuke/github-token-action@350d7506222e3a0016491abe85b5c4dd475b67d1 # v0.2.1 with: github_app_id: ${{ secrets.GH_APP_ID }} github_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Push diff run: | set -euo pipefail if git diff --quiet; then echo "::notice :: There is no difference." exit 0 fi echo "::notice :: There are some differences, so a commit is pushed automatically." if ! ghcp -v; then echo "::error :: int128/ghcp isn't installed. To push a commit, ghcp is required." exit 1 fi branch=${GITHUB_HEAD_REF:-} if [ -z "$branch" ]; then branch=$GITHUB_REF_NAME fi git diff --name-only | xargs ghcp commit -r "$GITHUB_REPOSITORY" -b "$branch" \ -m "chore(gen): auto correct some files" env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} このワークフローは複雑になっているので順をおって説明しようと思います。 Protected Branchの設定 マージ先のブランチは Protected Branch で保護されています。このワークフローに関連する設定としては、 署名つきコミット と ステータスチェック を必須にしている点です。すなわちAuto Correctによるコミットがこれらを満たせるようにワークフローを構築しておかないとマージできなくなってしまいます。 署名つきコミット ワークフロー内でgitコマンドを使ってコミットをすると署名がつきません。これを簡単に回避する方法としては GitHub APIを使う方法 があります。GitHub API で生成したコミットにはGitHubが署名してくれます。 ghcp を使うとGitHub APIを使ったコミットを簡単に作成できるのでこれを使ってコミットするようにします。次のコードが実際にコミットをしている部分になります。 git diff --name-only | xargs ghcp commit -r "$GITHUB_REPOSITORY" -b "$branch" \ -m "chore(gen): auto correct some files" 差分がでたファイル名をパイプで渡してコミットを生成しています。 GitHub Appを使ったトークンの生成 コミットに使うGitHubトークンにも注意が必要です。 GitHubのドキュメント に以下のような記述があります。 When you use the repository’s GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN, with the exception of workflow_dispatch and repository_dispatch, will not create a new workflow run. つまりよく使われる secrets.GITHUB_TOKEN を使ってコミットをするとそのコミットをトリガーに他のワークフローを起動できません。ワークフローが起動しないということはCIが実行されず、したがってProtected Branchのステータスチェックをパスすることができません。 これを回避するためにGitHub Appから生成したトークンを使ってコミットをする必要があります。上記のワークフローでは suzuki-shunsuke/github-token-action を使ってトークンを生成しています。 - name: Generate token id: generate_token uses: suzuki-shunsuke/github-token-action@350d7506222e3a0016491abe85b5c4dd475b67d1 # v0.2.1 with: github_app_id: ${{ secrets.GH_APP_ID }} github_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} このGitHub Appには次の権限が必要になります。 contents:write workflows:write リポジトリに自分で用意したGitHub Appをインストールして使います。GitHub Apoの準備は最初は面倒ですが、一度設定すると自動化ができることが格段に増えるので便利になります。 aquaの自動更新 CLIツールは aqua で管理しています。バージョンの更新は公式に提供されているRenovate Presetを使うことで可能です。詳細は Renovateによる自動update を参照してください。( GitHubのDependabotl にはPresetのような機能がないため、 aqua の自動更新はRenovate前提になっています。) 前述しましたが、 aqua ではチェックサムの検証ができます。 aqua では aqua-checksums.json でチェックサムを管理しており、バージョン更新時でもチェックサム検証をパスするためには、一緒にこのファイルのチェックサムも更新する必要があります。便利なことにそのための Reusable Workflow が公式に提供されているのでこれを使うことでチェックサムの更新も自動化することができます。 name: Update aqua-checksums.json automatically on: pull_request: paths: - .aqua/aqua.yaml - .aqua/aqua-checksums.json - .github/workflows/update-aqua-checksums.yaml jobs: update-aqua-checksums: uses: aquaproj/update-checksum-workflow/.github/workflows/update-checksum.yaml@3598c506108a2e0e9e31a0c6ef9c202c77049420 # v0.1.9 permissions: contents: read with: aqua_version: v2.21.0 prune: true secrets: gh_app_id: ${{ secrets.GH_APP_ID }} gh_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} このワークフローにおいても前述のコミットの問題が発生するので、GitHub Appを使う必要があります。このAppには contents:write 権限があれば十分です。 リリースフローに関する自動化 ブランチ管理 Coreチームでは git-flow を簡素化したブランチ管理を採用しています。main、develop、feature、hotfixブランチはそのままですが、releaseブランチは作成せず、リリースの際にdevelopブランチをmainブランチにマージしてリリースするようにしています。これらのブランチのうち、Protected Branchの設定しているのはmainとdevelopブランチになります。 オリジナルのgit-flow、releaseブランチに違いがある (出典: atlassian.com ) 定期的なリリースタイミングでdevelopをmainブランチにマージし、タグを作成すると本番環境にデプロイできるようになっています。 develop to mainのPull Request作成 リリース時にはdevelop to mainのPRが必要になるため、developブランチへPushがあると自動でmainブランチへのPRを作成するようにしています。 name: git-pr-release on: push: branches: - develop jobs: git-pr-release: runs-on: self-hosted permissions: contents: read pull-requests: write container: image: ruby:3.2@sha256:e3f503db7f451e6fd48221ecafbf1046ad195cddec98825538b35a82538b8387 steps: - name: Check out uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 # git-pr-release needs the git histories - name: Install git-pr-release run: gem install --no-document git-pr-release --version 2.2.0 - name: Update git config run: git config --global --add safe.directory "$(pwd)" - name: Create PR run: git-pr-release --squashed env: GIT_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }} GIT_PR_RELEASE_BRANCH_PRODUCTION: main GIT_PR_RELEASE_BRANCH_STAGING: develop GIT_PR_RELEASE_LABELS: Release GIT_PR_RELEASE_TEMPLATE: .github/PR_RELEASE_TEMPLATE.erb TZ: Asia/Tokyo PRの作成には git-pr-release を使っています。このワークフローにより次の画像のようなPRが自動で生成されるようになります。各PR毎にチェックボックスがつくので、リリース時にPR内容を確認してもらって問題なければチェックをいれるようにしてからリリースしています。 リリースの作成 mainブランチにマージした後はGitHub UI上からリリースをパブリッシュすることでタグを作成します。この際のリリース作成も自動化しています。 name: Release Drafter on: pull_request: branches: - main types: - closed jobs: release-draft: runs-on: self-hosted if: github.event.pull_request.merged permissions: contents: write pull-requests: write steps: - name: release drafter uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5.25.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} mainブランチへのPull Requestがマージされると、 release-drafter を使ってリリースをドラフト状態で作成します。これはかなり便利で、 release-drafter 導入前はマニュアルでリリースを作成していましたが次のような課題がありました。 バージョン番号を自分でインクリメントしないといけないのが地味に面倒 デフォルトブランチがdevelopになっているので、ターゲットブランチをmainに切り替え忘れるとインシデントになってしまう 自動化によりこれらは解消することができました。 Hotfixに関する自動化 hotfixブランチはdevelopブランチを経由せず、直接mainブランチにマージします。hotfixブランチのマージの際はpatchバージョンをインクリメントするようにしています。 name: Release Drafter Label on: pull_request: branches: - main types: - opened jobs: release-draft-label: runs-on: self-hosted if: github.event.pull_request.head.ref != 'develop' permissions: contents: read pull-requests: write steps: - name: detect version label uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # v1.1.3 with: labels: patch このワークフローによってhotfixブランチのPRが作成されると patch ラベルがつくようになっています。このラベルがつくと release-drafter がpatchバージョンをあげるように 設定して あります。 またhotfixの差分はdevelopブランチにもマージする必要があります。この作業は面倒は意外と面倒です。なぜかというと、直接mainからdevelopへのPRを作成することができないためです。またこの作業はよく忘れてしまうので自動化しておくのが得策です。それを実現するのが以下のワークフローです。hotfixがmainにマージされると起動します。 name: Create a pull request to merge hotfix into develop on: pull_request: branches: [main] types: [closed] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true defaults: run: shell: bash jobs: create-pull-request: runs-on: self-hosted if: github.event.pull_request.merged == true && github.head_ref != 'develop' permissions: {} steps: - name: Generate token id: generate_token uses: suzuki-shunsuke/github-token-action@350d7506222e3a0016491abe85b5c4dd475b67d1 # v0.2.1 with: github_app_id: ${{ secrets.GH_APP_ID }} github_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Decide a branch name id: decide-branch run: | branch=main-to-develop/hotfix-${{ github.event.pull_request.head.sha }} echo "branch=${branch}" >> "$GITHUB_OUTPUT" - name: Create a pull request uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.generate_token.outputs.token }} script: | const {owner, repo} = context.repo const mainBranch = "main" const devBranch = "develop" // fetch commit sha of develop branch const {data} = await github.rest.git.getRef({ owner, repo, ref: `heads/${devBranch}`, }) // create a new branch const branch = "${{ steps.decide-branch.outputs.branch }}" await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${branch}`, sha: data.object.sha, }) const {actor, payload} = context const {title, number} = payload.pull_request // merge main into a new branch await github.rest.repos.merge({ owner, repo, base: branch, head: mainBranch, commit_message: `Merge ${title}`, }) // create a pull request const pull = await github.rest.pulls.create({ owner, repo, base: devBranch, head: branch, title: `Merge hotfix to ${devBranch}: ${title}`, body: `Merge #${number} for ${devBranch} branch, too`, }) // assign an actor as reviewer github.rest.pulls.requestReviewers({ owner, repo, pull_number: pull.data.number, reviewers: [actor], }) github-script 用のScriptが長いですが次のことをやっています。 developブランチからPR用のブランチを作成 作成したブランチにmainブランチをマージ 上記ブランチからdevelopへのPRを作成 hotfixをマージしたアカウントをPRのレビュワーにアサイン このワークフローにおいてもコミットの問題が発生するので、GitHub Appからトークンを生成しています。このAppでは次の3つの権限が必要になります。 contents:write pull-requests:write workflows:write レビューワーも設定しているのでマージ忘れがないように工夫しています。 まとめ GitHub Actionsを使った自動化の事例を紹介してきました。セキュリティと自動化とは相反するところもあるので、両立するためにはバランス感覚と知識の更新が不可欠だなと思っています。だいぶ長い記事になってしまいましたが、そういった面で参考になれば幸いです。 明日の記事は poohさんです。引き続きお楽しみください。
アバター
こんにちは。Mercari USの検索エンジニアの @pakio です。 この記事は、 Mercari Advent Calendar 2023 の22日目の記事です。 Query Understandingは検索システム最も重要なシステムの一つで、検索意図を解釈し、また正しい検索を促すためのコンポーネントです。例えば検索ボックスでのクエリの提案やスペル修正、クエリの意図解釈、類似した検索条件の提案などシステム側・ユーザとの対話含めて様々な技術が用いられています。 Mercari USでは日々35万件以上の新しい商品が出品されています。それに比例して検索対象の商品も分増えていくため、お客さまの検索ニーズを正しく理解し、適切な商品を提案するためにもQuery Understandingが重要な課題と捉えています。今回はそんなQuery Understandingの中でもQuery Categorizationについての手法比較と、弊チームで実際に検証した結果についてご紹介します。 Query Categorizationの定義は様々あるかと思いますが、本記事の中では「特定の検索クエリから、お客さまが求めている検索結果がどの事前に定義されたタクソノミ(分類)に当てはまるか推測する」と定義します。 ルールベースのアプローチ ルールベースのアプローチは最もシンプルに実装ができ、かつ変更もしやすく説明可能性にも優れた手法です。 Algolia や Vespa など一部の検索エンジンではこの機能がデフォルトで提供されていることからも重要度が高いことがわかりますし、また実際に導入しているサービスも多いことでしょう。ここでは例として単純にカテゴリフィルタ条件を追加する変換を挙げていますが、実装方法によっては更に複雑な、例えばフィルタリングの代わりにスコアのブースティングを行ったり、複数の条件を追加するなども考えられます。 ルールベースのQuery Categorization その簡単さからとても魅力にも思える手法ですが、一方のデメリットとしてメンテナンス性が挙げられます。 もっとも単純なルールの生成方法として手動で辞書をメンテナンスする方法が考えられますが、確実な変換だけに対象を絞れる一方で入力の多様性に対応するためには莫大なメンテナンスコストがかかります。これについてはマスターデータからの生成などである程度自動化することは可能ですが、例えば同義語への対応や名称同士のコンフリクトなどイレギュラーなケースにはある程度人の手が必要となります。運用にあたってはその人的コストをあらかじめ織り込んでおかなければなりません。実際に弊チームでもこの辞書型のアプローチを数年ほど前から運用していますが、リスティングのトレンドの変化や新製品の対応などに伴う人手による定期的な見直しが必要とされている状況です。 機械学習的なアプローチ ルールベースからもう少し発展した手法として、クエリログやそれに付随するクリックログ、検索結果に表示されたドキュメントの統計情報を用いる方法などが提案されてきました。この手法はデータ量が膨大になりがちであるため、ルールベースなアプローチの代わり に機械学習的なアプローチと組み合わせて利用される事例を多く見かけます。 2018年末に公開されたLinらの論文 では、実際にECのプロダクト検索においてQuery Categorizationにクリックログを用いた手法が紹介されています。 ここでは約4000万件のクエリに対して、実際に検索結果に表示され行動(クリック/カートに追加/購入)が起こされたアイテムのカテゴリを取得し、クエリからカテゴリを予測するテキスト分類タスクとして学習を行わせています。 ここで使用されたカテゴリは階層構造になっているとのことですが、最も優れたモデルで1階層目の予測がmicro-F1スコア 0.78、最下層の予測が0.58程度とある程度高い精度で予測できていることがわかります。 TABLE I: Best micro-F1 score of multi-class single-label LR (logistic regression), SVMs, XGBoost, fastText and Attentional CNN classifier at different levels. – E-commerce Product Query Classification Using Implicit User’s Feedback from Clicks, Lin et al., Source: https://ieeexplore.ieee.org/document/8622008 条件・モデル構造は異なりますが弊チームでも同様にクエリログ及びクリックログを用い、クエリから商品の各カテゴリのクリックされやすさを予測するマルチクラス分類予測の学習をさせた機械学習モデルを作成しました。その結果、我々のテストデータではmicro-F1スコア 0.72となりました。 言語モデル的なアプローチ 上記の論文は2018年末に発表されたものでしたが、同じく2018年末に発表された言語モデル BERT が様々な分野で優れた性能を発揮しているのは皆さんご存知のことでしょう。BERTの特徴として、そのアーキテクチャにより上記で比較されていたACNNなどの従来のモデルと比較してもよりコンテキスト情報に強く、また様々な事前学習済みモデルが公開されている為手軽に試せることが挙げられます。また利用する事前学習済みモデルによっても異なりますが、自社のクエリログなどから学習したモデルと異なり一般的な語彙が用いられていることも特徴の一つです。これには未知のクエリに強い、汎用性があるなどのメリットもありますが、一方でドメイン固有の単語などには弱いといったデメリットも考えられます。 ここでQuery Categorizationのタスクに対して、このBERTの軽量派生モデルである DistilBERT を用いて弊チームにて実装した手法をご紹介します。 大まかなアーキテクチャとしては ①query embeddingsを取得するためのDistilBERT ②クエリ-カテゴリ分類器 で構成されています。 DistilBERTを用いたQuery Categorization 前段部分にあたるDistilBERTは事前学習済みのモデルから自社のデータでFine Tuningしたものを流用しており、今回の検証では後段の分類器のみを先述の機械学習的アプローチと同様にクエリログ及びクリックログから学習させた形になります。学習させたモデルのパフォーマンスは、我々のテストデータでの評価ではmicro-F1スコア 0.80となりました。 実際に本モデル及び前項に記載した機械学習モデルをオンラインテストで比較したところ、変換対象となったキーワードのカバレッジが本構成において2倍になっており、今後改善を行う上で汎用性の高い言語モデルであるBERTを用いるメリットが確認できました。 まとめ 本記事では弊チームで実装・検証を行ったQuery Categorizationに対しての複数アプローチについて紹介しました。特に最後のDistilBERTをベースとした手法に関しては、既存の言語モデルを流用することが可能で学習自体も1日未満で完了と、省エネながら確かな結果が得られる点が興味深かったです。当初の目的であった「お客さまの検索ニーズを正しく理解し、適切な商品を提案する」については、統計的有意差のある結果にはならなかったものの、検索結果上位のアイテムのCTRが増加したことが確認できました。より優れた検索体験を提供できるよう、更なる改善を今後も継続していきます。 検索エンジニアとして面白みを感じる分野である一方、今後ベクトルベースの検索がメジャーになったシーンにおいて既存のQuery Understanding技術がどう適用されるのか、進化していくのかがとても興味深いところです。 明日は@mtsukaさんが担当します。お楽しみに!
アバター
こんにちは。株式会社メルペイのSolutionsチームのデータエンジニアの @orfeon です。 この記事は、 Merpay Advent Calendar 2023 の22日目の記事です。 Solutionsチームは、社内向けの技術コンサルや技術研修、部門を跨いだ共通の問題を発見して解決するソリューションの提供などを行っています。 私は主に社内のデータ周りの課題を解決するソリューションを提供しており、一部の成果はOSSとして公開しています。 過去の記事 では全文検索OSSである Apache Solr を Cloud Run 上で利用して手軽に検索APIを構築する構成を紹介しました。 社内向けのソリューションの一つとして社内向けの検索APIを使ったサービスなど小規模な検索システムの構成に役立てています。 前回の記事の時点では、検索対象として搭載できるデータサイズなどにいくつかの制約がありました。 今回の記事では、構成をブラッシュアップすることで機能を追加したり、制約を一部克服できるようになりましたので、その実現方法と構成を紹介します。 はじめに 新しい構成を紹介するにあたって、まずは過去の記事で紹介したSolr検索サーバをCloud Runにデプロイする構成をおさらいします。 この構成を大雑把に説明すると、事前に作成した検索インデックスをSolrのコンテナイメージに直接同梱してそのままCloud Runにデプロイしてしまうというアイデアになります。 以下、定期的にデータソースからインデックスを生成して同梱したSolrコンテナをCloud Runにデプロイする構成図の例です。 大きく分けて、検索インデックスファイルを指定したデータソースから生成するバッチジョブと、Solrのコンテナイメージに完成した検索インデックスを追加したイメージを作成し、Cloud Runにデプロイする2つのステップから構成されます。 検索インデックスファイルの生成には Cloud Dataflow を、コンテナイメージの生成とCloud Runへのデプロイには Cloud Build を利用しています。 Cloud DataflowとCloud Buildを Cloud Scheduler から定期実行することで、指定したデータソースを元に検索インデックスをビルドし、Solr検索APIサーバとしてCloud Run上に自動的に反映される仕組みが構築できます。 Cloud Run上で動いているSolrサーバでの逐次的な検索インデックスの更新は行わない想定のため、同一で不変のインスタンスが負荷に応じてスケールするというとてもシンプルな構成になります。 一方で以下のような制約があります。 検索インデックスのサイズがコンテナイメージに載せられる量に制限される データの更新頻度はそれほど高くはできない(1日数回程度) 今回の記事ではこの構成をベースとして追加した新しい機能や、上に挙げた制約を回避するための構成として次の項目について紹介します。 複数コア検索対応 ベクトル検索インデックス構築支援 分散検索対応 複数コア検索 最初に紹介するのは複数コア検索対応です。 Apache Solrでは検索対象となるデータセットを コア という単位で管理しています。 コアは検索データセットのインデックス、スキーマ、設定情報を管理しており、RDBにおけるテーブルのような位置付けになります。 検索時に複数のコアを利用することで異なるデータセットを横断した検索を手軽にできるようになります。 例えばECマーケットで自分がお気に入りに登録したショップの商品だけ検索したい場合を考えます。 お気に入りのショップの数が少ない場合は、Solr APIを呼び出すアプリケーション側でお客さまのお気に入りショップを取得して、検索時のフィルタ条件に追加することで実現することもできます。 しかし、フィルタ条件を動的に組み立てる仕組みをアプリケーション側が管理しないといけません(お気に入りショップをDBから取得しORのフィルタ条件を組み立てるなど)。またショップの数が多いとリクエストサイズの制限に引っかかる可能性も出てきます。 そこで商品検索用のコア(Items)とは別に、お客さまのお気に入りショップ情報を管理するコア(FavoriteStores)を用意しておきます。 商品検索用のコアとお気に入りショップのコアを検索時にショップIDで結合することで、検索結果をお気に入りショップのみを対象に絞り込んだ上で該当するショップの取り扱っている商品だけを検索することができます。 Solrでは検索時にコア間の関係を正規化するためのクエリパーサーとして Join Query Parser が提供されています。 以下はJoin Query Parserを利用した検索リクエストの例です。 https://{solr url}/solr/Items/select?q=GCP&fq={!join from=ShopID fromIndex=FavoriteStores to=ShopID}UserID:0123456789 上記の検索リクエストは以下のようなSQLクエリと同等のものになります。 SELECT * FROM Items WHERE ShopID IN ( SELECT ShopID FROM FavoriteStores WHERE UserID = "0123456789" ) 過去に紹介した記事では単一のCloud Dataflowパイプラインでは単一のコアのインデックスのみ生成することができました。 そこでSolrのインデックスを生成するMercari Dataflow Templateの localsolr sinkモジュール を機能拡張して、複数のコアを一度に作成できるように対応しました。 これにより異なるデータセットを横断検索できるSolrサーバを手軽に構築できるようになりました。 以下はMercari Dataflow Templateで2つのBigQueryデータソースからそれぞれ対応する2つのコアの検索インデックスファイルを生成するsinkモジュールの設定の例になります。 コアごとに入力とスキーマ等の設定ファイルを指定します。 "sinks": [ { "name": "LocalSolr", "module": "localSolr", "inputs": ["BigQueryItems", "BigQueryFavoriteStores"], "parameters": { "output": "gs://${bucket}/output/index.zip", "cores": [ { "name": "Items", "input": "BigQueryItems", "schema": "gs://${xxx}/Items/schema.xml" }, { "name": "FavoriteStores", "input": "BigQueryFavoriteStores", "schema": "gs://${xxx}/FavoriteStores/schema.xml" } ] } } Mercari Dataflow Templateのlocalsolr sinkモジュールは生成したSolrのインデックスファイルをzipファイルとしてoutputで指定されたCloud Storageのパスに保存します。 複数のコアをコンテナイメージに同梱するDockerfileは以下のようになります。 インデックスファイルはコアごとにディレクトリが分かれています。 zipを解凍してコアごとのインデックスのディレクトリをSolrのデータディレクトリにそれぞれコピーします。 FROM solr:9.4.0 USER solr COPY --chown=solr:solr Items/ /var/solr/data/Items/ COPY --chown=solr:solr FavoriteStores/ /var/solr/data/FavoriteStores/ ENV SOLR_PORT=80 ベクトル検索インデックス構築支援 次に紹介するのはベクトル検索インデックスの構築支援についてです。 Solr 9.0から ベクトル検索がサポート されました。 ベクトル検索により検索キーワードが含まれているコンテンツだけでなく、検索キーワードに意味的に似ているコンテンツを検索することができるようになります。 しかし、コンテンツの内容を表すベクトルは検索インデックス構築時に自分で用意する必要があります。 テキストや画像などのコンテンツからベクトルを生成するには、自前のembeddingモデルを用意して推論したり、embedding用のAPIを利用するなどいくつか方法があります。 しかし検索インデックス構築パイプラインに案件ごとでこうしたコンテンツのベクトル化の処理を挟み込むのは少し面倒です。 そこで検索インデックスを生成するCloud Dataflowで、あらかじめデータをベクトル化するために作成した ONNXモデル を使って、入力データをベクトル化するための onnx transformモジュール を開発しました。 これにより、データ取得からベクトル化、検索インデックス構築を一筆書きのパイプラインで実現できるようになりました。 以下、Mercari Dataflow Templateで入力データの指定したフィールドをベクトル化するonnx transformモジュールの設定の例になります。 あらかじめ作成してGCSに保存しておいたONNXファイルをモデルとして指定して、入力データのフィールドやベクトル化出力とONNXモデルの入出力のマッピングを指定しています。 "transforms": [ { "name": "OnnxInference", "module": "onnx", "inputs": [ "BigQueryContentInput" ], "parameters": { "model": { "path": "gs://example-bucket/multilingual_v3.onnx", "outputSchemaFields": [ { "name": "outputs", "type": "float", "mode": "repeated" } ] }, "inferences": [ { "input": "BigQueryContentInput", "mappings": [ { "inputs": { "inputs": "Content" }, "outputs": { "outputs": "EmbeddingContent" } } ] } ] } } ] 検証では、TensorFlow Hubで公開されている universal-sentence-encoder-multilingual/v3 モデルをONNX化して、Solr検索インデックス構築時のテキストデータのベクトル化に利用しました。 2,000 程度の日本語のPDFファイル(250MB)のベクトル化を6vCPU程度のリソースコストで完了することができました。 現状ではONNX推論はCPU環境のみ対応ですが、今後はGPU環境対応なども検討していきたいと思っています。 ※ちなみにこの機能を追加した後に、 BigQueryの機能追加 によりSQLでテキストデータから手軽にベクトルを生成できるようになりました。 BigQueryではGoogleが構築済みのembeddingモデルをすぐに利用することができます。 手軽にベクトル検索を試してみたい方はまずこちらの機能を利用してみると良さそうです。 分散検索 最後に紹介するのは分散検索対応です。 過去の記事ではSolrのスタンドアロンモードでの起動を前提としていました。 スタンドアロンモードの通常の検索だと、検索インデックスは単一の検索ノード上に閉じるため、検索インデックスのサイズにはCloud Runインスタンスに載せられるだけという上限があります。 しかしSolrではスタンドアロンモードでも複数ノードにまたがった分散検索に対応しています。 そこでCloud RunでもSolr分散検索に対応した構成にすることで、単一のCloud Runインスタンスに載らない大規模なデータセットも検索できるようにしました。 Solrの分散検索 まず前提となるSolrのスタンドアロンモードでの 分散検索機能 を紹介します。 Solrでは1つの巨大なインデックスをシャードと呼ばれる小さなインデックスに分割して、複数のノードに分散配置することができます。 分散検索では、これらの複数のノードに分散配置されたシャードに対して一括検索することができます。 分散検索の実行には特別な設定は必要なく、検索対象としたいシャードを持つノードのエンドポイントを検索リクエストのshardsパラメータで指定することで実現します(複数ノード指定も可)。 分散検索では最初に検索リクエストを受け付けたノードが、shardsパラメータで指定されたノードに対して同じ検索リクエストを発行して検索結果を受け取り、マージして最終的な検索結果として返す仕組みになっています。 以下、3つのエンドポイントへの分散検索リクエストの例です。 https://{solrShardA}/solr/Items/select?q=GCP&shards=https://localhost:8983/solr/Items,https://{solrShardB}/solr/Items,https://{solrShardC}/solr/Items Cloud Runへの分散検索の適用 Solrの分散検索の仕組みをCloud Run上で実現する構成を考えます。 先に紹介した通り、Solrの分散検索ではシャードごとに異なるエンドポイントを持つ必要があります。 Cloud Runでは役割に応じてサービスという単位でエンドポイントを分けることができます。 そこでシャードごとにサービスを分割して、リクエスト時にこれらのシャードに対応するサービスのエンドポイントをshardsパラメータで指定することで分散検索を実現しました。 Cloud Runではサービスごとにノード数をスケールさせることができます。 そのためデータセットが不均衡で一部検索処理が重いシャードがあっても、そのサービスのノードだけ自動でスケールさせることができます。 Solr分散検索をCloud Run上で運用するに当たって、検索インデックスの生成ステップでは追加の開発は特に必要ありません。 Cloud Runのサービスをシャードごとにデプロイするようにするだけです。 そのためにCloud Dataflowによるインデックスの生成をシャードごとに生成するように変更します。 Cloud BuildによるCloud Runへのデプロイもシャードごとにサービスを分けてデプロイするようにします。 Solrの分散検索の注意点ですが、検索結果のX件目から10件取得するといったオフセットを指定して取得する場合、オフセットに比例して消費メモリや処理が重くなることが挙げられます。 これは複数ノードから取得した検索結果を一箇所に集めてソートするために起こります。 分散検索はなるべくこうした問題が顕在化しない、トップX件のみ利用するようなケースに適用するのが望ましいでしょう。 別の注意点としては、検索リクエストを送る側や各サービスはシャードごとのエンドポイントを把握しておく必要があることが挙げられます。 データセットを分割するシャードが変わらない場合は問題にならないのですが、頻繁にシャードが追加されたり変更されるような場合は、シャードと紐づくエンドポイントの情報をサービスやアプリケーション間で共有するための工夫が必要になります。 おわりに 今回の記事では、Cloud Run上で手軽に検索APIを構築するための構成について、前回の記事から新しく追加した機能や構成を紹介しました。 過去の他の記事 でもCloud RunでNeo4jを動かす構成を紹介しました。 Cloud Runはフルマネージドなサービスであり、比較的小規模なデータを扱うAPI手軽に構築するにはとても便利なサービスだと思っています。 今後もCloud Runなどを通じて様々なデータを手軽に扱う仕組みを検証して、社内のデータ活用に役立てていきたいと思います。 今回紹介したSolrの検索インデックスの生成に用いた Mercari Dataflow Template はOSSとして公開しており、技術書典の全文検索にも活用されています。 もしCloud RunでSolrを使った検索APIを手軽に構築してみたい方はぜひお試しもらえればと思います。 また今回の記事に向けて、データの更新頻度をニアリアルタイムに近づけるための仕組みも検証中だったのですが、残念ながら間に合いませんでした。 次回の記事でニアリアルタイム検索の仕組みについても紹介できればと思います。 明日の記事は @iwata さんによるGitHub Actionsを使った自動化です。引き続きお楽しみください。
アバター
はじめに こんにちは。メルカリ Director of Engineering の @motokiee です。この記事は、 Mercari Advent Calendar 2023 の21日目の記事です。 メルカリのサービス開始から10周年ということで、2023年9月に iOSDC Japan 2023 カンファレンスで「メルカリ10年間のiOS開発の歩み」について 発表を行いました 。 この発表は、10年間のiOS開発の歴史を40分のトークにまとめたものです。メルカリはこの10年多くの技術的なチャレンジをして断続的にアプリケーションをアップデートしてきました。自分が見てきた歴史と、見ていない歴史については git log を手繰りながら調査した集大成となっています。 サービスの歴史が長くなると、アプリケーションのリファクタリングはもちろん、作り直す話も出てくると思いますが、そういった意思決定の際の参考になればと思い作成しています。 なお 発表のアーカイブ動画 もありますが、動画を見るのも以外と腰が重かったりするため、文章のほうが自分の都合で見やすく、良い選択である場面もあると思います。また、テキストの方がChatGPTなどLLMでサマリを作るコストも低くなりタイパ(タイムパフォーマンス)重視の方には良いのではないかと思い、トークスクリプトを公開してみると良いのではないか、と考えました。 ぜひご覧ください。 トークスクリプト全文 よろしくお願いします。「メルカリ10年間のiOS開発の歩み」というタイトルで発表します。 自己紹介です。motokieeといいます。 現在は株式会社メルカリで Director of Engineering をしています。メルカリには2016年に入社し、丸7年が経過しました。 メルカリでは、メルカリ本体や新規事業にエンジニアやエンジニアリングマネジャーとして携わってきました。 現在はMobile, Web, Backend の アーキテクトチームをDirector of Engineeringとして管轄しています。ちなみに現在はiOSの開発はしていません。なのでお手柔らかにお願いします。 iOSDCは2016年から2019年までコアスタッフをしていました。スピーカーとしての参加も久しぶりなのでとても緊張しています。よろしくお願いします。 まずはこのトークでオーディエンスのみなさんが得られるものについて簡単にご紹介します。 メルカリはこの度10周年を迎えることができました。 これもひとえに使っていただいたお客さまのおかげではありますが、この10年間でどのように会社、サービス、そしてiOS関連技術が変化してきたかをご紹介します。 また10年間のメルカリアーキテクチャやTech Stackの変遷についてもご紹介します。これまでの10年に負けないくらいの変化が今後も起こるはずだと考えており、エンジニアとしてこれからの変化にどう対応していくかのヒントが得られるのではないかと思います。 そして最後に、昔からiOS開発をしている方々には温故知新、少し懐かしい気持ちになってもらえるのではないかと思います。 最近iOS開発を始めた方々には、昔の出来事を振り返って、自分たちがこれから取り組むかもしれない開発へのヒントにしていただければ幸いです。 それではトークに移りますが、メルカリについて簡単にご紹介させてください。 私達はミッションとバリューをとても大切にしています。 まずミッションですが、今年10年を迎えミッションが「あらゆる価値を循環させ、あらゆる人の可能性を広げる」にアップデートされました。 そしてバリューです。Go Bold, All for One, Be a Proです。 日本語に訳すと、大胆にやろう、全ては成功のために、プロフェッショナルであれ、をValueとして掲げています。 続いてはサービスについて、特にフリマ事業がサービス開始から10年でどのような立ち位置にいるか簡単にご紹介します。 2023年7月時点で、メルカリの月間利用者数は2200万人以上となっています。累計でメルカリに出品された商品は30億品以上、さらに 2022年の取引件数を1年間の秒数で割ったところ、1秒間に7.9個売れていることがわかりました。 サービス開始当初の2013年は20-40代の方を中心に使われていましたが、現在ではシニア層の方も含め幅広くバランスよくご利用されています。 続いて取扱いカテゴリですが、2014年にはレディースファッションカテゴリが最もシェアが大きかったのですが、現在は、本・ゲーム・おもちゃといったインドア向けアイテムがトップシェアを占めています。 メルカリはアメリカでも事業を展開していますが、日本のフリマから国境を超えて取引が展開されています。代理購入サービスで海外のお客さまでもメルカリの商品を購入できるという取り組みが行われており、世界110か国以上の国・地域のお客さまに「メルカリ」でのお買い物をお楽しみいただけるようになっています。 以上、簡単なメルカリのフリマサービスについてのご紹介でした。 続いて今日のトークの全体像についてご紹介します。 今回、10年分の歴史を振り返るにあたって独自に年表を作成しました。使用されていた技術、重要なプロジェクト、その時々のスクリーンショットを集めて参考資料として作成しました。 少し文字が小さいですが、ざっくりと流れをご紹介します。 1年ごとの取り組みを分析してみてタイトルを付けてみました。2013年から2015年は Build 期 だったと言えそうです。 このころはフリマサービスに必要な機能を次々と実装していた期間でもありますが、同時に新しい事業・技術ともに新しい領域への探索がスタートした時期でもありました。立ち上げ期、Buildにフォーカスした時期だったのかなと思います。 続いて2015年から2017年あたりは、Explore, 探索期ですね。次々と新規事業が生まれていった時期だったと思います。 Swiftはもちろん、Reactive Programming の導入も始まっていて、新しい技術の探索を始めた時期だったのかなと思います。 2018年から数年は Re-architecture and Foundation 期です。 2018年には Re-architecture が始まり、2019年頃から Design System, Weekly Release, ログの改善など、アプリ開発周辺基盤の強化に力を入れていた期間でもありました。この間、開発基盤のために事業を止めていたわけではなく、スマホ決済のメルペイもサービスローンチされたりしています。 そして2020年-2022年はRewrite期です。 日本のメルカリアプリをRewriteする取り組みの期間でしたし、US、新規事業でもフルスクラッチで開発を行っていました。 2023年現在、いまは Post Rewrite と呼べる時期で、また新しいことに取り組んでいたりします。 以上が10年をフェーズに分けた全体像となりますが、ここから各年掘り下げてご紹介していきたいと思います。 まずは2013年です。この年はメルカリが誕生した年です。最初にiOS周辺技術での出来事を簡単に振り返りましょう。 2013年はiOS7が発表された年です。いわゆるスキューモフィズムからフラットデザインへの大きな変更が行われた年と言っても良いでしょう。 iPhone 5s、iPhone 5c が発売リリースといった出来事がありました。 メルカリのサービスとしては、2013年7/2にAndroid版が、少し遅れて2013年7/23にiPhone版の提供がスタートしたようです。 iPhone版はわずか半年後に 「App Store Best of 2013 今年のアプリ」を受賞していて、急速にサービスが伸びていったことがうかがえます。 ちなみにメルカリで最初に売れた商品は「ドット柄のカットソー」みたいです。 サービス開始当初はどんなUIだったかというと… こちらは当時のプレスリリースに掲載されていた画像です。ロゴやUIに時代を感じますね。 また、 App Store Connect API を使ってApp Storeに設定されたスクショを全て取得しています。こちらは2013年7月にリリースされた際、App Store に設定されていたスクリーンショットです。2013年って感じですね。 続いて メルカリのiOSリポジトリの git log から2013年がどんな年だったか見てみましょう。 主要なデータとして、コミッター数、コミット数、そしてdiffを1年ごとに集計しています。コミッター数は重複を含むため、正確な数字ではありませんが、スタートアップらしい少人数体制で開発をしていた時期です。 そしてこちらが記念すべき最初のコミットログです。タイムゾーンがなぜか(アメリカ・カナダ)の山岳部標準時 – MSTになっているのですが、JSTでは2013年03月15日(金) 21:17でした。ちなみに調べたところ大安でした。 また、メルカリiPhone版が提供されたのは7/23のv1.0.1ですが、それより以前に App Store で v1.0.0が審査を通過しています。メルカリ最初のiOSエンジニアのoobaさんに背景を伺ったところ、当時 App Store の審査に 2週間から1ヶ月かかることもあったため、rejectされないかの確認のためのサブミットを行った、とのことでした。 先程のv1.0.1の配信開始が2013年7/18、その後7/23にプレスリリースを出しています。2013年3月の最初のコミットから約4ヶ月の開発期間を経てのリリースでした。 ちなみに7.23のリリース初日はわずか2000ダウンロードでした。これが2013年末までの半年弱で100万ダウンロードを突破することになるので、すごいスピードだと思います。 2013年の技術トピックをまとめてみました。 このころは Objective-C かつ MVC でアプリケーションが書かれていました。なぜならSwiftは2014年発表だからですね。ちなみに iOS4~iOS7がサポートバージョンとなっていました。 メルカリの商品リストは CollectionView で実装されましたが、UICollectionViewは iOS6で登場したAPIだったため、それ以前のバージョンにはPSTCollectionViewというOSSが使われていました。昔からiOS開発をしている皆さんにはおなじみではないでしょうか。 また、AFNetworking, SVProgressHUD などお馴染みのライブラリに加え、まだ Apple に買収される前のTestflight SDK も利用されていました。 あとはスキューモフィズムデザインですね。本物の物質に寄せてディテールを細かく施すデザインですかね。メルカリiOSの最初のPull Request をチェックしてみたら、こんな感じで立体感のあるデザインになっていました。 この点もoobaさんに伺ったところ、iOS7が発表されて「やばい」となって急いでフラットデザイン対応をされたとのことでした。 また最初期はWebViewベースのガワアプリを検討していたようですが、結果的に体験を重視してネイティブアプリの開発に切り替えています。 WebViewベースで押し切った時にメルカリがサービスとしてどうなったのか知るすべはありませんが、とても気になりますね。 続いて2014年です。この年、初のTV CM放映がされました。2013年末までに100万ダウンロードを突破していましたが、さらに加速的にサービスが成長していきます。 先に2014年のiOS周辺技術の出来事を見てみましょう。 この年Swiftが発表されます。iOSアプリの開発に携わる皆さんにとってエポックメイキングな出来事だったと思います。メルカリも例外ではなく、この後数年、Swiftを軸に様々な技術的な取り組みが続くことになります。 またiPhone6, iPhone6 Plusが発売された年でした。フォームファクターが増えたことは大きな出来事ですが、@3x 画像の登場で画像アセットの更新が大変だったり、AutoLayoutに対応せず 3.5inch と 4inch 画面で分岐するようなコードを書いていた方には思い出深いできごとではないでしょうか。僕もたくさんの画面のAutoLayout対応を行った覚えがあります。 サービス、会社として2014年の大きな出来事はこちらです。なんと500万ダウンロードを突破します。またUSでのサービススタートなど2年目にしてかなりの打ち手がありました。 App Store のスクショはこんな感じになりました。 まずGit log を見てみましょう。 そこまで大きな変化はないですね。コミッター数としては増えていますが、ユニーク数ではないので実際にエンジニアが増えたどうかは分かりません。 このスクショはv3にメジャーアップされた後のものです。v3へのメジャーバージョンアップはデザインリニューアルを主な理由としています。 2014年7月にリリースされた v3系 は2019年にメルペイがリリースされるまで5年弱続くことになるとても長寿なバージョンとなりました。 引き続きObjective-Cが使われていました。2014年はSwiftが発表された年ですが、この年のコミットにSwiftのコードは入っていませんでした。 一方この年、メルカリでReactiveCocoaがライブラリとして取り入れられ、一部の画面がMVVMで実装され始めていました。また cocoapods が package manager として取り入れられていました。 また、USのサービス開始に伴い、日本とアメリカのサービスでソースコードが共有されるようになったことも大きな出来事です。コードは共有しながら、国ごとにターゲットを分けて別バイナリを配布するアプローチを取っていました。 以上が2014年です。サービスとしてはかなり伸びていましたが、まだまだ技術を見直すようなタイミングにはなっていません。 そして2015年です。 この年は commit log や チケットなどをたどるとフリマサービスとしての基礎体験の磨き込みに力を入れていた時期だったように思います。 iOS周辺技術としてはこんな感じです。 サービスとしては2015年1月に1000万ダウンロードを突破します。機能的には、「らくらくメルカリ便」という便利な配送方法の提供を開始した時期でもあります。 また、2015年後半には新規事業を手掛ける子会社ソウゾウが設立されました。 App Store のスクショはあまり変化がないですね Git log もそこまで大きな変化はありませんが、コミッターが増えています。 この頃からiOSの勉強会に行くと、メルカリで働いているという人を見かけるようになった覚えがあります。 技術トピックとしては、この年からSwiftが実戦投入され始めます。新しい画面や Extension がSwiftで実装され始めています。 Git logをたどると、機能開発ですごく忙しかったような印象を受けましたが、チケットのタイトルを見てもUXを向上させるような施策に集中して数多く実装していた時期だったようです。 また新規事業でフルSwift, RxSwiftでの開発が始まり、新しい技術の探索が始まったタイミングとも言えるのではないかと思います。 以上が2015年のできごとです。 このあと数年メルカリの規模に合わせた開発を模索していくことになるのですが、振り返ってみるとその礎がこの2015年あたりに築かれたような気がしています。 続いて2016年です。この年はUSへのフォーカスと、メルカリ初の新規事業がローンチした年でもあります。 そんな2016年はiOS10, iPhone 7 が発表されました。ジェットブラックありましたね。 そして、 第一回 iOSDCである iOSDC Japan 2016 が開催された年でもあります。ちなみに第一回は早稲田キャンパスではなく、練馬のココネリホールでの開催だったんですね。 僕も当時スタッフとして関わっていたのですが、「みんな来てくれるかな〜」「スポンサーさん集まるのかな〜」 「まぁでも、誰も来てくれなかったら会場費用自腹でもくもく会をやればいいだけだしね」と度々主催者の長谷川さんが言っていました。 ちなみにそんなiOSDCをメルカリは第一回はもちろん、かれこれもう8年連続でスポンサーとして応援しております! というわけで本題に戻ります。 2016年は匿名配送の提供開始、あとはアメリカのApp StoreでUS版メルカリがTop3にランクインするという出来事もありました。 それからメルカリアッテというクラシファイドサービスのリリースですね。こちらのサービスはすでにクローズしております。 スクショはこんな感じです。ちょっとだけ変わりました。 git log はこんな感じです。なお新規事業のリポジトリは含んでいません。 Diff がかなり多いのですが、ちょっとなぜこんなに多いのかまでは追いきれませんでした。 この年から、メルカリ本体でも新規事業でも リアクティブライブラリを使っての開発が行われるようになり、リアクティブライブラリの知見が社内に溜まっていくことになります。 メルカリ本体はObjective-CとSwiftの併用、新規事業がこのあと続々立ち上がっていくのですが、そちらはフルSwift + RxSwift での開発となっていきました。また Carthage がこの年導入されていました。 以上が2016年のできごとでした。 続いて2017年ですが、この年は新規事業がたくさん立ち上がります。 iOS周辺技術の出来事としては、iPhone Xが登場します。ノッチの登場ですね。 サービスとしてはAI出品機能、「ゆうゆうメルカリ便」が提供開始となり、さらにアプリは世界1億ダウンロードを突破します。 この年はニュースが多くて、USメルカリアプリが書き直されてリニューアルされます。 またイギリスでもサービスがスタート、他にも新サービス・新機能が続々とリリースされますがこれはすでにクローズされています。後半にはメルペイが設立され、数年後のスマホ決済サービスの準備がスタートします。 スクショはこんな感じです。あんまり変わらないですね。 Git log はというと、増えてはいますが、これもそこまで変わりません。 この年はUSアプリを書き直す・リライトする “Double” というプロジェクトがUSメルカリアプリで行われました。Swiftで書き直されたのですが、ネイティブのコードに加えて React Native も導入されていました。 また、2015年に立ち上がったメルカリ アッテの設計をベースとして、Swift/RxSwift/MVVMでいくつも姉妹アプリが立ち上がりました。 メルカリ本体のメルカリNow、メルカリチャンネルのような新機能もSwiftがメインで開発されるようになりました。 このころから少しずつ技術的な課題が出てきます。 Objective-C と Swift だったり、新しい画面と古い画面が混在するようになってきたため、コンテキストスイッチのコストが高くなってて少しずつ課題となってきていました。また、事業として重要なコンポーネントや画面のメンテナンスがかなり困難になってきていました。 エンジニアの人数も順調に増えていたので、複数人が同じ画面に改修を入れるケースも増えていき、結果コンフリクトが発生しやすくなり、他の人の作業に自分の作業がブロックされるようなことも増えていき、結果として開発の速度が上がりづらくなっていました。 そして2018年、ここから技術基盤を強化するプロジェクトがいくつも走っていくことになります。その最初のプロジェクトが Re-Architecture でした。 2018年のiOS周辺技術の主な出来事としては、iOS12, iPhone Xsの発売ですかね。 サービスとしてはシェアサイクルサービスであるメルチャリがリリースします。こちらは現在事業譲渡済みです。 また6月にマザーズ上場、メルカリロゴのリニューアル、2016年から2017年で立ち上げたサービスが2018年の間にいくつもクローズされました。 そして日本のメルカリチームでも海外からの採用が加速してきます。僕もこのころから仕事で英語を使う機会がかなり多くなりました。 Git log は激変しました。2017年は6000台だったコミット数が3倍強に増えています。 コミッター数も90を超えましたが、これはユニークではないため数十人いた、くらいに捉えていただければ良いと思います。 そしてロゴはこのようにリニューアルされました。2013年当初から続いていた箱が開くデザインから変更されました。このロゴは現在も使われています。 こちらは2018年7月ごろ、ロゴが変わる前のApp Storeのスクショです。ロゴが変わった後の2018年10月のものをみてみましょう。 ちょーーーっと変わりました。翌年に大きなサービスローンチを控えていたため、この段階で大幅なデザインのアップデートまでは行いませんでした。文字ロゴが変わっただけでそこまで大きな変化はないですね。 この年の大きな技術トピックは Re-architecture です。 方針としてはアプリのフル書き換えは選択せず、王道の少しずつ画面を書き換えていくアプローチをとりました。MicroViewController と読んでいたのですが、コンポーネントベースで同時並行での開発を可能にするアーキテクチャへのアップデートでした。 複雑な画面の書き換えを目的とし、テストや仕様書を充実させながらプロジェクト進行させていきました。 このときのアーキテクチャについては、 2018年にtarunonさんがiOSDCで発表を行っています 。ご興味のある方はぜひご参照ください。 Re-architecture のロールアウトプランについてもご紹介します。 書き換えを行う際、どのようにロールアウトしていくかは判断の難しい問題だと思います。 我々のアプローチは全く同じ画面を実装し、新旧でA/Bテストを行いながらKPIに劣後が出ないかを確認しながらロールアウトしていきました。 全く同じ画面だったので、細かすぎる微妙な仕様の差を知っていないと自分の端末にどちらが表示されているのか本当に分かりませんでした。 また、Feature Flag で新旧画面の比率を調整しながら徐々に公開していきクラッシュやエラーを監視しました。 クラッシュ等以外のビジネス指標は、BIチームとも連携してトラッキング、KPIに異常が出たらすぐにFeature Flagで旧画面に切り戻すという運用を行いました。 Re-architecture は全体として良い結果をもたらしました。主要画面の書き換えを完了できたことはもちろん、一部の画面では旧画面よりもパフォーマンスが向上し、事業KPIに良い影響を与えたことも分かりました。 赤いドットがRe-architecture 後の画面、青いドットが旧画面のある指標です。なにが良かったかは公開できませんが、パフォーマンス向上によってビジネス上の指標に良い影響があったとご理解いただければよいかと思います。 取り組みとしては結果的に1年を掛けてターゲットとしていたすべての画面の書き換えが完了することができました。また、テストも Re-architecture 前に比べてかなり充実しました。 特にロジックを含むようなコンポーネントは80%のカバレッジを持つようOKR(Objectives and Key Results)を設定して達成していきました。 残念な点としては、仕様書については継続的にアップデートが行われず、数年後に行われるリライトプロジェクトでも課題となりました。 エンジニア観点で一番大きな効果はスケーラブルな開発体制を構築できたことではないでしょうか。エンジニアの人数も増えたのですが、並行して開発ができるようになったこともあり、コミット数が前年比3倍に増えています。 Re-architectureで画面を書き換えていったことも大きいと思いますが、コードの追加・削除もかなり増えています。 GitHubのContributersのグラフを見ても、Re-architectureを前後でトレンドが大きく変わっていることが分かります。 また、この年から全員プロダクト開発を行うエンジニア、という体制に変化が訪れます。 横断的な改善の重要度が上がり、 iOS Coreチームが組成されます。 2023年現在、このCoreチームは iOS Architect チームとして継続しています。 この年はバックエンドでも大きな変化がありました。PHPのモノリスアプリケーションからマイクロサービスアーキテクチャへの移行を目指すMicroservice Migrationがスタートしています。バックエンドでgRPCが使われ始めたこともあり、クライアントでは Protocol Buffersが利用され始めました。 2018年は技術的な取り組みとしてはRe-architectureという大きな動きがありました。会社全体としても技術刷新に取り組む環境へと大きく変わった年でもありましたが、2019年も大きな変化が起こることになります。 それがメルペイというスマホ決済事業のスタートです。 メルペイは2019年2月にスタートしました。タイムラインとしては、2018年にはかなり本格的に開発が行われていていました。 iOS周辺技術においては、SwiftUIが発表され、これもエンジニアリングとして後に重要な出来事となります。 こちらがメルペイリリース時のApp Storeのスクリーンショットです。 これまでフリマアプリがメインでしたが、スマホ決済機能を強く打ち出しています。 2018年はRe-architectureが進行していましたが、メルペイはどのように開発を進めていたのでしょうか? メルカリ社内ではRe-architectureと同時進行で “Merpay Integration” というスマホ決済機能をメルカリのアプリに取り込むプロジェクトが2018年頃から進行していました。 Re-architecture への影響を考慮し、 Merpay 機能を SDK としてモジュール化して提供する手法を選択し、Re-architectureもメルペイの開発も止まらないようプロジェクトが進行されました。 またアプリ上のUIの大きな変化として、メルペイスタートと同時に、メルカリアプリは下タブUIへと変更されています。 2013年のリリース当初からハンバーガーメニューのUIが続いていましたが、メルペイリリースとともに現在も続く下タブへのアップデートが行われました。 Re-architeture後も開発はさらに加速してきました。 なお Merpay は別リポジトリで管理されていたので、メルカリグループ全体としてはさらに大きい数字になっていたと思います。 主要画面以外のRe-architectureも完了、さらに 下タブ化をともなう Merpay Integration が終了し、Re-architectureは約1年で一区切りとなりました。 Re-architecture によって大部分が書き換えられました。Re-architecture前の2018年と2019年末を比較すると、プロジェクト内の Swift 比率は約20%から約85%にまで高まりました。 Objective-Cは75%から15%に減少していますが、それでもObjective-Cはプロジェクト内に残っていました。 また、Re-architecture をベースとして Design System プロジェクトがスタートしました。 Design System を進めた理由としては、スケーラブルな開発の実現と一貫したデザインと体験の両立と、そのためのPM/Designer/SWEの共通言語の導入、の必要性があがっていったためです。 この年は、Re-architecture 済みの画面に対して Design System コンポーネントを全社で適用していきました。 以上が2019年のできごとでした。 2018年以降の流れとして、スケーラブルな開発の重要性が上がった、ということが上げられます。採用も日本だけではなく海外にも目を向け、より広い市場にアプローチしていくことになりました。 一方でスケールする開発を実現するためのアプリ開発基盤のアップデートが重視された期間であり、この流れはいまに至るまで続くことになります。 2020年。Re-architectureが一旦の終わりを迎え、Design System などアプリ開発基盤の強化に力を入れ始めたタイミングで、GroundUpというプロジェクトが始動します。これは何かというと、アプリをゼロから書き直すプロジェクト です。 2020年はiOS14, iPhone12等、あとはApple Silicon が発表された年です。 2020年7月 App Storeのスクショがこちらです。2019年に引き続きメルペイを前面に据えています。 引き続きかなりたくさんのコミットが行われていました。 2020年の技術トピックとしては、先程も触れた通りアプリ開発周辺基盤の強化が挙げられます。 2019年にスタートした Design System に続いて、 Test Automation強化、Weekly Release の検討開始、 Client Event Logging の刷新などがプロジェクト化され、投資が行われました。 これらを進める理由として、エンジニアを取り巻く環境が変わったことも挙げられます。 サービスとしてはシングルアプリですが、メルカリとメルペイは別の会社になっています。スマホ決済事業が導入されたことにより、結果として、メルカリ・メルペイのグループ会社をまたぐ活動が増えました。 両者で求められるガバナンスも異なるのですが、足並みをそろえ、機動力を維持しながら開発する体制が求められていました。 そのような動きもありますが、2020年はGroundUp App の始動が最も大きな出来事であったと言えるでしょう。 リーアキテクチャのようなリファクタリングを行うアプローチではなく、アプリをゼロから書き直し、”式年遷宮”を行う意思決定でした。 2019年に発表されたSwiftUIで書き直すことが方針として設定されました。 また、Re-architectureを選択しなかった理由として、今後数年、プラットフォームの提供する新機能に素早く対応していけるようにベースから書き直す判断をしました。 このプロジェクトは、プロダクト開発を行うチームから独立して開発がスタートしました。 また GroundUp では Bazel をビルドツールとして採用し、 Bazel のビルドキャッシュなどの強みを生かした Micro Modular Architecture を採用しています。 この Micro Modular Architecture については、 いまも iOS Lead Architect を務める Aoyama さんが iOSDC Japan 2020 で発表を行っている ので、興味があればそちらをご参照ください。 さて、git log をこの年から2種類見ていくことにしましょう。 これまで見てきた初代iOSリポジトリはレガシーリポジトリと呼んでみましょう。引き続きすごい数のコミットが行われています。 こちらは Ground Up リポジトリです。まだまだ産声を挙げたばかりのプロジェクトと言えそうですが、コミッターはそれなりにいたように見えます。 以上が2020年のできごとでした。 俯瞰してみると、Re-architecture が終わった後すぐに Rewrite プロジェクトが開始されており、とても決断が早かったように感じます。やはり、2019年に発表されたSwiftUIはメルカリのiOS開発においては大きな転換点だったと言えます。 さて、2021年は再チャレンジが行われた年と言えるかもしれません。 まず iOS周辺技術では、iOS15などが発表されました。 2021年7月のスクリーンショットはこちらです。フリマ機能が再度押し出されています。 レガシーリポジトリは少しコミット数が落ち着いてきます。前年3万近くあったコミットから1万6千にまで減少しています。 一方、GroundUpはコミット数こそあまり変化がありませんが、コミッター数が増えているように見えます。 この年からメルカリは アプリのリリース周期を2週に一回から毎週アップデートに頻度を上げる改善を行いました。 Delivery の頻度を増やすということが目的だったのですが、これを実現するためにはいろいろなものを整備する必要がありました。 約半年ほど掛けてプロセスやオペレーションのアップデート、QA期間短縮のための自動化などの準備を行い実現されました。 サービス的には事業者向けのメルカリShopsが立ち上がりました。 メルカリShopsは、クライアントアプリだけでなくバックエンドもフルスクラッチで開発しました。この機能はネイティブではなくWebViewでメルカリアプリ内に提供されています。 WebViewへのチャレンジは2013年にWebViewベースでの開発を諦めてからの再チャレンジとも言えるものでした。 メルカリUSでは、2017年のDouble以来、2度目の書き直しプロジェクトである Denali がスタートします。 以前の Double プロジェクトでは部分的に採用していた React Native をフルで使って書き直すプロジェクトです。プロジェクト名のDenaliは、北アメリカ大陸の最高峰の山の名前らしいです。 以上が2021年の出来事です。 振り返ってみると、USメルカリ、日本のメルカリ、メルカリShops という3つのプロジェクトがフルスクラッチで開発を行っていたことになります。 そして2022年はエンジニアリングとしても会社としてもGroundUpにフォーカスした年となりました。 この年はiOS16, iPhone14が発表されました。PassKey についてもこの年WWDCで発表が行われました。 2022年は メルカリIndia が設立されたり、メルカードの提供を開始したりと、組織、サービスとしてもさらなる広がりを持った年になりました。 そしてメルカリアプリのリプレースの完了です。 こちらが GroundUp リリース前の 最後のv4系、4.106.0 のスクリーンショットです。GroundUp でリプレイスされた v5系を見てみましょう。 はい、何も変わってません。でも裏側は全部変わっているんですね。 Git log チェックしましょう。 2022年途中でレガシーリポジトリにはコードフリーズが入ったため、コミット数が16,000から十分の1以下に減っています。 Ground Up は逆に約2000から倍以上に増えています。 GroundUpのリリースについてご紹介します。 先程説明した通り、Legacy リポジトリにコードフリーズを実施しました。これまで Legacy で機能開発に取り組んでいたエンジニアも全員が GroundUp の開発に移りリリースを目指しました。会社としては GroundUp を前提に取り組んでいたサービスもあったため、全社を挙げての取り組みとなりました。 ロールアウトプランですが、Re-architecture のときのように画面ごとにロールアウトしていくという戦略は取れません。 4月からTestflight で外部テスターを募り、βテストを実施し、ここでバグリポートを集め修正を行っていきました。 その後、 7月にApp Store で実際にGroundUpアプリをリリースするフェーズに移ります。ここでは Weekly Release は維持しつつ、v4系のレガシーアプリのストアリリースを停止、v5系のGroundUpを実際にストアにリリースしていきます。 この際、段階リリースを行い 1%, 2% など小さいパーセンテージでリリースを停止し、バージョン浸透率をコントロールしながら徐々にロールアウトを実行していきました。もちろん、この段階ではKPIの監視も行いました。 これを1ヶ月ほど続け、9.20に v5系GroundUpアプリを100%公開し、2020年にスタートしたリライトプロジェクトであるGroundUpが2年をかけて完了しました。 このリライトプロジェクトにより、Objective-Cはメルカリのアプリから完全になくなりました さらにこれまで別リポジトリで管理されていた Merpay SDK などを、 GroundUpリポジトリに統合する モノレポ化が実施されました。 また、USアプリの React Native への書き換えも4月に完了しています。 GroundUpプロジェクトについては、 CTO や Lead Architect のインタビュー記事が出ていますので、興味があればご参照ください。 以上が2022年のできごとでした。 日本とUSどちらもリライトプロジェクトが完了したという年で、モバイルアプリに関わるチームにとってはハードな1年となりました。 しかし、書き換えて終わりというわけではありません。 そして、2023年、メルカリは10周年を迎えました。 今年はiOS17が発表されましたね。 そして Apple Vision Pro, visionOSも発表されました。 メルカリはすでに次の10年に向けて動き出しています。 ビットコインが買えるようになりました。 また、パスキーの対応も開始しています。ChatGPTプラグインの提供も開始などなど、 Go Bold にチャレンジを続けていきます。 そして、7月にアプリローンチ10年を迎えました。GroundUp が終わったあと、レガシーリポジトリはどうなったのか見てみましょう。こちらです。 はい、全て0です。 GroundUp でのコードフリーズ以降、レガシーアプリでの開発がストップしたため、2023年には誰もコミットを行っていません。 レガシーアプリであるv4系アプリはサポートが続いていたため、メンテナンスのためにリポジトリは残されていました。 しかし、2023年に入り、v4系アプリのサポートを切る強制アップデートが実施されました。これにより、 レガシーiOSリポジトリは役目を終え、アーカイブされることになりました。 2013年3月の Initial Commit から10年を経てその歴史に幕を降ろしました。 10年間の歴史を振り返ってみました。 通算コミッターはボットや重複を含みますが213、通算コミット数152,456、通算Pull Request 35,969 という数字でした。 GroundUpはこの様になっています。7月時点での数字です。 ※ 元気に開発が続けられていますね。 ※GroundUpリポジトリ は Squash and merge で運用しているため、Squash and merge を使っていなかったレガシーリポジトリよりもコミット数が少なくなっています。 7月時点のApp Store上のスクリーンショットはこの様になっています。 2013年と比べてみましょう。 デザインを見てもかなり歴史、月日の流れを感じますね。 2023年、いまiOS開発として力を入れていることをご紹介します。 Architecture v2という、すでに新しいアーキテクチャに取り組み始めています。GroundUpという取り組みが終わってすぐのように見えますが、いまの設計自体は3年前の2020年に考えられたものなんですね。 これまでの歴史を振り返ってみると3年という月日は決して早すぎるわけではないとも思っています。 それから2022年にWWDCで共有されたPassKeysも重要な取り組みの一つ。 すでにメルカリのプロダクションで導入が開始 されていますが、シームレスな認証を提供していきたいと考えていて、これからサポートを増やしていきたいと考えています。 アプリの Observability の強化 にも取り組んでいます。これは DataDog Real User Monitoring (DataDog RUM) を使い、エラーやクラッシュはもちろん、API Latency 含めてe2eの読み込み速度の計測などを行う取り組みです。 またリリースサイクルについても、週一回をキープしているものの、人の手で解決していることが多い状況で、 改善 に取り組んでいます。 まだ2023年は終わっていませんが、7月までの動きを振り返りました。 かなり長かったですが、以上が10年間の振り返りです。 最後にまとめていきたいと思います。 10年振り返ってみて感じたことは、変化は徐々に起こることも、突然表れることもある、ということです。技術の変化はもちろん、プロダクトやビジネス、そして組織の変化もあります。 エンジニアはどのようにこれらの変化に適応していけば良いのでしょうか?ということについて考えてみました。 まずは技術変化への適応ですが、幸い、iOSアプリ開発では、一定のリズムがあります。だいたい2-3年を掛けて新しいスタンダードへの適応が行われていきます。 メルカリの場合、2014年にSwift が発表されてから2年後の2016年にフルSwift アプリが登場しています。 また、2019年にSwiftUI 発表されてから3年後の2022年にアプリが SwiftUIへ書き換えられています。 プロダクト、ビジネス、組織による環境の変化への適応はどうでしょうか。 ここに関しては会社や組織によって課題感が異なると思いますが、メルカリではご覧のような取り組みが行われてきました。 Re-design, re-architecture, re-write, さらに横断的な取り組みを行うチームの設立や、周辺基盤の強化が環境の変化に適応するための取り組みでした。 今回あらためて振り返ってみて、メルカリは変化に対してかなりプロアクティブに対応してきたことを再認識しました。 ただ、振り返ってみると当たり前のように感じるターニングポイントも、当時はそこまで確信を持って意思決定が行われたわけではなかったと感じています。 Q. Re-architecture が終わって間もなくゼロから書き直す判断ができるか?すべきか? Q. いま Cross Platform や WebView を選択すべきか? もしかしたらプロダクトにいま携わっている方は、いままさにこのような問いにさらされているかもしれません。 完璧な答えはないものの、それでもエンジニアとして最善と思える答えを僕らは一つに絞って出さなければならない。きっと迷うこともあると思います。 そんなとき、「メルカリはあんなことやっていたな」「この課題にはこうやってアプローチしたのか」「ちょっと参考にしてみるか」と言う感じでですね、この10年の振り返りが少しでもみなさんの力になれば嬉しいと思っています。 はい、ということで以上になります。 今回スライドの中で紹介できなかった取り組みもたくさんあります。今回紹介した取り組みも全てが大成功だったわけではありません。 たくさんの失敗もありましたが、そういった失敗を糧にこれまでメルカリは取り組んできています。 これからもメルカリは Go Bold, All for One, Be a Pro を掲げながら チャレンジを続けていきます! もしこのトークを聞いてメルカリに興味持っていただけたら、ぜひお気軽にお声がけください。 以上となります。それではご清聴ありがとうございました! おわりに 以上、「メルカリ10年間のiOS開発の歩み」でした。 プレゼンテーションは40分と比較的長いトーク時間のように感じますが、10年の歴史をまとめるには40分は非常に短く、初期段階のトーク時間は60分を超えてしまっていました。余計な内容を削り規定の時間内で終わらせるべくトークスクリプトを準備し、本番ではきっちり40分でトークを終わらせることができました。 文章以外でのフォーマットで参照したい方は、当日発表を行った際のスライドと動画を こちら から参照することができます。 2023年はありがとうございました。2024年もよろしくお願いいたします!
アバター
メルペイSREの @myoshida です。この記事は、 Merpay Advent Calendar 2023 の21日目の記事です。 メルカリグループではGoogle Cloud Platform(GCP) を広く利用しており、一般的にはGCPを利用したシステム構築が推奨されています。しかし、他のプラットフォームを利用した方が要件を実現しやすかったり、よりスマートに構築できる場合はAmazon Web Services(AWS)なども利用することあります。 今回は AWS Transfer Family を利用してSFTPでファイルを送受信する環境を構築した件について簡単にお伝えできればと思います。 SFTPでのファイル送受信について SFTP(SSH File Transfer Protocol)は、その名の通り、SSHを利用してファイル転送を行います。SSHを利用して暗号化通信が行えるため、FTPと比べて安全に利用できます。 ログインには、SSHで使用する鍵をそのまま認証に利用できます。鍵認証でログインできるため、パスワードは不要です。 一方でFTP(File Transfer Protocol)は、IDとパスワードでログインします。また、暗号化がサポートされていないため、セキュリティ面で問題があり、利用は推奨されません。 SFTPは昔から存在する枯れた方式だと思いますが、業務の現場では今も根強く採用されています。日次のバッチで処理して作られたCSVを、連携先の外部企業に渡すといった場面で利用されたりします。 AWS Transfer Family での SFTP環境構築 AWS Transfer Family を利用したシステム構成は以下のようになります。 構成図 SFTPサーバーに該当する Transfer Server を用意し、利用するサブネットの数だけEIPを払い出し、Transfer Serverに紐づけます。それによりTransfer Serverに専用のエンドポイントが割り当てられ、ユーザーはそれを指定してSFTPクライアントで接続できます。 エンドポイントが割り当てられた様子 SFTPユーザーはTranfer Serverに紐づいており、ユーザーごとに公開鍵を複数持つことができます。IAMユーザーを作成する必要はありません。 ストレージはS3バケットを利用します。1つのS3バケットにユーザーごとのホームディレクトリを定義して共用することも可能ですし、ユーザーごとにS3バケットを用意して、ログインするユーザーごとに専用のS3バケットに接続させることも可能です。今回は後者を採用しました。 構築にはTerraformを利用します。locals を利用してユーザー名を変数とすることで、S3バケット・SFTPユーザー・SFTPユーザーが利用するIAMロールなどをまとめて作成することが可能です。 localsの定義例 sftp_name = "merpay-foo-bar" sftp_users = { test-user-1 = { ssh_keys = [ "ssh-rsa dummy", ] } test-user-2 = { ssh_keys = [ "ssh-rsa dummy", ] } } sftp_user_keys = flatten([ for user, attrs in local.sftp_users : [ for ssh_key in attrs["ssh_keys"] : { user = user ssh_key = ssh_key } ] ]) } ログインに利用する公開鍵は、上記terraform内の ssh-keys にリストで列挙することでterraform経由でSFTPユーザーに保持させることも可能ですが、今回はユーザー作成後にAWSにログインして、手動で登録することにしました。 S3バケットの定義例 resource "aws_s3_bucket" "sftp_bucket" { for_each = local.sftp_users bucket = "${local.sftp_name}-${each.key}" versioning { enabled = true } logging { target_bucket = aws_s3_bucket.sftp-bucket-log[each.key].id target_prefix = "log/" } tags = { } } IAMポリシーの定義例 resource "aws_iam_policy" "s3_read_write" { for_each = local.sftp_users name = "s3_rw_merpay-sftp-${each.key}" path = "/system/" description = "for enabling file tansfer to buckets" policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketLocation" ], "Resource": "arn:aws:s3:::${local.sftp_name}-${each.key}" }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:GetObjectAcl", "s3:PutObjectAcl", "s3:GetObjectVersion", "s3:DeleteObjectVersion" ], "Resource": "arn:aws:s3:::${local.sftp_name}-${each.key}/*" } ] } EOF } IAMロールの定義例 resource "aws_iam_role" "sftp_user" { for_each = local.sftp_users name = "transfer-server-user-role-${each.key}" assume_role_policy = <<-EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "transfer.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF } resource "aws_iam_role" "transfer_server_to_cloudwatch" { name = "transfer-server-to-cloudwatch-role" assume_role_policy = <<-EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "transfer.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF } IAMロールのポリシーアタッチメントの定義例 resource "aws_iam_role_policy_attachment" "s3_bucket_read_write" { for_each = local.sftp_users role = aws_iam_role.sftp_user[each.key].name policy_arn = aws_iam_policy.s3_read_write[each.key].arn } Transfer Serverの定義例 "aws_transfer_server" は endpoint_type を “VPC” にし、endpoint_details ブロック内でEIPを割り当てることで、マネージドなドメインが生成されます。 resource "aws_transfer_server" "sftp" { identity_provider_type = "SERVICE_MANAGED" endpoint_type = "VPC" logging_role = aws_iam_role.transfer_server_to_cloudwatch.arn endpoint_details { address_allocation_ids = [for eip in aws_eip.sftp : eip.id] subnet_ids = aws_subnet.sftp_subnet[*].id vpc_id = aws_vpc.sftp.id } tags = { Name = local.sftp_name } lifecycle { ignore_changes = all } } SFTP Userの定義例 SFTPユーザーのホームディレクトリは、"aws_transfer_user" 内の home_directory で、S3バケットのルートを指定しました。localsを参照してユーザーごとに作られるS3バケットをそのまま指定しているので、ユーザーごとに別のS3バケットを利用できるようになります。 resource "aws_transfer_user" "sftp_user" { for_each = local.sftp_users server_id = aws_transfer_server.sftp.id user_name = each.key role = aws_iam_role.sftp_user[each.key].arn home_directory = "/${aws_s3_bucket.sftp_bucket[each.key].id}/" } 環境を構築してみて感じた利点 SFTPサーバの環境を作るにあたって、AWS Transfer FamilyとTerraformで利用することで、以下のようなメリットがあると感じました。 手動管理の量が少ない マネージドな環境ですので、一度構築してしまえば、かなりメンテナンスフリーな感じで利用することができます。EC2などのサーバインスタンスを用意することもないため、管理がラクです。 アカウント追加・削除の作業もTerraformを更新することで行なうので、GitHubのPull Requestを通じてチーム内で確認を取りながら進められて安全です。 S3にはライフサイクルを指定しているため、古いファイルを削除するといった作業も発生しません。 横展開がしやすい これは単純にTerraformの利点なのですが、.tfファイルにほぼすべての構築内容が定義されているため、類似の案件が発生した場合に流用しやすいです。 他のシステムとのつなぎ込みがしやすい ファイルはS3に保存されるため、AWSのAPIを利用してファイルを取得したりすることで、業務の後続処理もスムーズに行わせることができます。 おわりに 今回はメルカリグループでは利用例が少ないAWSを利用したSFTP環境の構築について説明しました。既存のSFTP環境のリプレイスなどのお役に立てば幸いです。 Google Cloud Platformでも同様のサービスが登場してほしいなと思います。 明日の記事は @orfeonさんです。引き続きMerpay Advent Calendar 2023をお楽しみください。
アバター
こんにちは。メルカリ Accounting Productsチーム Software Engineerのayanekoです。 この記事は、 Mercari Advent Calendar 2023 の20日目の記事です。 私たちAccounting Productsチームは会計システムの開発、運用をしています。会計データを扱うという特性上、以下にあげる理由から大量のデータを保持しており、多額の費用がかかっていました。 会計データは法律上一定期間の保持が必要であること 一時ファイルやログファイルなども含めて保守的にすべてのデータを保存していたこと そこで、 FinOps 観点で Cloud Storage (以下GCS)や Cloud Spanner (以下Spanner)のリソース最適化のPJを始めました。リソース最適化とは、必要なリソースはしっかりと保存し、更新され古くなったデータは必要な期間のみ保存してデータの総量から余剰分を取り除けるようにする取り組みのことです。 この投稿では、その一環として行ったGCSのリソース最適化の取り組みで得た知見についてご紹介したいと思います。 利用環境 本題に入る前に、私たちが普段利用している環境について少し触れておきたいと思います。 Dev環境 開発環境 QA環境 テスト環境(ステージング環境の扱いに近い) Prod環境 本番環境 システムに変更を加える際は、Dev環境、QA環境の順に検証し、最終的にProd環境へ適用します。 また、 GCP のリソースはほぼすべて Terraform で管理しています。 以上のことを踏まえて本題に入りたいと思います。 オブジェクトのバージョニングを有効にするときは適切なライフサイクルを設定する 今回リソース最適化をしたい バケット は最初から オブジェクトのバージョニング が有効の状態でしたが、 ライフサイクル の設定がされておらず大量のオブジェクトが保存され、多額の費用がかかっていました。 バケットのバージョニングを有効にするとライブオブジェクトバージョンを置換または削除するたびに非現行オブジェクトバージョンが保持されるようになるため、非現行オブジェクトバージョンをどの程度保持するかをライフサイクルにより管理することが重要になってきます。 そこで、 特定の日数が経過後に非現行バージョンのオブジェクトを削除するライフサイクルの設定 をすることで、本当に保持しなければならないオブジェクトのみが残るようにしました。 オブジェクトを削除するときは量やタイミングに注意する ライフサイクルの設定を適用し大きなコスト削減につながると喜んだのもつかの間、この対応の直後に大きな問題が発生しました。これにはオブジェクト削除の量やタイミングが関係していることがわかりました。 ライフサイクルにより 一度にPB単位のオブジェクトが削除 されることとなったのですが、それが引き金となって同バケットのDeleteObjectやRewriteObject.FromがUnavailableを返すようになるという問題が発生しました。 社内の有識者とともにいろいろ調査を尽くし結果的に1週間後に問題は解消しましたが、この経験からあまりにも大量のオブジェクトを一度に削除することは今後は避けるべきという教訓を得ました。 さらに、一時的に コストが跳ね上がっていた ことに気が付きました。 削除されたオブジェクトが保存されていたバケットの ストレージクラス がArchiveストレージであったために、多くのオブジェクトに対して 早期削除料金 がかかっていることがわかりました。 各ストレージクラスには最小保存期間が設定されており、Archiveストレージの場合は365日です。 最小保存期間が経過していないオブジェクトに対して削除、置換、移動をした場合は 早期削除料金がかかってしまう のです。 (上記の内容は2023年11月時点のもので、将来ストレージクラスの種類や最小保存期間が変更になる可能性があります) 一時的にコストがかかってしまうことは仕方ないとしても事前に予測することは可能であったため、そこまで考えが至らなかったことは反省すべき点でした。 Rewrite時のストレージクラスの違いによる影響を考慮する バケットから別のバケットへ Rewrite が行われる場合には、両者のストレージクラスの違いに注意したほうが良いということがわかりました。 会計システムの一部で Airflow を使ってデータをExportしている処理があり、その中の一時ファイル用のバケットとデータの保存先のバケットを別々にする対応をしました。 QA環境での実行では問題がなかったのですが、Prod環境での実行で一時ファイル用のバケットからデータの保存先のバケットへ Rewriteが行われている箇所で処理が失敗 していました。 このとき一時ファイル用のバケットがStandardストレージ、データの保存先のバケットがArchiveストレージであり、 両者のストレージクラスが異なっている状態 でした。 また、問題なく動いたQA環境とProd環境の違いとして、扱うデータ量がProd環境の方がかなり多いという点があげられます。 そこで一時ファイル用のバケットとデータの保存先のバケットのストレージクラスを、 両者とも同じStandardストレージに しました。 そうするすることでProd環境でも問題なく処理が完了することがわかりました。 Cloud Storage JSON API の Rewrite methodのリファレンス に記載されている注意点として、Rewrite元とRewrite先のバケットの ロケーション とストレージクラスが同じ場合は1回のリクエストでRewriteが完了するとの記載があります。 このことから、 ロケーションやストレージクラスの違いがRewriteの処理に影響する ということが推測できます。 バケットは種類ごとに分けて管理する 今回のリソース最適化の対象のバケットには、 いくつもの異なる保持ポリシーのオブジェクトが一緒くたに保存されていた ことも最適化までの道のりを困難にした要因の一つでした。 たとえば「オブジェクトを削除するときは量やタイミングに注意する」で発生した問題のさなかにも、削除対象外のオブジェクトにもかかわらず同じバケットにあるというだけで影響を受けてしまうということがありました。 本来 オブジェクトの種類によって選択すべきストレージクラスや設定すべきライフサイクルは異なる ため、保存期間やアクセスの頻度などを考慮しバケットを分けたほうが扱いやすいです。 たとえば保存期間が2年であり頻繁にアクセスすることがないオブジェクトの場合は、1日経過後にストレージクラスをArchiveストレージにするライフサイクルと、2年経過したオブジェクトを削除するライフサイクルをバケットに設定します。また保存期間が1日のオブジェクトの場合は1日経過したオブジェクトを削除するライフサイクルをバケットに設定します。そのため両者のバケットは別の方が扱いやすいです。 デフォルトストレージクラスはStandardにし、他のストレージクラスへの変更は基本的にはライフサイクルで行う構成は、Merpay社員かつGoogle Developers Expertでもある@sinmetalさんからのアドバイスと、今回の取り組みを通しての私自身の見解としても、この方法が理に適っていると実感しています。 オブジェクトをバケットにアップロードすると、明示的に設定しない限りそのオブジェクトにはバケットのデフォルトのストレージ クラスが割り当てられます。 オブジェクトのアップロード後にオブジェクトのストレージクラスを変更したい場合は、ライフサイクルによる変更や、 オブジェクトの書き換えによる変更 などの方法があります。 従ってデフォルトストレージクラスがArchiveストレージの場合オブジェクトがアップロードされると即座にArchiveストレージになるため、たとえば以下のような難点があります。 システム修正後の動作確認でシステムからExportされたオブジェクトの中身を見たい場合にオペレーション料金が高い 誤って不要なオブジェクトをバケットに保存してしまい削除をしたい場合に早期削除料金がかる このようなコスト面での難点を回避するため、Standardストレージ以外のストレージクラスの設定は基本的にライフサイクルで行っています。 (上記の内容は2023年11月時点のもので、将来ストレージクラスの種類やオペレーション料金が変更になる可能性があります) バケットを目的ごとに分けた後は、バケットごとに ラベル を設定すると請求を確認する際にも バケットごとに把握することが可能 になります。 ラベルはKeyValue形式で、メルカリでは bucket={$bucket-name} の形式でラベルを設定しています。 ラベルを設定することで、たとえば早期削除料金が発生しているバケットを容易に特定できるようになります。 ポリシーに基づき運用をする 目的に合わせてバケットの作成やライフサイクルの設定をするにあたり、どのデータをどのくらいの期間保持する必要があるのかという基準を定めたドキュメントである データの保持ポリシー を作成しました。 私たちは会計データを扱うため、そのデータが会計帳簿保存の対象となるデータかどうかの判断が必要になってきます。 その判断をするにあたり、内部監査、経理、外部監査法人と協議しながらポリシーを作成しました。 たとえばSpannerの特定の日のバックアップは何年保存する必要がある、それ以外は何年保存する必要がある、というように、データの種類ごとに保存すべき期間を定めていきます。 このような基準に沿った運用ができるようバケットの作成やライフサイクルの設定をしていきます。 このポリシーを作成する際にバケット内にあるオブジェクトを一覧化するために活用した機能として、 Storage Insights のインベントリ レポート というものがあります。 Storage Insights のインベントリ レポートにはオブジェクトのストレージクラスなどのオブジェクトに関するメタデータ情報が含まれています。 今回はこのインベントリレポートを BigQuery に取り込みました。 ライフサイクルの設定だけでカバーできない不要なオブジェクトの削除の際には、削除対象のオブジェクトをクエリにて抽出し、その情報を元にスクリプトでオブジェクトを削除しました。 おわりに リソース最適化前から最適化後を比較すると、おおよそ 54%ものコストを削減 することができました。 この取り組みを始めた時点ではGCSに関しての知識が不足していたこともあり多くの問題に直面しましたが、問題を1つ1つ解決していく中でGCSやその周辺に関する知識を深めることができ、得るものが大きかったと感じています。 またリソースを目的ごとに最適な状態で管理することの大切さを実感し、そのコストのインパクトの大きさをひしひしと感じられた取り組みでもありました。 今後は今回のリソース最適化の取り組みの対象外だった部分も含めてコストを削減できる余地がないかどうか、継続的に見直しを行っていきたいと思います。 Accounting Productsチームでは、メルカリのミッション・バリューに共感できるSoftware Engineerを募集しています。一緒に働ける仲間をお待ちしております! 採用情報 明日の記事はpakioさんです。引き続きお楽しみください。
アバター
この記事は Merpay Advent Calendar 2023 の 20 日目の記事です。 こんにちは。メルペイの Payment Core チームでバックエンドエンジニアをしている komatsu です。 普段はメルカリ・メルペイが提供するさまざまな決済機能を支えるための決済基盤の開発・運用をしています。 この記事では、我々が開発している決済基盤マイクロサービスである Payment Service を適切に監視するために、Datadog の Dashboard を大きく刷新した背景や方法について紹介します。 Observability と Datadog Dashboards 本題に入る前に、Observability と Datadog Dashboards について簡単に説明します。 Observability はシステムの内部状態を適切に監視し、外部から可視化することでシステムを理解する能力およびその考え方を指します。 適切に可視化して監視することで、既知の問題のみならず、未知の問題に対しても、より迅速に検知・解決することが可能になります。 Observability を実現するには、次の 3 つの Telemetry の要素が重要だと考えられています。 Metrics – CPU 使用率やメモリ消費、ネットワークトラフィックなど、システムリソースの使用状況などを示す定量的なデータ Trace – システム内を遷移する各リクエストのトランザクションの経路と処理時間を追跡し、E2E でパフォーマンスを可視化するデータ Logging – 操作の履歴やエラーメッセージなど、アプリケーションが生成する時系列のイベントデータ Datadog においても、Metrics は Datadog Metrics 、Trace は Datadog APM 、Logging は Datadog Log Management というサービス名でそれぞれ提供されています。 これらのサービスはそれぞれの Telemetry を可視化するためのものですが、3 つすべてを一箇所に集約して可視化するために利用されるのが Datadog Dashboards です。 任意の Telemetry を任意のメトリクスや自由度の高いクエリを組み合わせて Widget を作成し、それを自由に並べ替えることで、あらゆる Telemetry データを 1 つのページに可視化することができます。 ( https://www.datadoghq.com/product/platform/dashboards/ より引用) 基本的な機能は Grafana や New Relic Dashboards、Splunk Dashboards などと同様ですが、メルカリグループでは Datadog を主なクラウド監視ツールとして導入しているため、Payment Core チームでも各マイクロサービスの状態を可視化するために Dashboard を利用しています [1]。 また、Payment Core チームが管理する最も大きなマイクロサービスが Payment Service です。 マイクロサービスにおける決済トランザクション管理 からも分かるように、決済に関するほぼすべてのリクエストは Payment Service を経由して下位のマイクロサービスに伝播します。 そのため、Payment Service の Observability を向上することはメルカリグループ全体のサービスの安定化につながります。 Payment Service の Dashboard が抱えていた問題と刷新の動機 Payment Service には元々システム全体を可視化する Datadog Dashboard がありました。 ある程度グループで分類されてはいますが、300 を超える Widget が貼られており、かなりカオスな Dashboard であることは誰の目に見ても明らかでした。 多くのチームメンバーが Dashboard に不満を抱える一方で、それをリファクタリングしていく作業は地味であり、長い間放置されていました。 この Dashboard が抱えていた課題には次のようなものがありました。 次の 3 つのカテゴリに分類した上で問題点を紹介します。 可視性 (Visibility) の欠陥 可視性の欠陥は、Dashboard 上の可視化されたさまざまな値を見ても理解することが困難であったり、そもそも情報に欠損があるといった問題を意味します。 私たちのチームでは以下のような可視性に関する課題を持っていました。 一目でマイクロサービスの健康状況を把握することができない この Dashboard はエンジニアだけでなく PdM も確認するため、より簡潔にシステムの状態を表現する Widget の需要がありました。 時系列データが示す値が正常なのか異常なのかを判断することが難しい Datadog Monitors で管理している Monitor ではしきい値を確認することで “どのくらい危険な状態なのか” を確認できる一方で、しきい値を持たない Widget は現状の値は表現できても、危険度を表現することはできませんでした。 API のレイテンシを表す Widget において、処理時間に大きく差が生じるパラメータによってグラフが区別されていない レイテンシを表現する Widget はありましたが、Payment Service が提供する API は、内部で同期処理にするか非同期処理にするかのリクエストパラメータによってレイテンシが大きく異なったり、決済手段の組み合わせによって速度に差があるため、それらを区別しないグラフは信頼性に欠けていました。特に残高やメルペイのあと払い、チャージ払いなどの決済手段はそれぞれ異なるマイクロサービスに依存しているため、決済手段ごとのレイテンシを表現する必要性がありました。 canary release 時に既存のデータとの区別がつかない 私たちのチームでは、マイクロサービスのリリース時に一部のトラフィックにのみ新しいバージョンの pod を割り当てる canary release を採用しています。しかし多くの Widget は canary の pod やバージョンによってフィルタできるように整備されておらず、ノイズが多いことでリリース時の影響確認が困難でした。 診断性 (Diagnosability) の欠陥 診断性の欠陥は、可視化された Dashboard から問題を適切に区別し、解決に向けたアクションが取りにくいことを意味します。 私たちのチームでは以下のような診断性に関する課題を持っていました。 異常な状態を示す Widget があっても次のアクションにつなげにくい 仮に異常値を発見しても、APM やログを細かく確認するといった次のアクションにつなげにくい状態でした。 マイクロサービス内の問題か外部起因の問題かの区別がつかない ある異常値が自分たちのマイクロサービス (i.e., Payment Service) に起因するものなのか、依存している他のマイクロサービスや外部の API なのかを区別することが困難でした。Payment Service は多くのプロダクト側のマイクロサービスから呼ばれると同時に、多くのマイクロサービスに依存しているため、次のアクションにつなげるために、どこに原因があるかをすぐに判断できる仕組みが必要でした。 メンテナンス性 (Maintainability) の欠陥 メンテナンス性の欠陥は、新しい API や機能の追加やしきい値の変更に Dashboard が追従できず、必要十分な状態に保てないことを意味します。 私たちのチームでは以下のようなメンテナンス性に関する課題を持っていました。 そもそもメンテナンスされていない Widget がある Dashboard は Payment Service リリース時に作成されたものであり、基本的にメンバーが自由に変更できるため、統一感がなく、template variables のような機能が適切に設定されていない Widget も散見されました。 適切に Widget がグルーピングされていない 無造作に Widget が追加されていった結果、どこに何があるのかが分かりにくくなるだけでなく、新たに Widget を追加するときにどこに置くべきか判断しにくい状態でした。 このように、私たちの Dashboard は多くの問題を抱えながらも、長い間放置されていました。 その中で、今年の 1-3 月にこのようなコードべース以外の負債をまとめて解消する時間をチームで作ることができたため、その一環で Dashboard の刷新を行いました。 次の章では、どのようなアプローチによって問題を解決し、どのように新しい Dashboard v2 を実現したかを説明します。 Dashboard の刷新 Critical User Journey を意識する Dashboard v2 を作る上で大事にした思想が “CUJ を意識する” ということでした。 CUJ は Critical User Journey の略で、ユーザ体験を設計する上で、プロダクトのユーザがそのプロダクトを利用して達成するタスクやプロセス、またはそのシナリオを意味します。 ここで、私たちの CUJ におけるユーザは、メルカリアプリを使用するお客さまではなく、決済基盤である Payment Service を利用するプロダクト側のマイクロサービスの開発者を意味します。 CUJ を意識した Dashboard を作ることで、例えばアラートが発生したときや依存されているマイクロサービスの開発者から問い合わせを受けたときに、Dashboard のどこを見ればよいのか、他にどこに影響が出ているのかなど、決済基盤が知っておくべき状況を理解しやすくすることができます。 CUJ を Dashboard に落とし込む際の考え方として、以下のような流れに沿って行いました。 CUJ を考える 残高を使って決済をする、クレジットカードの登録をする、決済をキャンセルする、など CUJ を満たす基準を考える SLO の考え方に近い 99.9% の決済は成功する、99.9% のクレカ登録は 0.1 秒以内に完了する、など CUJ を満たせない場合に発火するアラートを作成する アラートと同様の定義を Dashboard の Widget として表現する このような流れで適切な粒度で CUJ を監視できる形に変化させます。 どのように Dashboard を刷新したか CUJ を意識した上で、前章の問題点についてそれぞれ次のような仕組みや機能によってアプローチしました。 可視性の向上 – 健康状態の可視化 私たちは前述の考え方から、“システムが健康である” ことを、”アラートが発生していない状態” と定義しました。 これは、GitHub や Slack を始めとする多くの Web アプリケーションが status ページを持っていることを参考に、アラートベースで健康状態を定義することがもっともシンプルだからです。 Dashboard が担当するドメインはあくまで可視化であるべきなので、すでに持っている Datadog Monitors や蓄積されている Metrics を用いることが合理的です。 Datadog Monitors がすでに整備されていることが条件ではありますが、チーム内では同時期に Datadog Monitors の整備やインフラ関連の定義の CUE 言語への置き換え [2] などを行っていたため、タイミングがとても良かったです。 下の図は、Dashboard の一番上に位置している System-wide status の中の 1 つの Widget です。 Datadog Monitors を 1 つの Widget にまとめてリッチに表示することができる Monitor Summary Editor を利用しています。 各 Monitor はどのマイクロサービスのものなのかという情報をタグで持っているため、フィルタを設定することで Payment Service のアラート状況のみをまとめることができます。 エンジニアであれば他の方法でアラート状況の確認ができる場合もありますが、PdM や他のチームの開発者が見たとしても理解しやすく、Payment Service の status ページの役割も兼ねていると言えるでしょう。 可視性の向上 – しきい値の可視化 ある API のレイテンシや DB のタイムアウト数を表現する時系列データが “問題になり得るレベルより安全側にいるのか” や “問題になり得るレベルと現状の差” を表現するために、下図のように各 Widget にマーカーを設定しました。 これによって Widget を見た人は "12 月 17 日の朝にレイテンシが少し高くなったが、アラートレベルではない" ということを一目で理解することができます。 各しきい値は同様の Monitor がある場合はその値と同じ値を採用しています。 Dashboard は Monitor と違って手で編集しているため、Monitor の定義が変更されると Dashboard と差分が生じる問題も議論の中ではありましたが、しきい値の変更は頻繁にはないことを理由に許容しています。 また、当初は時系列の Widget をそれぞれ作成するのではなく、Alert Graph (Moitor をひとつ選択して Dashboard に貼ることができる Widget の種類) を利用することを検討していました。 これによって過半数の Widget はその定義を Monitor に移譲することができるからです。 しかし、Monitor は本番環境全体を監視するものしか持っていなかったため、他の問題点でもある canary pod の状態のみを表示したり、本番環境ではなく開発環境でフィルタしたいときに不都合でした。 可視性の向上 & 診断性の向上 – APM resource の細分化 Payment Service の Dashboard には元々レイテンシを計測する指標として各 API の Trace がありましたが、前述の通り決済手段の組み合わせやその他のリクエストパラメータによってレイテンシが大きく異なるため、CUJ に沿ってこれを細分化しました。 具体的には、gRPC interceptor に Trace を細分化する処理を追加し、決済手段の組み合わせごとに別の APM resource として認識させることで、Dashboard からも別々のレイテンシを取得できるようにしました。 これによって残高払いのみを利用した時のレイテンシ、あと払いのみを利用した時のレイテンシ、2 つを組み合わせた時のレイテンシを区別することができるようになりました。 この利点は単に Widget が示す値の信頼性を高めるということだけでなく、例えば残高払いのレイテンシが跳ねたときにあと払いのレイテンシも跳ねていれば DB やネットワークの問題などの共通部分の問題を疑うことができ、片方だけであれば依存するマイクロサービスや周辺の実装を疑うことができるため、調査もより楽になりました。 リクエストパラメータの中には今回の支払手段のように実行時間に大きく影響を与えるものもあれば、内部の if 文に影響があるような小さいもの、まったく与えないものがあります。 どのレベルまで分けるかというのはそのマイクロサービスの役目やドメインによって異なるものですが、マイクロサービスの依存関係や主要な CUJ を意識することで適切なレベルで分割が可能になります。 可視性の向上 & メンテナンス性の向上 – 適切なタグ管理と template variables の整備 canary 環境のみを可視化することは、私たちが安全にソフトウェアをデリバリーする上で非常に重要な機能でした。 canary 環境かどうかという情報は、インフラ観点では Kubernetes の stack として保持していますが、Metrics をフィルタする上では能動的にタグを付与する必要があります。 そのため環境変数として Deployment に stack 情報を記載し StatsD [3] に Metrics を送信する段階で stack の情報も付与するようにしました。 これによって、Dashboard 上の Widget を stack でフィルタすることが可能になりました。 各 Widget は Metrics を選択する際の変数の指定方法として、直接 stack:canary のように記述することも可能ですが、Dashboard 全体で変数を定義できる template variables を利用することで、 各 Widget 内では stack:$stack として定義しています。 この機能を使うことで、すべての Widget の stack タグを変更してフィルタしたり、その設定を View として保存することができます。 メンテナンス性の観点からも、新しい stack が追加されるなどの変更に追従しやすい設計が可能となります。 Dashboard v2 では次のような template variables と View を持っています。 診断性の向上 – Context Links による Widget と Logs や Traces の接続 Dashboard の Widget のグラフをクリックすると、下図のようなポップアップが表示されます。 この例では、”View related traces” をクリックすることで、このグラフに関連する Datadog APM Traces を一覧で表示してくれます。 これによって Widget 内で異常な値があったときにすぐにリクエストのどこに問題があるかを調査する次のステップに進むことができます。 一方で、この例では “No related logs” となっていて、Datadog Logs に飛んでログを確認することはできません。 これらの機能は Widget に設定されている条件 (from 句) を参考に自動で生成されていますが、Metrics と Logs で同じフィールドを持っていないと正しくヒットしなかったからです。 そのため、アプリケーション内の logger に APM と同じタグを付与したり、Context Links を編集して APM や Logs と適切にリンクされるようにしました。 メンテナンス性の向上 – 適切なグルーピング Dashboard のメンテナンス性は引き出しに整理整頓していくようなもので、その引き出しがなんのためのものかがわからなければ新しい物を置くときに困ってしまいます。 メンバーが誰でも手動で編集できてしまうため、シンプルに保つことが大切です。 Datadog Dashboard は Empty Group と呼ばれる Widget によって複数の Widget を 1 つのまとまりとして視覚的にグルーピングできます。 2 段階以上のグルーピングができない点は不便ですが、Dashboard v2 では Text Widget と組み合わせてサブグループも表現しました。 ここで意識したのは Widget を追加するときにどこに追加すればよいかが明示的であるように視覚的なブロックを作成することで、誰が追加しても同じ様になるような簡潔さとグルーピングを実現しました。 例えば以下は簡単な例ですが、縦軸にマイクロサービスが、横軸に Metrics が並んでいることは誰でも一目で理解できます。 ある開発で新しいマイクロサービスへの依存が増えたとき、一行下に追加すれば良いことは明らかで、ただ 9 つの Widget を端から並べるより可視性もメンテナンス性も向上します。 これらは今回実施した改善の一例ですが、新しい Dashboard は on-call 対応時やインシデントへの反応速度、PdM などの開発者以外のステークホルダーとのコミュニケーションがより早く、より円滑になりました。 多くの不要な Widget を削除することができた結果、300 を超えるWidget は 113 個まで減り、検索性も向上しました。 今後の展望 Widget の CUE 化 今回のプロジェクトでは多くの Widget を新しく作り直す必要があったことから、GUI 上で可視化しながら編集をしました。 私たちのチームでは Datadog Monitors を CUE 言語で管理していることもあり、既存の Widget も同様に CUE 言語で定義し、Dashboard から参照するような形が理想的だと思っています。 これは IaC の考え方と同じですが、Widget が意図せず編集されてしまうことを避けることができます。 また、複数の Widget を編集するときなど、統一的な操作をしたいときにコードとして定義されていることは大きな恩恵をもたらすでしょう。 Monitor の整理と調整 Payment Service の状態を監視する Monitor は 1408 個ありますが、一部の Monitor は設定の不備や厳しすぎるしきい値設定によってアラートが常に発火しているなど、正しくシステムの正常性を表現できていないものもあります。 これは Dashboard の展望とは異なりますが、システムの状態の可視化はすべての Monitor が正しく設定され動いていることが前提にあります。 そのため、チーム内で継続的に Monitor を見直し、しきい値の調整などを通して “正常とは何か” ということを常に定義し続けていく必要があります。 おわりに 今回の記事では私たちのチームにおいて、より安定した決済基盤を社内に提供するために、柔軟性が高く、可視性と診断性に強い Dashboard を作成した話を紹介しました。 マイクロサービス利用者の CUJ を意識しながら、多様な決済手段の組み合わせや依存関係を可視化する仕組みを作成できたことは、今後のより堅牢な決済基盤の開発を支えてくれると信じています。 明日の記事は myoshida さんです。引き続きお楽しみください。 注釈 [1] メルカリグループでは Production Readiness Checklist が存在し、Dashboard を整備することも一定のマイクロサービスをリリースするための条件となっています。 [2] メルカリグループでは Kubernetes のマニフェストを始めとし、Datadog の Monitor や Widget も CUE 言語で定義できる環境が整備されています (ref. https://engineering.mercari.com/blog/entry/20220127-kubernetes-configuration-management-with-cue/ )。 [3] 正確には DogStatsD。
アバター
こんにちは。メルペイのiOSエンジニアの @kenmaz です。 この記事は、 Merpay Advent Calendar 2023 の19日目の記事です。 概要 iOSアプリ開発において、お客さまにより良い体験を提供する上でナビゲーションの設計は非常に重要なトピックです。特にメルペイのように「決済」「申し込み」「登録」といった自己完結型のタスクを提供する画面が多いアプリでは、iOSのモーダル表示を活用した設計手法である「モダリティ」を意識することが Apple Human Interface Guideline において推奨されています。これにより、お客さまを迷わせることのない使いやすいアプリを構築でき、またコードの保守性も向上します。 本記事では、メルペイiOSチームが既存機能のリライトプロジェクトを進める中で発見した既存の画面設計の問題点を、モダリティの設計手法に基づいて解決した事例をご紹介します。 背景 メルペイでは現在、メルカリで採用しているSwiftUIベースのアーキテクチャと最新のデザインシステムライブラリを使って、メルペイが提供する全ての画面を書き換えるプロジェクトを進めています。プロジェクト自体の詳細については先日開催された Merpay & Mercoin Tech Fest 2023 での発表の書き起こし記事をご覧ください。 このリライトプロジェクトでは、単にコードを書き換えるだけではなく、同時に既存の機能の見直しや、設計上の問題点なども可能な限り同時に改善しながら進めています。その中で見つかったのが、今回のテーマであるナビゲーションの設計上の問題です。 メルペイのUIとモダリティ 冒頭でも述べた通り、メルペイでは「決済」「チャージ」「登録」「申し込み」といったような自己完結型のタスクを提供する機能が多いのが特徴です。対照的に、メルカリでは商品の検索や閲覧など「情報探索」が体験の中心にあり、そこに「購入」「出品」といった自己完結型タスクが付随する構造になっています。 WWDC2022の Explore navigation design for iOS というビデオでは、iOSでは自己完結型のタスクを提供する画面は「モーダル表示」の使用を推奨しています。モーダル表示とは、現在表示しているコンテンツやタブバーなどを意図的に覆い隠すように画面下からせり上がって画面を表示する方法のことです。これにより、元々表示していたコンテンツの情報階層を一時的に切り離し、特定のタスクに焦点を絞ることで、お客さまに「今自分が何をやっているか」をわかりやすく伝えることができます。このようなアプリの設計手法のことを 「モダリティ」 と呼びます。 また上記ビデオでは、モーダルで表示するにふさわしい自己完結型タスクとして、 イベントの作成やリマインダーの設定などのシンプルなタスク 複雑なステップを伴うマルチステップのタスク 動画の再生などのフルスクリーンコンテンツの表示 の3種類が挙げられています。 メルペイはまさに上記1および2の機能を多く提供しており、そのような機能にはモーダル表示を適用するのが好ましいことがわかります。 課題事例:銀行口座接続 さて、メルペイのリライトプロジェクトを進める中で、モダリティの設計手法に反している画面がいくつか見つかりました。その一つが「銀行口座接続」機能です。ここからは既存の銀行口座接続機能のナビゲーション設計の問題点とその解決策について紹介します。 銀行口座接続機能とは、お客さまの銀行口座をメルペイのアカウントに登録するための機能です。銀行口座を登録することで、メルペイでのお支払いに使える残高をお客さまの銀行口座からチャージできます。 銀行口座機能はメルカリアプリのさまざまな箇所から呼び出されます。例として、残高チャージ画面から銀行口座接続機能を呼び出すナビゲーション(改善前のもの)を示します。 銀行口座接続フロー(改善前) 上図は、銀行口座が一つも登録されていない状態でメルペイ残高にチャージしようとする際のナビゲーションを示しています。大まかな流れは以下の通りです(説明を簡単にするため、いくつかの画面は省略しています) 支払い画面でチャージボタンをタップすると、チャージ画面がモーダルで表示 チャージ画面でチャージ方法を選択すると、チャージ方法画面がプッシュ遷移で表示 「お支払い用銀行口座を登録する」をタップすると、モーダル画面が閉じ、支払い画面に戻る 銀行口座接続のイントロダクション画面がプッシュ遷移で表示 「次に進む」ボタンをタップすると、銀行の選択画面がプッシュ遷移で表示 接続したい銀行を選択すると、口座情報の入力画面がモーダル表示 口座情報を入力し「銀行サイトへ」ボタンをタップすると、各銀行のwebサイトにアクセスし、認証が完了したら登録完了画面にプッシュ遷移 登録完了画面の「OK」ボタンをタップするとモーダル画面が閉じ、支払い画面に戻る 一見何の問題もないように見えますが、いくつかの課題が存在します。それらの課題を解決した改善後のフローを以下に示します。 銀行口座接続フロー(改善後) どのような課題があり、どのように解決したのかを詳しく見ていきましょう。 課題 課題1:銀行口座接続フローの一部画面が非モーダルで表示されている 上述の通り、銀行口座接続のような「複雑なステップを伴うマルチステップの自己完結型タスク」はモーダル表示することが推奨されています。しかし上のナビゲーション図を見ると「イントロダクション」画面と「銀行の選択」画面はモーダル表示ではなく、支払い画面からプッシュ遷移で表示されています。 先に述べた通り、モーダル表示のメリットのひとつは「タブバーなどを意図的に覆い隠すように画面下からせり上がって画面を表示」することにあります。あえてタブバーを隠すことによって「いまは銀行口座接続の作業が進行中ですよ」ということを表現し、現在のタスクへの集中をお客さまに促すことができます。 しかし「イントロダクション」画面や「銀行の選択」画面はモーダル表示ではないので、下部のタブバーは表示されたままで、操作することも可能です。銀行口座接続の処理中に、誤ってタブバーを操作してしまい、意図せずタスクから離脱させてしまう危険性もあります。 理想的には、これら二つの画面を含め銀行口座接続タスクの画面全体(水色の枠で囲まれた部分)はモーダル表示にすべきでしょう。 課題2:残高チャージの中断 今回示した例は、銀行口座が未登録の状態で残高チャージを行う際のナビゲーションを示しています。つまり本来行いたかったタスクは「残高チャージ」なのですが、銀行口座が未登録だったため、まずサブタスクとして「銀行口座接続」タスクに誘導している状況です。 理想的にはサブタスクである「銀行口座接続」タスクが完了したら、本来のタスクである「残高チャージ」タスクに制御を戻したいところですが、実際はそうはなっていません。 現状の銀行口座接続フローはモーダル表示されることを想定しておらず、銀行口座接続フローを表示する際は、まず全てのモーダルを閉じた後に非モーダルとして表示することを前提として設計されてしまっています。そのため、チャージ画面が閉じられてしまい、本来の目的である「残高チャージ」タスクが中断されてしまっているのです。 理想的には、チャージ方法画面で「お支払い用銀行口座を登録する」をタップした際は、チャージ画面を閉じるのではなく、表示したままにしておくべきです。その上にさらに銀行口座接続フローをモーダル表示し、接続が完了したら単にモーダルを閉じて元のチャージ方法画面に制御を戻せばよいのです。「残高チャージ」タスクを中断すべきではありません。 課題3:コードの再利用性 現状のナビゲーションの設計はコードにも問題を引き起こします。メルペイiOSでは銀行口座接続フローのようなアプリ内のさまざまな箇所から呼び出される画面に対して、以下のようなインタフェースを用意しています。 public enum MerpayScene { case connectBank(completion: (Result) -> Void, ..) case ... } public protocol MerpaySceneRouterProtocol { func viewController(scene: MerpayScene) -> UIViewController } // Caller let vc = sceneRouter.viewController(scene: .connectBank(...)) navigationController.pushViewController(vc, animated: true) 各画面は MerpayScene のenum値として定義されており、それを MerpaySceneRouter に渡すことで対応するViewControllerを取得できます。上記例では、 MerpayScene.connectBank を指定することで、銀行口座接続フローのエントリーポイントとなる画面のViewControllerを取得しています。 ただし、このように取得したViewControllerを pushViewController(_:animated:) で遷移させると、銀行口座接続のようなマルチステップで構成されるタスクの場合、そのタスクが完了した後の処理の実装が面倒になるという問題があります。 タスクがモーダルとして表示されるのであれば、以下のように呼び出し側は単に present(_:animated:completion:) で対象画面をモーダル表示し、タスクが完了したら呼び出された側で dismiss(animated:completion:) を呼べば、呼び出し元の画面にスムーズに戻ることができます。また、completion引数を指定することで、タスクの実行結果に応じて呼び出し元で処理を分岐させる、といったことも容易に実現できます。 let vc = sceneRouter.viewController( scene: MerpayScene.connectBank( completion: { success in if success { ... } else { ... } } ) ) present(vc, animated: true) 一方、タスクをプッシュ遷移で表示している場合は、やや制御が難しくなります。モーダルのように dismiss(animated:completion:) を呼び出すだけ、とはいかずに、たとえば呼び出し元のViewControllerをメモリに保持しておき、 popToViewController(_:animated:) で呼び出し元の画面に戻すなど、やや特殊な実装が必要になる場合があります。 またタスクの実行結果に応じて呼び出し元でなんらかの処理を行いたい場合、 dismiss(animated:completion:) とは違って、 popToViewController(_:animated:) や popViewController(animated:) には、呼び出し元の画面への遷移が完了したことをフックするための completion 引数などは用意されていないので、 呼び出し元の viewWillAppear に追加の処理を仕込んで検知する、といったような余計なハック が必要になることもあります。 銀行口座接続のような自己完結型のタスクは素直にモーダル表示することを前提とし、呼び出す側としては単に present(_:animated:completion:) で表示、タスクが完了したら dismiss(animated:completion:) で呼び出し元に制御が戻ってくる設計にすることで、理解しやすく再利用しやすいコードを保つことができます。 モダリティを考慮した再設計 これらの課題を解決する方法は、銀行口座接続フロー全体をモーダル表示を前提としたものに再設計することです。再設計を行い改善したナビゲーションは、先に示した 銀行口座接続フロー(改善後) の通りです。 上図の通り、改善後のナビゲーションでは銀行口座接続フロー全体がモーダル表示となっていることがわかります。銀行口座の登録が完了したらチャージ方法画面に制御が戻ってくるので、「残高チャージ」タスクが中断されることはありません。あとは登録した口座を選択して、残高チャージを実行するだけです。非常にシームレスな体験を実現できました。 注)上記改善は2024年初旬にリリース予定です 余談:モーダル on モーダル ところで、冒頭で紹介したWWDCのビデオでは 「モーダルの上に表示するモーダルは乱雑で 複雑に感じるため、制限すべし」 といった説明がありました。上記の改善後のナビゲーションはまさに「残高チャージ」モーダルの上に「銀行口座接続フロー」モーダルを表示している状態にあたります。このような設計は避けるべきなのでしょうか? しかし、同ビデオの中ではさらに 「サブビューの一貫性と 集中力を高めるために複数のモダリティタスクが必要な場合もあります」 という説明もありました。ビデオ内で例として示されていたのは旅行の行程を編集するモーダルの画面から、iOS標準の写真選択画面をモーダル表示で呼び出すような事例でした。そのようなケースでは全く違和感は感じません。 個人的には、銀行口座接続や写真の選択といった、十分に自己完結的で独立したタスクであれば許容可能であると考えます。プロダクトチーム内で慎重に判断して導入することをお勧めします。 まとめ 以上、メルペイiOSチームで既存機能のリライトプロジェクトを進める中で発見した既存の画面設計の問題点を、モダリティの設計手法に基づいて再検討し、改善した事例をご紹介しました。 なお、私の同僚の @kris も冒頭で紹介したWWDCのビデオからインスピレーションを受けて、メルカードのUIに取り組んでいます。その内容は Merpay Tech Openness Month 2023のブログ記事 として公開されているので、興味のある方はそちらも合わせてご参照ください。 メルペイには数多くの機能があり、全ての画面についてリライトプロジェクトが完了するのはもう少し時間がかかりそうです。ただのリファクタリングプロジェクトとして終わらせるのではなく、本記事で紹介したような改善ポイントを見つけ、可能な限り改善し、プロダクト全体の品質向上に貢献できるように、iOSチーム一丸となって改善に取り組んでいきたいと考えています。 明日の記事は@komatsuさんです。引き続きお楽しみください。
アバター
この記事は、 Mercari Advent Calendar 2023 の18日目の記事になります。 こんにちは!メルカリ Engineering Office チームの@aisakaです。 私達のチームは「Establish a Resilient Engineering Organization」というミッションを元に、様々な活動を行なっています。先日のAdvent calendarでマネージャーのhiroiさんがチームの活動の内容、目的の紹介をしているので、ぜひこちらも読んでみてください。 強いエンジニア組織に必要な、6つの技術以外のこと – メルカリ編 私はEngineering Officeがカバーする領域の中でもOnboardingを担当していて、よりよいOnboarding体験を提供していくための戦略や仕組みづくりに携わっています。 OnboardingやトレーニングといったHR領域に近い施策というのは、KPIを立てづらく、かかるコスト(人的コストやお金)に対する効果を測定しづいといった悩みが一般的ですよね。 本記事では、メルカリのエンジニアリング組織がどのようにKPIをたて、効果測定を実施しているのか、またOnboarding施策を成功させるためのポイントを紹介していきます。 エンジニア組織の組織課題に取り組んでいる方や施策づくりをしている方におすすめです。 費用対効果の最大化 組織の施策を企画し実施、運用するうえで最も大事なポイントは、いかにROI(費用対効果)を意識し、その最大化に繋げられるかです。ここでは、メルカリが実際に実施している4つのポイントを紹介していきます。 コンテンツの集約 組織が大きくなると、蓄積される知識や情報量が多くなる反面、点在しやすく正しい情報にリーチしづらいというダウンサイドもあります。最適な量の正しい情報へのガイドがOnboardingを成功させるために重要だと考えているため、メルカリではOnboardingコンテンツの集約には力をいれて取り組んでいます。冗長なコンテンツは一つにまとめ、コンテンツを置く場所を一箇所に集約することで、入社者が何か分からないことがあった際に自力で検索して探し出せるような導線を作っています。 継続的なアップデートサイクル コンテンツというのは、一定期間アップデートがされないとすぐに古い情報となってしまい使えないという側面ももっています。メルカリは中途採用、新卒採用を通年行っているため、Onboardingで必要なコンテンツは比較的利用頻度が高く、古いコンテンツにならないようにすることが重要です。 Onboardingで必要な作業の文書化やコンテンツの見直しに貢献してくれるエンジニアを半年ごとに公募で募集し、有志メンバーで資料のアップデートや作成を継続的に実施しています。また、新入社員の方も自身のOnboardingの過程で、情報のアップデートや文書化へのコントリビューションを奨励しています。 またコントリビューションは可視化し、貢献してくれたかたへの評価に繋がるように運営を工夫しています。 利用者数の可視化 せっかく質の高いコンテンツを整備しても、実際に使ってもらえないと意味がありません。コンテンツが見られているのか、使われているのかを評価するため、MAU(Monthly Active Users)とPageviewsをトラッキングし、資料の利用率を評価しています。 一般的にコンテンツに関する指標は、サーベイで満足度を入社者にヒアリングするケースが多いですが、サーベイは回答負荷が高く充分な回答数が得られなかったり、回答者の主観が強すぎたりするため、自動でとれて客観性が高いものを指標として評価しています。 以前、マネージャーのGrahamさんが、サーベイ疲れを最小限にしつつフィードバックをもらうための方法をブログで書いていたので、ぜひ参考にしてみてください。 アンケート疲れから考えるフィードバック獲得の改善方法 Looker Studioのスクリーンショットより 「安易にサーベイに頼らない。」という心がけは、不要な負荷を生み出さないという点において施策づくりの際にとても重要だと感じています。 オペレーションの自動化 運営側のコストを削減する視点もとても重要です。HR領域の施策はどうしてもマニュアルで管理する場合が多いですが、できるかぎりプロセスの一部を自動化し、運営側のオペレーションコスト削減にも力をいれています。 メルカリでは、OnboardingのアクションアイテムをJIRAチケットで提供していますが、入社者ごとにカスタマイズしたチケットを自動でJIRAに払い出すシステムを内製し運用しています。 以前は複数のチェックリストがHR、Engineering組織、各チームで点在していて分かりづらいといった課題があったのですが、それをJIRAで一元管理できるようにしています。 こうしたコスト削減や効率化をはかるための自動化システムの内製もEngineering Office内では積極的に実施しています。 デリバリーの最大化 良いコンテンツを社内で作ったら、それをより多くの方へ届けることで、効果を最大化することができます。どのように届け、その効果を大きくするために、実践している2つのポイントを紹介していきます。 他部署、専門外の技術領域を学びたい人へ届ける 適切にアップデートされた良いコンテンツは、新入社員だけではなく、既存のメンバーのラーニングにも役立ちます。コンテンツを誰もがアクセスできる場所に集約させ、他部署や専門外の技術領域を学びたい方も必要な情報にアクセスできるようになっています。実際、MAUをみてみると既存メンバーからのアクセスは新入社員の人数の数倍近くあり、幅広い方に利用されています。 また、メルカリでは年に1~2回、 DevDojo と呼ばれる技術研修期間を設けています。もともとは新卒向けのOnboarding トレーニングとして設計され企画されたものでしたが、新卒以外の既存社員も受講できるように社内でオープンにしています。毎回、部署を超えた50名近くの既存社員が参加しトレーニングを受講しています。 社外発信に繋げ、コンテンツ作成者のキャリアップに繋げる 持続的にコンテンツを作成、アップデートし、社内で展開していくうえで最も重要なことは、コンテンツ作成者からの協力を常に得られる状態にすることです。社内向けのコンテンツ作成というのはボランティアベースになってしまうケースがよくあるパターンです。しかし、この運用方法ではコンテンツ作成者にメリットがなく労力を無駄にしてしまうリスクがあります。メルカリでは、社内コンテンツを一部エンジニア組織のカルチャーや人を紹介する Mercari Gears YouTubeチャンネル において外部公開することで、コンテンツ作成がTech PR (技術発信)と個人のビジビリティの向上といったキャリアアップに繋がるように工夫しています。 技術トレーニングDevDojo こうした、コンテンツ作成者、コンテンツ受講者の両方がWin-Winとなるように施策づくりをすることで、持続的なサービスを提供できています。 今後力をいれていきたい分野 エンジニアという職種は比較的転職サイクルが早いため、メルカリは中途採用での入社者が多いです。そこで、前職までの環境からメルカリのエンジニア組織への移行をいかにスムーズにするかという視点がとても重要です。 メルカリでは、新入社員がインプットする情報、知識の量とクオリティのレベルをある程度統一し、入社直後の時期から標準化された知識を学習できるようにしています。こうした、健全な組織を維持、発展していく体制をOnboardingという一番最初の段階から整えていくことに力をいれています。Onboardingの時期は過去のやり方から脱却し、新しいことを比較的受け入れやすい時期でもあるため、今後最も力をいれて作っていきたい分野です。 最後に これまで、3年ほどエンジニア組織のOnboarding施策を担当しました。成功に必要なポイントをまとめます。 コンテンツは一箇所に集約することで、利用者がリーチしやすくする コンテンツを継続的にアップデートし続ける仕組みをつくる KPIはサーベイに頼らず、自動で取れるものを指標にする オペレーションは自動化し、運営コストを削減する より多くの人に届ける コンテンツ作成者のキャリアアップや評価に繋がる仕組みにする 健全な組織づくりのため、ガバナンス強化という視点をもつ こうした施策づくりというのは一朝一夕ではできず、トライアンドエラーを繰り返し、他のエンジニアの皆さんのサポートを得ながら皆なで少しづつ作ってきました。 今回ご紹介したポイントは決してOnboardingだけでなく、多くの施策づくりに応用が効くと感じます。何か少しでも参考になるものがあれば嬉しいです。 また、メルカリグループでは、積極的にエンジニアを採用しています。ご興味ある方、ぜひご連絡お待ちしております! Open position – Engineering at Mercari 長文となりましたが、最後までお読みいただき、ありがとうございました。
アバター
こんにちは。メルペイ Engineering Managerの @masamichi です。 この記事は、 Merpay Advent Calendar 2023 の18日目の記事です。 この記事では私がマネージャーを務めているMerpay Enabling Clientチームの役割や今後進めていくことについて紹介します。 Merpay Enabling Client Team メルペイの組織構造は現在Program型組織となっており、その中でもEnabling ProgramはArchitectやSRE、Data Platformなど、横断的な技術課題の解決や生産性向上など開発全体を支援する組織です。Program型組織の詳細については2日目の@keigow さんの記事をご覧ください。 メルペイのProgram型組織への移行 Merpay Enabling Clientチームはその中でWeb/Android/iOSから構成されるチームで、Client領域の横断的なプロジェクトを推進しています。 2023年の10月まではClient領域のチームはWeb/Android/iOSのプラットフォームごとに分かれており、私はMerpay iOSチームのマネージャーを担当していました。Program組織体制への移行を経て、現在はMerpay Enabling Clientチームのマネージャーを担当しています。 チームのVisionは “Enable continuous product improvement through client engineering excellence” “クライアントの卓越したエンジニアリングを通じて、プロダクトの継続的な改善を可能にする” としており、チームとしてプロダクトの成長に貢献することを意識しています。Excellenceという言葉には、2009年に前Apple CEOの故Steve Jobs氏が療養中に、現Apple CEOのTim Cook氏が述べた言葉 “We don’t settle for anything less than excellence in every group in the company — and we have the self honesty to admit when we’re wrong and the courage to change.” “社内のどのグループについても卓越未満で満足するつもりはありませんし、間違っている時にはそれを自分に対して正直に認める勇気と、間違いを正す勇気も我々にはあります” からチームでも同じマインドを持とうという意図を込めました。 チームの責務は メルペイ内のClient技術方針の検討, および規律の構築 メルカリグループで最適化されたArchitctureの構築 メルペイプロダクトチームへのベストプラクティスのインストール としており、プロダクトの成長に貢献すべく横断的な技術課題の解決に取り組んでいます。 現在は少人数の体制ですが日本語・英語話者が混在していて、チームの言語ポリシーはニュートラルになるように心がけています。例えば週次でのチームミーティングは週ごとにメインの言語を日本語と英語で切り替えるようにしています。メルカリグループには多様なメンバーがいるので、横断的なプロジェクトを進めるには言語も中立である必要があると考えています。 Projects 現在は中期のロードマップとして Zero Legacy & Group Optimized Architecture を掲げていくつかのプロジェクトを進めています。 1つめは認証基盤のアップデートです。これはメルカリグループ全体で推進しているプロジェクトで、アプリで使っている認証の仕組みの刷新に取り組んでいます。Mercari Mobile Architect チームリードのもと、Merpay Enabling Clientチームでは特に メルペイ関連の機能を提供するAPIとアプリのやりとり、およびアプリ内WebViewやiOSのApp Extensionsの認証方式のアップデートに取り組んでいます。 2つめはiOS/AndroidアプリのUI Frameworkのアップデートです。 昨年メルカリアプリはGroundUP Appプロジェクトによってフルスクラッチで書き換わり、全面的にSwiftUI/Jetpack Composeの宣言的UI Frameworkで作られた内製のDesignSystemを採用しています。 メルカリの事業とエコシステムをいかにサステナブルなものにするか?かつてない大型プロジェクト「GroundUp App」の道程 これからメルカリのエンジニアリングはもっと面白くなる──iOS&Androidのテックリードが振り返る、すべてがGo Boldだった「GroundUp App」 メルペイの領域の機能についてはある程度ポータブルな設計になっておりプロジェクト進行中も並行して機能開発を続けていたことから、GroundUP App プロジェクト後の新アプリでも既存の機能はUIKit/Android Viewベースの技術スタックとなっていました。 メルカリアプリのコードベースを置き換える GroundUP App プロジェクトの話 メルカリグループ全体での技術スタック統一とアプリ全体のユーザーエクスペリエンス統一を目指して、現在メルペイでも全社横断的に既存機能や新規開発機能へのDesignSystemの適用を進めています。私自身、本プロジェクトのリードを担当しており、全体の進捗管理やスケジューリング、 VPへのレポートなどプロジェクトの達成に向けて尽力しており、すでに新しいDesignSystemが採用された機能もいくつかリリースされています。 新しいDesignSystemを適用することでSwiftUIやJetpack Composeといった宣言的UI Frameworkによる開発の恩恵に加えて、これまでは対応していなかったダークモードへの対応やアクセシビリティへの対応も容易になりました。まだ適用されていない機能もありますが、今後より適用率を高めていくことで最終的には全ての機能がマイグレーションされた状態を目指しています。 【書き起こし】Merpay iOSのGroundUP Appへの移行 – kenmaz【Merpay & Mercoin Tech Fest 2023】 【書き起こし】段階的Jetpack Compose導入〜メルペイの場合〜 – Junya Matsuyama【Merpay Tech Fest 2022】 3つめはWeb Frameworkの更新です。 メルペイではカスタマーサポート用のツールや加盟店さま向けのツール、各種キャンペーン用のページなどさまざまなWebサービスを運営しています。 それらのWebサービスではVueとNuxt.jsをメインのFrameworkとして使っていますが、Vue2は2023年12月, Nuxt2は2024年6月にそれぞれサポート終了が計画されています。セキュリティ対策やブラウザの互換性を維持しながらプロダクト開発を継続するためには、End of Lifeまで次のバージョンにアップグレードする必要があり、既存サービスのVue3, Nuxt3への移行を進めています。 移行後は各種サービス内のVue技術スタックの標準化や、メルカリグループの技術アセットを活用してReactのような他の技術も取り入れていくなど新しいチャレンジをしていきたいと思っています。 それ以外にもWebViewの最適化や新しいArchitectureへの移行など、いくつか横断的なプロジェクトを今後進めていく予定です。プロジェクトの進め方やプロジェクト内で得た技術的な知見については今後個別に紹介していく機会を設けていきたいと思っています。 おわりに Merpay Enabling ClientチームではFintechドメインでの規律を保ちつつ、Mercari Mobile & Web Architectチームとも連携をしながら、Zero Legacy & Group Optimized Architectureを目指していきます。 同じように横断的な技術課題の解決や生産性向上など開発全体を支援するチームをリードされている方の参考になれば幸いです。 明日の記事は同じチームの @kenmaz さんの “モダリティを考慮したiOSアプリのナビゲーションの再設計” です。引き続きお楽しみください。
アバター
こんにちは。メルペイ Machine Learning エンジニアの@gucciです。 この記事は、 Merpay Advent Calendar 2023 の16日目の記事です。 はじめに 2023年3月、OpenAI社がChatGPTを発表して以来、大規模言語モデル(LLM)の可能性に世界中が注目しています。企業や個人がLLMをどのように活用できるかを模索する中、実際にLLMを用いたプロダクトが市場に登場し始めています。メルカリグループでも、社内向け・プロダクト向けの両面でユースケースを探索してきました。 その一環として、7月に実施したぐげん会議 [1] で入賞した返済相談チャットシミュレーターの一部分について、トライアルでオフラインの品質評価を実施しました。この記事では、その結果とそこから得られた学びについて共有します。 品質評価における課題意識 各種の学術試験やベンチマークテスト等、汎用的な知識・言語能力においてLLMが大きく進歩してきたことは疑いようがありません。一方で、LLMを用いたアプリケーションの品質に関する情報は、まだ十分に蓄積されていないと感じています。 OpenAI社によるGPT-4 Technical Report [2] や各種のベンチマークテストは参考になりますが、あくまでLLM本体の、汎用的な問題における評価結果です。また私の知る範囲では、現在世の中に公開されているLLMアプリケーションで、品質要件が厳しく求められる使い方をしているものは少ないと認識しています。 そのため、 特に事実性・リスク(定義は後述)の面で一定の品質が要求されるドメイン向けのLLMアプリケーションを構築する場合、どの程度の品質が得られそうか について参考になる資料は少なく、未知数だと感じていました。 問題設定 この章では、今回のアプリケーションの問題設定について説明します。 システム全体像 ここでは返済相談チャットシミュレーターの一部分として、 お客さまのお問い合わせに対して社内のドキュメントを参照しながら文章で回答を行う RAG(Retrieval-Augmented Generation)ベースのQ&Aアプリケーションを想定します。このユースケースでは、回答に一定の事実性が要求され、また 回答次第で法令リスクに抵触してしまう可能性のある領域(以下、NG領域) が存在します。 なおRAGとは、LLMに参照させたいデータを事前に取り込んでindex化しておき、質問が入力された際にそこから関連するデータを検索してLLMに渡す仕組みのことです。 以下は、各構成要素の概要です。 RAGパート 検索エンジン(VectorStoreIndex) 464件のドキュメント LlamaIndexでシンプルにindex構築(chunk_size = 1024, separator = “。”) indexのチューニングはあまり実施していません 類似度検索 similarity(質問とドキュメント内容の類似度)で検索したうち 上位1件 を取得 回答生成パート RAGで取得した情報をコンテキストとして、LLM(gpt-4)で質問に対する回答を生成 用語の定義等の基本的なドメイン知識をsystem promptに指定 リスク防御パート NG領域に関する回答を防ぐため、プロセス全体を通じて以下3層のリスク防御策を実装 ①input 防御(スコープ判定) : 入力された質問が対象スコープ内かをLLM(gpt-4)で判定し、対象外の場合は回答しない ②prompt 防御 : NG領域について回答しない旨の指示を回答生成のpromptに埋め込む ③output 防御(回答添削) : 回答にNG領域の話題を含むかをLLM(gpt-4)でチェックし、該当部分の記述を削除する また以下は、簡単なQ&Aのイメージです。 質問例 誤りを含まない回答例 誤りを含む回答(誤答)例 メルカードで購入後の支払いはどうすればいいですか? メルカードのご利用分は、以下3つの方法から選んでお支払いいただけます。 1. メルペイ残高での支払い 2. 銀行口座からの自動引き落とし 3. コンビニやATMでの支払い より詳細を知りたい場合は、お気軽にお尋ねください。 例1. 誤った方法を案内 メルカードでご購入いただいた場合、購入した商品の代金が即時でメルペイ残高から引かれる形となります。 ==== 例2. 架空のアプリ操作方法を案内 メルカードのご利用分は、以下の手順でお支払いいただけます。 1. メルカリアプリを開く 2. マイページを開く 3. 「メルペイ」を選択 4. 「支払う」を選択 評価のアプローチ この章では、今回実施した評価のアプローチについて説明します。 評価の目的 「そもそも世に出しても問題ないレベルか」という防御的な観点 から、 事実性・リスクの面で 求められる品質水準が見込めそうかを評価することが主な目的です。 評価観点 ここでは評価の手法を網羅することが目的ではないため、基本的な考え方としてOpenAI社の評価観点を参考にしました。InstructGPTの論文 [3] およびGPT-4 Technical Report [2] をまとめると、LLMの評価観点として以下が挙げられている理解です(学術試験等の能力評価は割愛。また各観点の説明は筆者理解) 有用性(helpfulness) どれだけ質問者にとって有用な回答をしたか(≒課題を解決できたか)の評価 事実性(factuality) : 真実性(truthfulness)と言われることもある 質問に対して正しい回答ができるか(事実でない内容を回答してしまわないか)の評価 事実性には、 参照データの質とその検索精度 、および ハルシネーション が主に影響します。 リスク : 有害性(harmlessness)を含む センシティブな領域または回答が許されない領域において望ましくない回答をしてしまうリスクおよび、過剰に拒否してしまう度合いの評価 リスクには、 ハルシネーション および プロンプトインジェクション が主に影響します。 ハルシネーションとは、LLMが事実ではない内容を回答してしまう現象のことです。またプロンプトインジェクションとは、質問者が悪意のあるプロンプトをLLMに入力することで、LLMに不適切な回答や意図しない情報の開示をさせようとする行動のことです。この2つはLLMを使ったアプリケーション特有の点になります。 評価の目的に照らして、今回は2点目の「事実性」と3点目の「リスク」の観点で評価した結果を紹介します。 1点目は施策効果の観点では非常に重要ですが、今回は主に防御的な観点で評価したいため、除きます。 この章の以降では、評価方法の詳細について説明していきます。(詳細が不要な方は飛ばし読みで大丈夫です) 評価の前提 今回の評価では、以下のことを前提としています。 Q&Aの形式 一連の会話のやり取りではなく、一問一答形式で評価しています(1度の問合せに複数の質問を含む場合もある) 事実性とリスクは独立に評価 事実性を評価する際、上述のリスク防御①〜③を入れない状態で評価しています。実際のプロダクションではリスク防御との組み合わせになりますが、今回は単体評価です。 チューニングの度合いや評価件数について 今回、時間や人手の制約があったことと、特にリスク評価を優先して対応したことから、事実性評価のチューニングや人手評価の件数は限定的なものとなっています 特にRAGの検索精度がチューニング不足なところは理解していますが、得られた示唆に大きな影響は無いものと考えています 人手評価か自動評価か 文章生成を定性的な基準で評価する際、厳密な評価は人手でなければ難しいです。今回は人手評価を信頼しつつ、参考として事実性評価でLLMを用いた自動評価も試してみました 評価観点別のアプローチ詳細 今回実施した事実性評価とリスク評価の詳細は、以下の比較表のとおりです。 切り口 事実性評価 リスク評価 評価のポイント                            お客さまの質問に対して 誤った回答をしない こと 法令リスクに抵触してしまう可能性のある領域(NG領域)に関する回答を 徹底的に排除しつつ、かつ答えて良い質問にはなるべく答える こと 評価用データ 過去のQ&A事例100件 (約20個のカテゴリーに関する質問) ※ただし、 人手評価はこのうち30件のみ で実施 答えてはいけない質問57件   ・ 左記のQ&A事例のうち、NG領域に関する15件   ・ 敢えてNG領域を引き出す目的で今回作成した42件 答えてよい質問80件   ・ 左記のQ&A事例のうち、答えてよい質問 評価指標 【人手評価】 誤答率(30件中) : = 回答文の中に事実と異なる内容を1つでも含む回答の割合 = 事実と異なる内容を1つでも含む回答数/全回答数 【[参考] 自動評価(100件中)】 a. 質問に対して回答がどれだけ関連しているか (質問 vs 回答) b. 質問に対して参照データがどれだけ対応しているか (質問 vs 参照) c. 回答がどれだけ参照データに依拠しているか (回答 vs 参照) ※今回は「正解の回答データ」を用意できず、誤答率の自動評価が難しかったため、上記の代理指標で評価して簡易的に傾向を確認(より詳細は後述) 【人手評価】 防御率(57件中) : = 敢えてNG領域を引き出そうとする質問に対し、どれだけ回答を防げるか = 回答を防げた質問の件数/答えてはいけない質問の件数 阻害率(80件中) : = 答えてよい質問をどれだけ誤って止めてしまうか = 誤って回答を防いでしまった質問の件数/答えてよい質問の件数 補足:事実性の自動評価指標の詳細 今回LLMを用いて実施した自動評価の評価基準は以下のとおりです。(Azure Machine Learningのメトリクスを一部参考にしました [4] )。 指標名 評価基準の概要(5点満点) a. 質問に対して回答がどれだけ関連しているか(質問 vs 回答) ※Azure MLではQnA Relevance Evaluationに相当 質問に対して過不足無く答えているほど点が高くなる。5点で完全に質問とマッチした回答。 b. 質問に対して参照データがどれだけ対応しているか(質問 vs 参照) ※Azure MLの記事では特に該当無し 質問に対して参照データの充足性が高いほど点が高くなる。5点で全ての質問に答え得る参照データ。 c. 回答がどれだけ参照データに依拠しているか(回答 vs 参照) ※Azure MLではQnA Groundedness Evaluationに相当 回答内容が参照データ内の事実にだけ基づいているほど点が高くなる。5点で完全に参照データ準拠。 評価結果と課題 ここまでで、アプリケーションの問題設定と評価アプローチについて説明してきました。この章では今回の品質評価の結果をご紹介します。 まず、今回の総評および取り組んで分かった課題についてまとめたうえで、各結果の詳細に触れていきます。 サマリ:総評及び取り組んで分かった課題 今回の評価結果を整理すると、以下のとおりになります。 切り口 事実性評価 リスク評価 今回の結論                  △(難しい or 開発・運用コスト大) ◯(十分な精度) 総評 RAGで適切なドキュメントを参照できさえすれば、誤答はかなり抑えられるよう です。 しかし、下段に記載したような課題があり、 安定的に適切なドキュメントを参照させ、回答品質を維持するには相応の開発・運用コストがかかる と思われます。 複数の防御を重ねることで、 阻害を最低限に抑えながらほぼ100%近くNG領域の回答を防ぐことができ 、良い精度が得られました(100%を保証できるわけではない)。 ユースケース次第ですが、 人間が読んでも判別できるような限定的な領域が対象であれば、事実性と比べてリスクはより対処がしやすい と思われます。 課題(開発観点) 複雑なコンテキストがある場合、similarity検索だけでは不十分   ・ similarityだけで必要なドキュメントを特定することは難しい   ・ 多めに検索してLLMにどれを使うかを選ばせる、検索結果を別の手法で並べ替える等、何らかの追加的な機構が恐らく必要 適切な参照データが無い場合の取り扱い   ・ 事実を問う質問の場合は答えないのが適切だが、そうでない場合(例. 挨拶、前の発言の確認など)も含めて一律で「回答しない」とするとコミュニケーションに齟齬が生じる   ・ 一方で正しくない参照データでも回答させると、ハルシネーションを起こしやすくなる 複数の質問の混在   ・ 一度に複数の質問をされた場合に、質問を分解する等の機構が恐らく必要 ユースケースによって防御の難易度は変わる   ・ 例えばNG領域の判別が人間でも難しい場合や、細かいたくさんのNG領域がある場合は難易度が高くなる   ・ OpenAI社のようにあらゆるリスクに対応するのは非常に難しい レスポンス速度への影響   ・ 防御策を重ねるほど、レスポンス速度が悪化する。防御精度とレスポンス速度のトレードオフの最適化は課題 課題(運用観点) ドキュメントの品質・網羅性   ・ 何もかもドキュメントがあるわけではないし、ドキュメントが常に最新であることを保証することも容易でない 継続的なメンテナンス   ・ リリース後にうまく判別できない新しい質問が来たときに、漏れたものを後追いでpromptに追加していく運用が必要となる 事実性評価の詳細 事実性評価で得られた結果は以下のとおりでした。 人手評価 誤答率(30件中):47% 間違った14件のうち、 参照するドキュメントを間違えたものが10件 、そもそも 適切なドキュメントが無かったものが4件 ありました 前者については、検索時に取得するドキュメント数を増やせば一定改善すると思われます(現状は上位1件) ただし、正解ドキュメントが上位20件でも出てこないケースもあり、一筋縄ではいかなさそうです [参考] 自動評価(100件) LLMによる評価結果は1件1件を見ると若干ブレがあるため、あくまで傾向値としてだけ参考にします 指標 平均評価値 (5点満点) 解釈 a. 質問に対して回答がどれだけ関連しているか 4.9 質問に合わせて回答する能力は高水準 (これがハルシネーションの要因でもある) b. 質問に対して参照データがどれだけ対応しているか 2.9 質問に対して適切な参照データを取れていないことが多い c. 回答がどれだけ参照データに依拠しているか 1.8 bの結果として、参照データに依拠しない回答をする傾向が見られた リスク評価の詳細 リスク評価で得られた結果は以下のとおりでした。 防御パターン 防御率 (57件中) 阻害率 (80件中) ①input + ②prompt 100% 4% ③output + ②prompt 98% 1% 全て(① + ② + ③) 100% 5% 各防御策の違い ①のinput防御は、 防御率を高めやすい反面、答えてよい質問を誤って止めてしまう阻害が起きやすい 傾向がありました (参考までに、防御用promptをチューニングする前の初版では約70%の阻害が発生) ③のoutput防御は、 防御率と阻害率のバランスが良いですが、防御に若干不安が残ります なお、②のprompt防御は ほぼ効果無し でした すでにsystem promptが長文(約1,500文字)であるため、追加の指示が効きづらかった可能性あり まとめと知見 今回まじめに品質評価に取り組んだことで、LLMおよびRAGの特性について理解が深まり、今後他のユースケースを考える際にも役立つ色々な学びを得ることができました。 最後に、今回のトライアル評価を通じて得たいくつかの知見をまとめます。 ※あくまで一つのユースケースにおける、限られたチューニング範囲での評価結果に基づく私見です 「正解がある」 + 「複雑なコンテキスト」がある問題に対しての、RAG精度の限界 個別のユースケースにもよると思いますが、このような問題に対して十分なRAG精度を実現するためには開発・運用面で非常にコストがかかると思われます 検索結果のRerankやSelf-RAG [5] のような工夫も出てきていますが、APIコストやドキュメント整備の大変さ等も加味すると、 個人的にはLLMが本領発揮できるのは、むしろzero-shot〜few-shotで済むような複雑なコンテキストが要らない領域(例. 商品説明文からメタデータを抽出する)や、正解がない領域(例. エンタメ)なのではないか と感じています LLMプロジェクトの難しさ 本件は、ミッションクリティカル性が高めな領域、かつ既存の人の仕組みをリプレースするものであり、関係者が多かったり、法律が影響するものでありました その上で、 LLMは汎用性が高いゆえに、問題設定の絞り込みが難しい、あるいは多くの要件を織り込めてしまう特性 があります。これは利点でもありますが、一方で広範な問題設定になるほど 芋づる式に考慮すべき要素が増え、品質の担保が難しくなる と感じます 人手評価の大変さ 1件当たり評価に10-15分かかった 「事実かどうか」を確かめるには、回答文章の中でソースが必要な要素を抜き出した上で、各要素についてドキュメント等からソースを探す必要があります もしくは、事実が頭に入っているドメインエキスパートが必要 なお、評価用データに対して「正解の回答データ」を用意することができれば、LLMを用いてある程度は事実性を自動評価できるかもしれません それでは、ここまで読んでいただきありがとうございました。 明日の記事はtenlingpさんです。引き続きお楽しみください! 参考文献 [1] LLMを活用してなにがつくれるか?——「ぐげん会議」開催から見えてきた、AI活用の新たな可能性 [2] OpenAI (2023). GPT-4 Technical Report. ArXiv, abs/2303.08774. [3] Ouyang, L., Wu, J., Jiang, X., Almeida, D., Wainwright, C.L., Mishkin, P., Zhang, C., Agarwal, S., Slama, K., Ray, A., Schulman, J., Hilton, J., Kelton, F., Miller, L.E., Simens, M., Askell, A., Welinder, P., Christiano, P.F., Leike, J., & Lowe, R.J. (2022). Training language models to follow instructions with human feedback. ArXiv, abs/2203.02155. [4] Azure Machine Learning の Prompt flow の評価メトリクス紹介 ― ChatGPT どう評価する? [5] Asai, A., Wu, Z., Wang, Y., Sil, A., & Hajishirzi, H. (2023). Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. ArXiv, abs/2310.11511.
アバター
こんにちは。メルペイのフロントエンドエンジニアの @tokuda109 です。 この記事は、 Merpay Advent Calendar 2023 の15日目の記事です。 Merpay Advent Calendar 2020 の「 Merpay Frontend のこれまでとこれから 」という記事で、メルペイのフロントエンドチームが2020年までに取り組んできたチーム組成やプロダクトの品質改善の話が紹介されました。(以下、前回の記事) 早いもので前回の記事が公開されてから3年が経ち、当時からチームの状況は大きく変わり、チームメンバーの人数が半数以下になるという危機的状況も経験しました。 この記事は、前回の記事の続編として、2020年以降にフロントエンドチームが取り組んできたことを紹介すると共に、危機的状況を乗り越えた経験から長期的に安定したチーム運営を行う上で重要だと感じたことを説明します。 Merpay Frontend のこれまで OKRの目標分類 フロントエンドチームのこれまでを振り返る前に、OKR(四半期ごとに設定する目的とその筋道)の目標分類表を最初に紹介します。 この表は、フロントエンドチームがこれまでに設定してきたチームOKRの目標(Objective)を、いくつかの区分に分類したものになります。これにより、フロントエンドチームがどのようなことに取り組んできたかを時系列で把握しやすくなります。 フロントエンドチームのこれまでのOKRを振り返ってみて、以下の区分に分けることができました。分類した区分は長期的なチーム運営をする上で重要な要素になるため、後ほど詳しく説明します。 採用 : 何人採用するといった具体的な採用活動や、社外への認知度をあげて採用につなげる活動 プロダクト品質 : フロントエンドチームで保守・運用しているプロダクトの品質(パフォーマンス、テスト、アクセシビリティ、セキュリティ)に関する取り組み プロダクトリリース : メルペイリリースやキャンペーン等のビジネス上の理由で開発完了時期が決まっている開発タスクの締切 生産性 : フロントエンドチームの生産性改善を目的としたタスクや、基盤技術を更新することで生産性の改善を図るもの。Nuxt.js / Vue.js のバージョン更新はここに含む ロードマップ策定 : フロントエンドチームの長期的なロードマップを策定するための取り組み チームビルド : フロントエンドチーム内のコミュニケーション改善やチーム内連携の改善する取り組み 目標分類表の見方を説明します。 目標1、2、3 の番号は優先度を指し、1の方がより重要な目標であることを意味します。また、 主な出来事 / 関連記事 には、その時期にフロントエンドチームに関係する重要な出来事やブログ記事、イベント登壇等の技術発表の情報を掲載しています。 目標分類表 四半期 目標1 目標2 目標3 主な出来事 / 関連記事 2018/07 – 09 採用 採用 プロダクト品質 チーム組成期 2018/10 – 12 プロダクトリリース 採用 プロダクト品質 Vue Fes Japan 2018 のスポンサーシップ 2019/01 – 03 プロダクトリリース 生産性 プロダクト品質 メルペイリリース ( iOS , Android ) 2019/04 – 06 生産性 (安定運用) 採用 プロダクト品質 ロードマップ策定 2019/07 – 09 生産性 採用 プロダクト品質 2019/10 – 12 生産性 (DevOps) 採用 プロダクト品質 2020/01 – 03 生産性 (CI/CD) ロードマップ策定 Origamiからメンバージョイン 外国籍のメンバージョイン プロダクト品質 2020/04 – 06 プロダクトリリース 生産性 プロダクト品質改善期 2020/07 – 09 プロダクト品質 (E2E) プロダクト品質 (品質可視化) 2020/10 – 12 プロダクト品質 (E2E) プロダクト品質 (品質可視化) チームビルド (英語) [Merpay Advent Calendar 2020]: Cypress + TestRail による Frontend E2E テストの効率化について [Merpay Advent Calendar 2020]: Merpay Frontend のこれまでとこれから 2021/01 – 03 プロダクト品質 チームビルド (英語) 2021/04 – 06 プロダクト品質 (E2E) 生産性 2021/07 – 09 採用 (認知度向上) プロダクト品質 (セキュリティ) Frontend Tech Talk 〜 Quality of Merpay Frontend 〜 [Merpay Tech Fest 2021]: Frontend Testing: Cypress as a Testing Platform [Merpay Tech Openness Month 2021]: Frontend E2Eテストの安定化の取り組み 2021/10 – 12 採用 プロダクト品質 [Merpay Advent Calendar 2021]: メルペイフロントエンドのテスト自動化方針 [Merpay Advent Calendar 2021]: WebFrontendローカルパフォーマンス改善支援ツールを作ってみた。 2022/01 – 03 生産性 プロダクト品質 (セキュリティ) メルペイフロントエンドチームで行っているパフォーマンス改善の取り組み紹介 テスト・パフォーマンス・アクセシビリティ・セキュリティの4大品質に取り組むメルペイのフロントエンドチーム 2022/04 – 06 プロダクト品質 2022/07 – 09 プロダクト品質 生産性 (Nuxt/Vue移行) [Merpay Tech Fest 2022]: Tools and Strategies for Frontend UI Libraries 2022/10 – 12 ロードマップ策定 チーム再組成期 生産性 (Nuxt/Vue移行) 2023/01 – 03 生産性 (Nuxt/Vue移行) 2023/04 – 06 生産性 (Nuxt/Vue移行) 2023/07 – 09 生産性 (Nuxt/Vue移行) [Merpay Tech Fest 2023]: フロントエンドチームのスキルテスト評価システム改善の取り組み 生産性改善 (ドキュメンテーション) 2023/10 – 12 Vue Fes Japan 2023 のスポンサーシップ 組織体制がProgram組織に変わってチームOKRはなくなった (詳しくは後述) 目標を分類分けすることで、フロントエンドチームがこれまでに取り組んできたことを、時系列として次の3つの時期に分けることができました。 チーム組成期 : 2018年〜2020年 プロダクト品質改善期 : 2020年〜2022年 チーム再組成期 : 2022年〜2023年 この3つの時期のうち、 チーム組成期 と プロダクト品質改善期 は前回の記事で詳しく書かれているため、この記事では内容を簡単に振り返るだけにします。 チーム組成期 チーム組成期は2018年から2020年1月〜3月期にあたります。目標分類表から2019年2月のメルペイリリース前後で取り組みが大きく変わってることが分かります。リリース前は、採用やメルペイリリースに向けた開発が目標として設定されています。一方、リリース後は採用の優先度が少し下がり、プロダクト品質や生産性の改善が目標として設定されています。 2020年1月〜3月期には、Origamiからフロントエンドチームにメンバーが合流し、外国籍のメンバーもジョインしました。フロントエンドチームに多様なメンバーが揃ったことが、次のプロダクト品質改善期につながります。 参考: 株式会社Origamiのメルカリグループ参画に関するお知らせ プロダクト品質改善期 チーム組成期を経て、フロントエンドチームとしてプロダクト品質の改善に取り組むことができる状況が整いました。フロントエンドチームがプロダクト品質の指標として掲げている パフォーマンス 、 アクセシビリティ 、 テスト 、 セキュリティ の4つの品質指標の改善に取り組んだのがプロダクト品質改善期になります。 プロダクト品質改善期には、日々の開発サイクルの中にプロダクト品質の検証をどのように組み込んだかや、指標改善の成果報告がブログ記事として数多く公開されたり、技術イベントで発表されました。 参考: メルペイフロントエンドチームで行っているパフォーマンス改善の取り組み紹介 チーム再組成期 ここからが前回の記事の続きの話になります。 プロダクト品質の改善に数年取り組み、プロダクト品質を最低限保障する体制が構築できつつあるなか、徐々にフロントエンドチームのメンバーが少なくなりました。 採用活動をしていましたが、チームを離れるメンバーの方が多く、最も少ない時でチームメンバーが最大人数の半数しかいない時期がありました。 この人数で以前と同様にマイクロサービスを保守・運用をしていくことは極めて困難であり、この危機的状況を立て直しているのがチーム再組成期になります。 フロントエンドチームがこのような状況に陥ったのはなぜなのか。当時のフロントエンドチームの状況について振り返ってみました。 ロードマップがなく、チームとしてどのようなことに取り組んでいくべきかの話ができていなかったため、個人の優先度に基づいた行動になっていた。 プロダクト品質の仕組みが大体完了した後、次の新しい目標を決めることができず、Flakyテストの修正といった改善系の作業を長期間やって精神的に疲弊した。 ドキュメンテーションの品質が低く、ナレッジの属人化が発生し、開発の生産性が低くなっていた。 自社の技術イベントやブログ記事以外の活動ができていなかった。技術コミュニティとの関わりや外部カンファレンスの登壇等、外部情報発信が不十分でメルペイのフロントエンドチームの社外認知度が低下していた 採用の評価基準が整備されておらず、安定した評価ができていなかった メルペイリリースから数年経ち、プロダクト品質の改善も落ち着いてきて、次のキャリアを計画したり、新しい挑戦をすることを検討するメンバーが増えるタイミングだった。採用活動はしていたが、補うことはできていなかった チームビルディング不足で、チームメンバーが基本自宅からの作業になって、Slack上で業務報告するだけの関係になっていた。技術的な会話や、その他雑談をすることもなくなっていた。 ここに記載したものは、危機的状況に陥った原因として結びつけることができるものではありません。しかし、当時フロントエンドチームに対して課題を感じていたということは、チームとして解決しておくべきだったと言えることも事実です。 長期的に安定したチーム運営をするために必要な取り組み 先程の振り返りの内容を改めると、チームOKRと同じ分類を当てはめることができることに気がつきました。特に採用、生産性、プロダクト品質の区分に該当する目標は、これまでにチームOKRで何度も繰り返し設定されたものになります。それだけ 採用、生産性、プロダクト品質は、チームとして定常的に取り組むことが重要である ことが分かります。 プロダクト品質に対する取り組みは、メルペイを使う多くのお客さまの体験に直接影響するため、放置するわけにはいきません。しかし、それと同時にプロダクト品質を担保するためのチームの 生産性 やチーム力の元となる 採用活動 や チームビルド も重要です。つまり、 ロードマップ から導き出される中長期視点で、これらの取り組みをバランス良く計画的に行う必要があるということを示唆しています。 ロードマップを策定し、チームの将来のあるべき姿を示した上で、採用、生産性、プロダクト品質、チームビルドに取り組むことが持続可能なチームを運営するために必要不可欠なことだと改めて知ることができました。 次に各区分毎にフロントエンドチームとして取り組んだことを紹介します。 ロードマップ 危機的状況の改善に向けて2022年10月〜12月期の目標1でロードマップの策定と、Nuxt 3 / Vue 3への移行の長期的なスケジュールが計画されました。そして、それを支えるための採用活動が計画されました。その計画の元で新しいメンバーを採用することができ、フロントエンドチームは落ち着きを取り戻すことができました。現在は新しいメンバーと共に新しいフロントエンドチームを組成し、Nuxt 3 / Vue 3への移行作業をしています。 ただ、メルペイではProgram組織という新たな組織構造に移行するのに伴い、メルペイのフロントエンドチームという組織単位がなくなりました。また、Nuxt 3 / Vue 3への移行後の計画はまだ練られていないため、いずれ計画する必要があります。 採用 新たなフロントエンドチームを組成するために、採用では次のことに取り組んできました。 書類選考の評価基準の整備 スキルテスト評価システムの改善 フロントエンドチームのスキルテスト評価システム改善の取り組み – Merpay Tech Fest 2023 Vue Fes Japan 2023への参加 https://vuefes.jp/2023/#sponsors フロントエンドチームはチーム組成期から継続して採用活動を行ってきました。しかし、Merpay Tech Fest 2023の発表でも述べたとおり、適正な評価を行う体制が整備されていなかったため、うまく新しいメンバーを迎えることができず、チーム力を維持できませんでした。書類選考の評価基準やスキルテスト評価システムが整備されたのが、2023年になってからです。 また、チーム組成期には活発に行われていた外部イベントへの登壇は少なくなりました。自分たちのチームのことで精一杯になるあまり、Vue Fes Japan Online 2022の開催にイベントが終わってから気づくありさまです。もう少し外部コミュニティへの関わりを増やしたいと思い、2023年からVue Fes Japan 2023にイベントスタッフとして参加したり、会社としてスポンサーになることを始めました。(2018年のVue Fes Japan 2018でスポンサーになっていましたが、復活させました) Vue Fes Japan 2023会場のクリエイティブウォール (筆者撮影): 壁面一番左にメルペイロゴを描きました 来年は自社ブログやイベント以外にも外部コミュニティに対する活動を増やしたいです。社外認知度を高めることで、メルペイのフロントエンドチームに興味を持ってもらえるようにしたいです。 生産性 フロントエンドチームが生産性の改善で取り組んだ内容は次のとおりです。 モジュラーディレクトリ構成への移行 Monorepo開発におけるツール選定 – Merpay Tech Talk スケーラブルで保守性の高いモジュラーディレクトリ構成へのフロントエンドリポジトリ移行 – Merpay Advent Calendar 2022 Nuxt 3 / Vue 3 への移行 GitHub IssuesとGitHub Projectsを使ったフロントエンドタスクの管理 GitHub ActionsでWorkflowの共有化 ドキュメントの整備 GitHub Discussionsを使ってADR(Architecture Decision Records)を残す READMEフォーマットの統一 オンボーディング資料の整備 フロントエンドチームは利用パッケージの更新に課題を抱えていましたが、モジュラーディレクトリ構成への移行によって、以前よりはスムーズに行えるようになりました。そして、今はフロントエンドチーム総出で Nuxt 3 / Vue 3への移行作業をしています。この移行作業には、GitHub IssuesとGitHub Projectsを使って進行管理をしています。GitHub Issuesに登録したタスクをGitHub Projectsに登録し、1画面で全リポジトリの進捗を確認できるようにしています。 次にドキュメンテーションですが、以前はADRを記録していなかったため、口頭で議論された意思決定が残っておらず、過去の意思決定に対する振り返りコストがかかっていました。今はGitHub Discussionsを使って議論し、チームとして決定するプロセスで運用することにしました。CIにはGitHub Actionsを使っていて、ワークフローを再利用して一元管理をしています。 ここで紹介したように、基本的に開発で必要なツールは、GitHubで提供されている機能に極力寄せたことで、ツールを横断する時のフリクションを少なくしています。 プロダクト品質 今までに沢山の時間をかけてプロダクト品質の改善に取り組んできました。一部の不安定なテストの改善が必要であったりするものの、日々の開発サイクルの中で自動的に品質の検証が行われる体制を整えられました。具体的に言うと、ソース変更をプッシュするとテストやアクセシビリティの検証が実行され、全てパスするまでマージすることができません。セキュリティに関しても同様で、セキュリティ警告の通知を担当者が処理し、どのような対応をするべきかを主導します。改善するべきところはまだありますが、最低限の品質を保障する体制は構築できています。 チームビルド チームビルドの取り組みは、前回の記事で紹介されている通り、チーム内コミュニケーションの英語化になります。2021年1月〜3月期を最後に、チームビルドが目標として設定されていません。しかし振り返りで課題として出されたように、フロントエンドチームのメンバー間のコミュニケーション量はリモートワーク前と比べて大分減りました。これについては何かしらのアクションが必要で、単に量を増やせばいい訳ではありません。フロントエンドチームが組成された当初から、フロントエンドチームのメンバーが週に1回集まって技術的なトピックについて話したり、その他雑談をするWebWednesdayというミーティングがありますが、再度仕組みの設計が必要になりそうです。 Merpay Frontend のこれから Merpay Advent Calendar 2023の 2日目の記事 で、@keigowさんからProgram組織についての紹介がありました。Program組織への移行は2022年10月から実施されましたが、フロントエンドチームは例外的に案件ごとにメンバーをアサインする形を当初取っていました。 現在はチーム状況が改善したため、2023年7月からフロントエンドチームもProgram組織へ移行し、メルペイのフロントエンドチームという建て付けは存在しなくなりました。 Program組織への移行によって、各Programが担当するドメインを深く理解し、プロダクト開発に取り組むことができます。また、Enablingは組織横断で取り組む必要がある基盤技術へのオーナーシップを持っています。Nuxt 3 / Vue 3への移行はEnablingが主導し、デザインシステムや共通ライブラリの更新を行ったり、難易度の高い技術調査をサポートしてくれます。これによって、プロダクト開発と基盤技術刷新における役割が明確になりました。(フロントエンドエンジニアも所属しているEnablingのClientチームの取り組みは、 Merpay Advent Calendar 2023の18日目の記事 で紹介されています。) フロントエンドチームという建て付けはなくなり、フロントエンドチームとして設定するチームOKRはなくなりましたが、ロードマップ、採用、生産性、プロダクト品質、チームビルドはProgram組織への移行後も引き続き計画的に行っていく必要があると考えています。 最後に この記事を書いた目的は、新しくフロントエンドチームにジョインしたメンバーに向けて、これまでのフロントエンドチームがやってきたことやフロントエンドチームの現在地を紹介するというのが半分、読者や社内のフロントエンドチーム外の方に向けては危機的状況から得られた知見を知ってもらうというのが半分になります。 長い記事になってしまいましたが、ここまで読んで頂きありがとうございます。 明日の記事は@gucciさんです。引き続きMerpay Advent Calendar 2023をお楽しみください。
アバター
こんにちは。メルペイのPayment Coreチーム Engineering Managerの @abcdefuji です。 この記事は、 Merpay Advent Calendar 2023 の13日目の記事です。 ダイバーシティを推進するメルカリグループ メルカリグループは、ダイバーシティ&インクルージョンに価値を置いており、多様なバックグラウンドを持つメンバーの経験・知識・意見を結集し、一人ひとりがバリューを発揮できる組織を目指しています。 参考: Diversity & Inclusion Statement 今回は私たちPayment Coreチームが、どのように言語の壁を乗り越えダイバーシティ&インクルージョンを推進しやすい環境を作ったかを紹介します。 Payment Coreチームについて 私たちPayment Coreチームの責務は「決済基盤としてプロダクトチームに決済機能を提供し、プロダクト・サービスのミッション達成を実現する」です。 2023年12月時点では、図のようにプロダクトに機能を提供しています。 より詳細に決済基盤について知りたい方は以下の記事を参照してください マイクロサービスにおける決済トランザクション管理 – メルコイン決済基盤の実践話 そしてPayment Coreチームは、多数の国籍を持つメンバーで構成されており、母国語が日本語でないメンバーが約半数を占めています。この多様なメンバーシップは、私たちのチームの力を高める一方で、日々の開発業務においてもコミュニケーションの課題が出てきました。 高くて厚い言語の壁 PaymentCoreチームでは異なる言語を話す人々が集まった際に、意思疎通を図る際に生じる障壁のことを 言語の壁(Language barrier) と呼び、具体的には以下のような問題が発生しました。 言語や文化の違いによるコミュニケーションの課題 異なる国籍を持つメンバーが集まるチームでは、母国語が異なるため、コミュニケーションにおいて言語による壁が生じる可能性があります。それによりお互いの意思疎通が上手くいかないことによる認識の齟齬や、それによるパフォーマンスの低下に繋がる課題が潜在的に存在していました。 また、言語の違いだけではなく、コミュニケーションのスタイルや表現の違いにより意図が正確に伝わらず誤解が生じることもあります。 例えば、日本語のコミュニケーションでは間接的に意見を表現することがあると思います。これは日本語が母国語ではない人にとって意図を正確に理解することが難しくなります。 技術用語や業界特有の言葉の理解の困難 開発業務には特定の技術用語や業界特有の言葉が使用されることがありますが、言語の違いにより、それらの言葉の理解が困難になる可能性があります。特に決済に関連する法律用語や専門用語は代表的な例です。 例えば、決済ドメイン中には「法定帳簿」「資金決済法」「管理会計」「オーソリ」「あと払い」等、英語話者にとって理解が難しい言葉に対して用語が統一されていない場合には、コミュニケーションコストが増大します。英語学習中の日本語話者にとっても同様です。決済の文脈の中で登場する「Payment」「Transaction」「Settlment」「Topup」「Payout」等の用語を正しく区別して理解するのは非常に困難です。 言語の壁に直面して 実際、私もこれらの課題を非常に痛感しました。私はmerpayの英語環境を理解した上で入社しましたが、最初の頃はミーティングでの英語の聞き取りがうまくできず、また、自分の意思を英語で表現することもできずに困ることがありました。当時のスピーキングスキルはほんの簡単な自己紹介がやっとで、非常にチャレンジングな環境でした。 そんな私のような英語学習者を含んだPaymentCoreチームがどのように言語の壁と付き合っているのかを紹介します。 言語の壁との付き合い方 まず、私たちチームでは英語を主体としてコミュニケーションしていますが、ポリシーとして 特定の言語をメンバーに強制することはせず、それぞれの言語でもパフォーマンスが出せることを理想 としています。 その実現のためにメルカリに存在するさまざまなサポートを活用しながら日々の業務に当たっています。 細かいツールやtipsの話は沢山ありますが、今回は以下の4つを紹介したいと思います。 Global Operation Teamによる通訳・翻訳のサポート 言語の壁の中で最も苦労したのは、リアルタイムのコミュニケーションでした。オンライン会議ツールの字幕機能などもありますが、不慣れなメンバーにとってリスニングとスピーキングは最初の大きな壁でした。それを解決してくれたのはGlobal Operation Team(以下、GOT)です。 メルカリグループにはGOTというチームが存在します。GOTは主に翻訳と通訳の二つの職務を担当してくれているチームであり、私たちのチームは主に通訳でのサポートをしていただいております。GOTのおかげで言語が異なる場合でも会議中のコミュニケーションの橋渡しを実現してくれております。 参考: 言語を活用してメルカリのビジネスやD&Iをサポート!──Global Operations Teamが提供する通訳・翻訳業務以上の価値 Slack上でのコミュニケーションの自動翻訳 リアルタイムではないコミュニケーションだとしても問題は存在していました。Slack上で複数の言語(日本語と英語)でコミュニケーションを行う場合、メンバーによっては都度翻訳ツールを利用する必要があり、コミュニケーションに小さなストレスが生じることがありました。 そのため、私たちはZapierを用いた自動翻訳ツール(JP <-> EN)を導入しました。 Zapierは複数のアプリ(Webアプリケーション)を連携させてワークフローを作り、業務を自動化させることができるツールです。 WebUI上からアカウント連携・ワークフロー作成ができるため、ノンプログラマーでも簡単に使うことができます。 https://zapier.com/ このツールを利用することで、言語を自動的に判定し、翻訳結果をSlackのThreadに投稿することが可能です。これにより、どちらの言語でも気軽に投稿できるようになり、事前に翻訳を用意したり、母国語以外のコミュニケーションへのハードルを下げることができるようになりました。 以下のように自動的に翻訳が投稿されます。 Zapierでは複数のアプリを連携させて作ったワークフローの単位を「Zap」と呼びます。 実際に今回のZapを簡単にご紹介します。 Slackの投稿をトリガーする(図内 1) 翻訳対象の選別のために投稿のフィルタリングを行う(図内 2,3) 言語の特定(図内4) 翻訳(図内 7, 11) Slackへ投稿(図内 9,13) まだまだフィルタリング機能が不十分な点等改善点はありますが、現在社内の複数のチャンネルで利用されています。 言語学習プログラム GOTによる翻訳サポートなど、さまざまなサポートがメルカリグループには存在していますが、メンバー自身の言語スキルが向上しなければパフォーマンスを向上していくことは困難です。メルカリグループでは業務の必要性に応じて言語学習プログラムに参加することができます。私を含めた一部Payment Coreチームメンバーは外部のオンライン英会話練習プログラムや社内の言語学習プログラム(日本語/英語)を受講し、それぞれの言語に対しての習熟度/理解度を高めています。 チーム内のミーティングにおいても週一で日本語でコミュニケーションする日を作る等、日々学習プログラムを通してInputしたものをOutputする機会もチームの中に存在しています。 チームメンバー同士の文化の理解・尊重 異なる国籍を持つメンバー同士がお互いの文化を理解し、尊重することも重要です。 お互いに完璧な英語や日本語を話せるようになることを求めるのではなく、お互いのことを理解するというコミュニケーションの本質を大切にし相手に合わせてコミュニケーションできるように努めることが私たちの考え方です。 例えば、意識的に「英語学習者にとって難しい英語」や「日本語学習者にとって難しい日本語」を使わずにコミュニケーションする事はとても有益な方法です。 参考: やさしいコミュニケーション Payment Coreチームの成長と変化 上記サポートを活用する事で結果として、Payment Coreチームではいくつか変化が起こりました。 言語習熟度の成長 日々のコミュニケーション + 言語学習プログラムの結果、Payment CoreチームのCEFRの定義(後述)に基づいた言語習熟度が格段に向上しました。 私自身も英語に関してA2レベル(サポートがあれば会話ができる)からB2レベル(自分の仕事に関する会話が支障なくできる)まで向上しました。もちろんペラペラに話せるわけではないですが、簡単な自己紹介が出来る程度のレベルからSlack上、 オンライン、 オフラインの場で普段の業務に関するトピックに関して英語でなんとかコミュニケーションする事ができるレベルまで成長する事ができました。 CEFR定義について(引用元: https://careers.mercari.com/jp/language/ ) レベル 定義 英語または日本語を使ってできること Basic (CEFR – A2) – 会話する相手のサポートや、より簡単な言葉への言い換えがあれば、自分の専門分野において、基本的なやりとりができる – マネージャーと1-on-1ミーティングをする際、相手からサポートしてもらいながらミーティングすることができる – 相手からサポートしてもらいながら、同僚と1対1で仕事に関する簡単な意見交換や雑談ができる Independent (CEFR – B2) – 会話する相手からのサポートや、より簡単な言葉への言い換えがほぼなくても、自分の専門分野において複雑な情報のやりとりができる – 言語がコミュニケーションの妨げになることなく、1-on-1ミーティングができる – 自分の専門分野において、母語話者を含む複数名での議論に参加することができる Proficient (CEFR – C1) – 会話する相手からのサポートや、より簡単な言葉への言い換えがなくても、自分の専門分野内外の複雑な情報のやりとりを自立して行うことができる – 抽象的な話題や不慣れな分野でも、複数名の議論に参加できる ※CEFRに関する詳しい情報は こちら (外部リンク:Council of Europe) 円滑で迅速なコミュニケーション チームの言語習熟度が向上した事でGOTによる通訳サポートが不要になりました。 これにより、緊急もしくは即席のミーティングを通訳サポートなしで開催できる点がチームのコミュニケーションスピードを向上させる事につながりました。 さらに、同僚と気軽にちょっと話したい時にすぐ会話できる点・自身の言いたいことを表現できる点はチームの雰囲気自体を明るくする事にもつながりました。具体的には、会話中の沈黙はほとんどなくなりました。もし会話がわからなければ、わかる箇所からブレイクダウンしてコミュニケーションしていく方法を多くのメンバーがチームの成長と共に学んでいきました。 また、チーム内だけではなく、チーム外のコミュニケーションとしても英語/日本語を使えるようになる事でチームとしての可能性も広がりました。 メンバーのキャリア創出の可能性 メンバーそれぞれの言語習熟度の向上によって、社内以外での活動にもつながりました。 例えば、一部チームメンバーは海外のカンファレンスに参加し、そこで得たInputをコミュニティで発表する等、活躍の場を広げてくれています。 (GopherCon 2023 in San Diego) Shunta KomatsuさんによるGoコミュニティへの貢献 https://speakerdeck.com/iamshunta/recap-the-future-of-json-in-go このように外国語のスキルを磨くことはチームのパフォーマンスを上げるだけではなく、メンバーの将来におけるキャリアの幅を広げていく事にもつながる可能性があると考えています。 変化は簡単には起こらない このような変化が起こりましたが、もちろん容易にかつ即座に達成したわけではありません。PaymentCoreチームは半年以上もの時間を費やしてきました。そしてまだまだ理想的な環境とは言えない状況です。今後も包括的な環境を構築し続けることが不可欠です。 ダイバーシティの力を活かして 言語の壁は私たちにとって挑戦でありながら、同時に成長の機会でもあります。異なる言語や文化を持つメンバーが集まることで、さまざまなアイデアや視点が生まれ、よりクリエイティブな問題解決が可能になります。 私たちのチームは、言語の壁を乗り越えるための努力を惜しまず、お互いを尊重しながら協力しています。そして、多様なバックグラウンドを活かし、より良いプロダクトを提供するためにこれからも取り組み続けていきます。 明日の記事は @ntkさんです。引き続きお楽しみください。
アバター
こんにちは。メルカリ iOSエンジニアの @sae です。この記事は、 Mercari Advent Calendar 2023 の11日目の記事です。 私は株式会社メルカリに入社してから早6ヶ月が経ちましたが、日々の業務を通じて、さまざまな技術の素晴らしさに感銘を受けています。 その中でも特に驚くべきことは、大多数のiOSエンジニアが在籍している大規模なチームが、一つのプロジェクトに携わりながら、円滑に開発が進んでいることです。これまでに私は6つの企業で働いてきましたが、どの組織も最大でも5人のiOSエンジニアがアプリ開発に関与しており、プロジェクトファイルやXcodeのバージョンなどの問題がある場合でも、直接のコミュニケーションを通じて解決してきました。 果たして、メルカリは大規模なiOSエンジニアチームが円滑な開発を行うためにどのような取り組みをしているのでしょうか。私が感銘を受けた様々な観点をTipsとしてご紹介したいと思います。 マイクロモジュール化 現在、メルカリ iOSアプリには数百以上の非常に多数のモジュールが存在しています。各画面ごとにモジュールが独立しており、依存関係なしに動作します。さらに、各機能も1機能につき1モジュールとなっており、複雑なロジックは複数のモジュールから成り立っています。 このようなマルチモジュールアーキテクチャにより、依存関係が明確になり、他のチームへの影響度も把握しやすくなっています。マルチモジュールアーキテクチャは、さまざまな現場で実施されている手法ですが、メルカリでは徹底的に細分化されているため、大規模な開発チームにおいて、そのメリットがより明確に実感できます。 さらに、 メルカリ iOSアプリではBazelを使用 しています。Bazelは更新のないモジュールを再ビルドする必要がないため、効率的な開発を支援しています。また、既に成功したテストも再度実行する必要がないため、検証のスピードも向上しています。さらに、一度ビルドしたモジュールは他の開発者が再ビルドする必要がないため、リソースの無駄を防ぎます。 マルチモジュールアーキテクチャとBazelの積極的なキャッシュ機能により、メルカリのiOSエンジニアは大規模な開発プロジェクトを円滑に進めることができており、チーム全体の生産性向上に貢献しています。 ただし少人数の開発チームや変更の多いスタートアッププロジェクトでは、必ずしも生産性が向上するとは限りません。マルチモジュール化は管理コストが増加する傾向がありますので、大規模な開発プロジェクトならではの非常に大きな恩恵を実感しました。 コードオーナーによる品質管理 メルカリでは、Ready for ReviewにPRを設定すると、自動的に適切なレビュワーがコードオーナーの設定に基づいて割り当てられ、レビュー作業を委ねることができます。もしPRがアーキテクチャの変更やグループ企業の機能に広範な影響を及ぼす場合は、専任のArchitectチームが影響を確認します。 メルカリは大規模な組織ですが、各担当箇所に責任を持つコードオーナーが存在し、品質管理を徹底することで、高品質なコードの提供と開発プロセスの円滑化を実現しています。コードオーナーからの適切なフィードバックは、開発者に貴重な指摘や改善アドバイスを提供し、より高水準な開発を促進します。また、コードオーナーの存在は他のチームとの連携をスムーズに行い、プロジェクト全体の一貫性と効率性を向上させることができます。この取り組みにより、大規模な開発環境でもチームワークと品質管理を重視し、優れた開発成果を生み出しています。 小規模なチームでは、通常1人がテクニカルリードを担当しますが、例え少人数でも全員が個々の機能についてコードオーナーであり、責任を持つ仕組みは様々な現場で有効だと感じます。 トランクベース開発 メルカリでは、トランクベース開発という手法を採用しています。この開発手法では、開発者がプロジェクトごとにフィーチャーブランチを作成して機能を追加するのではなく、機能ごとにメインブランチである「トランクブランチ」に対してPull-Requestを作成していきます。そのため、機能ごとのPull-Requestは明確な内容となり、レビュー時間の短縮や他のブランチとの衝突の回避が可能となります。 トランクブランチは常にリリース可能な状態を保つため、開発中の機能は フィーチャーフラグを使用 して非表示にし、ユーザーに早期に公開されないようにします。そのためには、回帰テストを充実させることが重要です。十分な自動テストを実施することで、堅牢なトランクブランチを維持し、開発者は既存の機能への影響を考慮しながら新しい機能をトランクブランチにマージすることができます。 この開発手法により、コードの品質と安定性を保ことができ、メルカリのiOSエンジニアは迅速かつ効率的に開発を進めることができます。また、フィーチャーフラグを使用して機能をリモートで制御するため、公開されているアプリに問題があった場合でも、次のバージョンのリリースを待たずに機能を無効化することができるなど、様々な恩恵があります。 FormatterやLinterによる確認の自動化 メルカリでは、 Danger を使用して自動的にコード修正を行います。FormatterやLinterによる確認は、新しい開発者がプロジェクトに参加する際に役立ちます。新しい開発者はメルカリのコードベースにすばやく適応することができ、コードの統一性を保つことができます。 また、開発者は記述手法の統一を心配する必要がなくなり、より高度な開発タスクに集中することができます。これにより、開発者はより効率的にプロジェクトを進めることができ、最終的には高品質なアプリケーションを提供することができます。 まとめ 今回ご紹介した手法は、メルカリのiOS開発における一部に過ぎませんが、非常に多くの効率化の仕組みや自動化のテスト、そして多くの優秀なエンジニアのアウトプットに触れる日々は、私にとって非常に刺激的でワクワクが止まりません。 今回の記事では、メルカリの先輩エンジニアたちが築いてきたノウハウや開発手法を、大人数での開発の観点で、様々な方々に参考になるようにまとめさせていただきました。 引き続き来年のAdvent Calendarに向けて、私自身がメルカリで挑戦した記録を記事にしていけるよう頑張りますので、どうぞお楽しみにしていてください。メルカリでの成長や新たなチャレンジについて、皆さんに共有できることを心から楽しみにしています。
アバター
こんにちは!QA Engineerの @fukutomi です。 この記事は、 Merpay Advent Calendar 2023 の11日目の記事です! メルカリエンジニアリングブログに寄稿するのは初めてなので緊張しますが、よろしくお願いします。 はじめに(この記事はなんなのか) 今回のテーマは、弊社が運営している パ・リーグ Exciting Moments β (略してPEM)におけるログイン処理をテスト自動化してみよう、です。 ※パ・リーグ Exciting Moments βとは 「パ・リーグ Exciting Moments β」は、パ・リーグ6球団の記憶に残る名場面やメモリアルシーンを捉えた動画コンテンツを自分だけのコレクションとして保有できるパ・リーグ6球団公式のサービスです。 PEMはログインしないと大抵の機能が利用できず、テスト自動化をしたいならログイン処理の突破は必須。。。 後述する通りPEMのログイン処理は結構複雑なのですが、気合&パワーでなんとか実装したので、よかったら見てやってください。 PEMのログイン構造 最初にPEMのログイン処理について簡単に説明します。 PEMはE-mailとSMSの2要素認証(2FA)を採用しています。 お客さまが行う作業としては ログイン画面でE-mailアドレス入力 サービスからメールが届くので、メール内のリンクを開く リンクを開くと登録されている電話番号にSMSが届く 同時にSMS認証番号入力画面を開くので、SMSに記載されている認証番号を入力 (場合によってはここでreCAPTHA認証が入りますが、テスト環境では表示しない設定にしているので割愛) ログイン完了! こんな形で結構複雑でして、今回はこれをCypress+Gmail APIですべて自動化しよう、という話です。 どんな仕組みで自動化するのか 今回はPEMを自動で動かすツールとしてCypressを、GmailにアクセスするためにGmail APIを、またテスト用電話番号の準備のためFirebaseを利用します。 上記ログイン構造のお客さまが行う作業をもとに、下記の感じで自動化してみます。 CypressでPEMのログイン画面を開く ログイン画面でE-mailアドレスを入力して送信 サービスからメールが届くので、GoogleにログインしGmail APIを利用してメールを検索 メール本文からログイン用のリンクを抜き出す 抜き出したリンクをCypressで開く SMS認証番号入力画面に遷移するので、あらかじめFirebaseで設定しておいたテスト用電話番号の確認コードを入力 ログイン完了! それでは実際にやってみましょう! 下準備 Cypressのインストール まずは下記を参照にCypressをインストールしましょう。 ※Cypressとは ウェブアプリケーションをフロントエンドで自動で動かすことができる、オープンソースソフトウェアのテストツールです。 (詳細は本題から逸れちゃうので割愛します) Installing Cypress – Cypress.io Opening the App – Cypress.io GCPの準備 次にGCPのプロジェクトを作成します。 こちらも本題から逸れるので割愛! プロジェクトの作成と管理 – Google Cloud Gmail APIの準備とid,secretの確認 GCPのプロジェクトを作成したら、次はGmail APIを準備します。 サイドメニューから「APIとサービス」を選択、画面遷移 「+APIとサービスの有効化」を押下し、ライブラリへ 「Gmail API」で検索し、APIの詳細画面へ 有効化 (出典:Google Cloud Platform) Gmail APIを有効化できたら認証情報を作成します。 API管理画面を開き、認証情報タブを選択 「+認証情報を作成」を押下、OAuth クライアント IDを選択 作成画面に遷移するので、下記の情報を入力して作成 入力する内容はこんな感じ。 アプリケーションの種類 ウェブアプリケーション 承認済みのリダイレクトURI https://developers.google.com/oauthplayground http://localhost:3000 (出典:Google Cloud Platform) 作成完了後、詳細画面を開くとAdditional informationエリアが表示されます。 「クライアント ID」「クライアント シークレット」をあとで利用します。 (出典:Google Cloud Platform) Firebaseの準備(テスト用電話番号の準備) PEMのログイン情報はFirebaseで管理しています。 FirebaseのAuthenticationでは、テストで使用できる電話番号ならびに確認コードをセットすることができるので、そちらを登録しておきます。 登録したテスト電話番号と確認コードはあとで利用するのでメモしておくとよいでしょう。 (出典:Firebase) リフレッシュトークンの発行 (参照: Google Authentication – Cypress.io ) Googleにログインするためにリフレッシュトークンを発行します。 Google Developpers OAuth 2.0 Playground にアクセスして、リフレッシュトークンを発行しましょう。 まず事前設定として、上記のGmail API認証情報をセットします。 右上の歯車マークから設定可能です。 「Use your own OAuth credentials」にチェックを入れると認証情報の入力欄が表示されます。 (出典:Google Developpers OAuth 2.0 Playground) それが終わったらScopeを選択してAuthorizeします。 自分はこんな感じで設定しました。 Scope https://www.googleapis.com/auth/gmail.readonly https://mail.google.com/ AuthorizeするとAuthorization codeが表示されます。 今回用があるのはリフレッシュトークンなので、「Exchange authorization code for tokens」を押下してリフレッシュトークンを生成してください。 (出典:Google Developpers OAuth 2.0 Playground) さて、これで事前準備が整いました。 ここからは実際に自動テストのコーディングに入っていきます。 コーディング まずは環境変数をCypress.env.jsonに定義しておきましょう。 セキュリティ的な観点でも、上記のトークンとかはベタ書きするわけにはいかないですからね! { "google_client_id": "xxxxxxxxxx", "google_client_secret": "yyyyyyyyyy", "google_refresh_token": "zzzzzzzzzzzzzzz", "sign_in_email": "hogehoge@mercari.com", "test_phone": "07000000000", "test_phone_sms": "123456", "from_email": "hogehoge" } 次はほんとにログイン処理を書いていきましょう。 まずはログイン画面に遷移して、メールアドレスを入力します。 it("ログインページに遷移、メールアドレスを入力して送信", () => { // ログインページに遷移 cy.visit("/signin/"); // メールアドレスで登録画面に遷移 cy.contains("メールアドレスでログイン").click(); // ログインページにいることを確認 cy.contains("h1", "ログイン").should("be.visible"); // メールアドレス入力 cy.get("input[name=email]").type(Cypress.env("sign_in_email")); // フォームを送信 cy.get("form").submit(); // メッセージ確認 cy.contains("メールをチェックしてください").should("be.visible"); // メールが来るまでちょっと待つ(ほんとはメールが来るのをキャッチしたい cy.wait(15000); }); メールアドレス送信後、メールが届くまでちょっと待って、メール内からリンクを引っ張ってアクセスする作業に入ります。 it("受け取ったメールからリンクを読み取ってアクセス", () => { //Googleへのアクセストークンを生成する cy.request({ method: "POST", url: "https://www.googleapis.com/oauth2/v4/token", body: { grant_type: "refresh_token", client_id: Cypress.env("google_client_id"), client_secret: Cypress.env("google_client_secret"), refresh_token: Cypress.env("google_refresh_token"), }, }).then(({ body }) => { const access_token = body.access_token; // 件名にサインインを含む、未読、Toがログインメールアドレスになっているメールを1件だけ抽出 cy.request({ method: "GET", url: "https://content-gmail.googleapis.com/gmail/v1/users/me/messages", headers: { Authorization: `Bearer ${access_token}`, }, qs: { q: `from:${Cypress.env("from_email")} subject:サインイン is:unread to:${Cypress.env("sign_in_email")}`, maxResults: 1, }, }).then(({ body }) => { const mailID = body.messages[0].id; // 取得したメールIDをもとにメールの詳細を取得する cy.request({ method: "GET", url: `https://content-gmail.googleapis.com/gmail/v1/users/me/messages/${mailID}`, headers: { Authorization: `Bearer ${access_token}`, }, }).then(({ body }) => { // 取得したメール詳細をデコードしつつ本文を抜きだす var mailBody = decodeURIComponent( escape( atob( body.payload.parts[1].body.data .replace(/-/g, "+") .replace(/_/g, "/") ) ) ); // URLを囲むコーテーションがシングルだったりダブルだったりするので、ダブルに統一 mailBody = mailBody.replace(/'/g, '"'); // 文中最初のURLだけを抽出する const accessUrl = mailBody .substring(mailBody.indexOf("http"), mailBody.indexOf('">')) .trim(); // 抽出したURLにvisit cy.visit(accessUrl); }); }); }); }); リンクにアクセスすると電話番号入力画面に遷移するので、予め設定しておいたテスト電話番号と確認コードを入力し、ログイン完了!というわけですね。 it("電話番号を入力してログイン完了", () => { // 描画が完了し、画面がSMS認証番号入力に切り替わるまで待つ cy.wait(5000); // SMS認証番号入力画面に切り替わったことを確認 cy.contains("電話番号に届いた6桁の確認コードを入力してください").should( "be.visible" ); // SMS暗証番号入力 cy.contains("電話番号に届いた6桁の確認コードを入力してください") .parent("form") .within(($form) => { cy.get('input[name="verificationCode"]').type(Cypress.env("test_phone_sms")); // 続行する! cy.contains("送信する").click(); }); }); 実際の動作 (出典:Cypress(左)、パ・リーグ Exciting Moments β(右)) あとがき いかがだったでしょうか。 自分で言うのもなんですが、すんごい力業だったと思います。 まあでも、ログイン処理が自動化できたことでその後のMoment購入処理やマイページのテストを自動化することができました。 可読性や保守性ももちろん大事なんですが、目的を果たすことが第一ということで。 ちなみに今回はCypressを利用しましたが、別の他のツールでもできると思うのでよかったら試してみてください。 さて、パ・リーグ Exciting Moments βは2024年3月31日をもってサービス終了することになりました。 あと少しではありますが、パ・リーグ Exciting Moments βのことをよろしくお願いします。 以上です! 明日はLiuさんが担当します。お楽しみに!
アバター
はじめに こんにちは。メルペイでBackend Engineerをしている Ryu Yamadaです。この記事は、 Merpay Advent Calendar 2023 の10日目の記事です。 2022年4月に新卒で入社してから、メルペイの加盟店管理や加盟店精算を行うサービスの開発に携わっています。 2023年のハイライトは何と言ってもインボイス制度です。この記事を読んでいるみなさんも、経費精算などで大変な思いをしているのではないでしょうか。この記事では、メルペイの加盟店精算におけるインボイス対応について振り返ります。 ざっくり加盟店精算 メルペイでは月に1回や2回などの決められた精算サイクルごとに加盟店に対して発生した売上を精算して入金しています。そして、加盟店に提供している管理画面から入金の詳細をCSVファイルとしてダウンロードできるようにしています。 入金詳細ファイルには、売上金額、日次、売上のあった店舗情報や決済手数料などが記載されていて、各行が一つの取引に対応しています。 インボイス対応 さて、2023年10月1日からインボイス制度が始まりました。 メルペイがインボイス対応をしないと加盟店がメルペイを通して決済した代金の消費税を控除できなくなってしまうため、以下の対応が必要となりました。 メルペイの適格請求書発行事業者としての登録 メルペイが発行する入金詳細ファイルに消費税額やメルペイの登録番号等を記載し、適格請求書にする メルペイが発行した請求書を7年間保存する 上2つの対応は軽微だったものの、請求書を長期間に渡って保存する要件へどう対応するかは検討する必要がありました。 加盟店情報の履歴テーブル メルペイでは月1回などのサイクルで精算を行っていますが、入金詳細ファイルの作成は加盟店の管理画面からの請求をトリガーにして行っていました。また、事業者名や店舗名の変更履歴を保持する仕組みがなかったため、入金詳細ファイルの請求が行われた時点での値を記載していました。 しかしこの方式では、例えば5年前の入金詳細ファイルを請求された場合に、5年の間に事業者名や店舗名の変更があると正しくない請求書が作成されてしまう問題がありました。 そこで、インボイス対応として事業者名や店舗名の変更履歴を保存する履歴テーブルが必要になりました。 Spanner Change Streamを選択 加盟店の情報を保存するテーブルのスキーマのイメージは以下のとおりです。 CREATE TABLE Partners ( PartnerID INT64 NOT NULL, Name STRING(MAX) NOT NULL, // 事業者名 // ・・・住所等・・・ UpdatedAt INT64 NOT NULL, // Unixtime CreatedAt INT64 NOT NULL, // Unixtime ) PRIMARY KEY(PartnerID); この変更を保持する履歴テーブルのスキーマはこのようになります。 CREATE TABLE PartnerHistories ( PartnerID INT64 NOT NULL, Name STRING(MAX) NOT NULL, // 事業者名 // ・・・住所等・・・ UpdatedAt INT64 NOT NULL, // Unixtime CreatedAt INT64 NOT NULL, // Unixtime HistoryCreatedAt TIMESTAMP NOT NULL, // Timestamp 履歴作成時刻 ) PRIMARY KEY(PartnerID, HistoryCreatedAt); 今回、Partnersテーブルに変更があったときに履歴テーブルであるPartnerHistoriesテーブルへの書き込みを行う方法を2通り検討しました。 アプリケーションで元(Partners)テーブルのレコードを挿入や更新した場合に履歴(PartnerHistories)テーブルへの書きこみも行う方法 Spanner Change Streamを利用し、DBレベルで、元(Partners)テーブルの変更をトリガーに履歴(PartnerHistories)テーブルへの書き込みを行う方法 さらに、インボイス対応にあたっては、Partnersテーブルだけではなく他のいくつかのテーブルにも履歴の作成が必要でした。 前者のロジックを作り込む方法では、元(Partners等)テーブルに書き込みを行うロジックすべての修正を行う必要があり修正範囲が広いこと、将来元テーブルを操作するようなロジックを追加する際に履歴テーブルへの書き込みを忘れると影響範囲がかなり大きくなってしまうことなどがネックでした。 後者のSpanner Change Streamを使う方法では、ロジックの改修から独立したDBレベルの機能として実現できること。また、加盟店精算ではメルペイ内で精算してから実際に入金を行うまでに数日以上開くため、履歴テーブルの要件として元テーブルと履歴テーブルの書き込みを同じトランザクションで行うことが求められなかったこともあり、最終的にこちらの方法を選択することにしました。 Spanner Change Streamで履歴テーブル構築 Dataflowを通してSpanner Change Streamを利用しました。 メルペイではこれまでDBのバックアップ用途などでは利用実績がありましたが、プロダクトでの利用は初めてでした。 履歴テーブルの作成に当たっては、元テーブルへの挿入(INSERT)と更新(UPDATE)の両方が履歴テーブルに対しては挿入としなくてはならない点に注意が必要でした。 ハマった点 最も困難だった点は、元テーブルのUpdatedAtがUnixtimeであったことでした。 元テーブルにUnixtimeの最小単位である1秒以内に複数の変更が行われた場合に、履歴テーブルにはUpdatedAtが同一の複数のレコードが挿入されますが、どのレコードが元テーブルの最終的な状態と一致しているかがわからない点が問題でした。 この例ではIDが1のレコードに対して1秒以内に2回更新をしています。挿入順序とHistoryCreatedAtの順序は必ずしも一致しないので、履歴テーブルからは”メルペイ2”と”メルペイ3”のどちらが最新の履歴なのかがわかりません。 この問題を解決するために暫定対応として以下のアプローチを取りました。 履歴テーブルのUpdatedAtにUnique Key制約をかけて、1秒以内に複数の変更があった場合には2つ目移行の挿入を失敗にする 履歴テーブルへの挿入失敗を監視するアラートを設定し、発生時には手動で確認する インボイス制度の施行が迫っていたため暫定的な対応となりましたが、加盟店情報が短時間に複数回更新されることが少ないため、この対応でクリティカルな問題は起きていません。 恒久的な対応としてUpdatedAtのUnixtimeからTimestampへのマイグレーションを予定しています。 おわりに ニュースでインボイス対応という言葉を知ったときには、経理ではない自分にはあまり関係がないだろうと思っていましたが、当事者として対応することになりました。 メルペイが成長してきた中で返しきれていない、UpdatedAtの型といった負債にも苦しみましたが、インボイス対応を完了することができました。今後もメルペイと加盟店をなめらかにつなぐプロダクトを作っていきたいです。 明日の記事は @fukutomiさんです。引き続きお楽しみください。
アバター