こんにちは。メルペイ Engineering Engagement チームの mikichin です。 10月5日に開催された「 YAPC::Hakodate 2024 」にメルカリはGold Sponsorをしておりました。今回は参加レポートをお届けします! YAPC::Hakodate 2024 について YAPCはYet Another Perl Conferenceの略で、Perlを軸としたITに関わる全ての人のためのカンファレンスです。 Perlだけにとどまらない技術者たちが、好きな技術の話をし交流するカンファレンスで、技術者であれば誰でも楽しめるお祭りです! 開催概要 開催日時 前夜祭:2024年10月4日(金) 本編:2024年10月5日(土) 場所 公立はこだて未来大学 ブラインドに「YAPC」とあり、いよいよはじまるんだなーとワクワク。素敵な演出をありがとうございます! メルカリメンバーの登壇 今回、前夜祭・本編でメルカリメンバーの登壇がありました! デジタルIDウォレットが切り開くHigh Assurance Identity Proofingの未来 / kokukuma 今回のYAPC::Hakodate 2024本編では、非常に多くのプロポーザルの応募があったということで、前夜祭にてrejectconが開催されkokukumaが登壇しました! デジタルIDウォレットを利用できる環境が整備されつつある現状を振り返りつつ、開発者として今後この状況を楽しみ尽くすための知識として、ユーザー体験の概略や、mDL/mdoc(ISO/IEC 18013-5)について解説し、デジタルIDウォレットによる身元確認が、なぜ身元確認として成立するのかを解説しました。また、これから出てきてほしい5大ニュースを6つ紹介しました。 https://speakerdeck.com/kokukuma/dezitaruiduoretutogaqie-rikai-kuhigh-assurance-identity-proofingnowei-lai フロントエンドの現在地とこれから / koba04 本編14:50〜 からkoba04が登壇しました。 「フロントエンドの現在とこれから」というタイトルで Server Components などの技術がなぜ登場しどういった問題を解決しようとしているのかについて取り上げ、フロントエンドの今とこれからについてを「点ではなく線」で捉えられるようまとめ、発表しました。 フロントエンドエンジニアで普段触れている技術を俯瞰的に捉えたい方や、フロントエンドエンジニアではないけどフロントエンドの最近の技術や変化を把握したい方は、ぜひ資料および後日公開される動画をご確認ください。 https://speakerdeck.com/koba04/hurontoentonoxian-zai-di-tokorekara 参加メンバーの感想 今回、多くのメルカリメンバーが参加していました。参加した感想をご紹介します。 YAPC函館市電LTの様子 普段はAndroidアプリの開発を主にしていますが、そんな自分でも受け入れてくれるワイワイなコミュニティ・イベントで楽しめました! 「CloudNative Meets WebAssembly: Wasm’s Potential to Replace Containers」のセッションは知らない単語が色々出てきて特にまなびになりました〜✨ イベント翌日に「 YAPC函館市電LT 」が開催されていたので、路面電車についてLTもしました🙌 (Kuu) さまざまな分野の話を1日で聞けて、満足度が高かったです。特に「クレジットカードを製造する技術」が個人的には面白いと感じました。クレジットカードという特に秘匿性の高さが求められるものの製造工程は普段あまり聞けない話だったので興味深かったです!(momom) 初めて技術のカンファレンスに参加しましたが、興味深いセッションをたくさん聞けたり、いろいろな人とお話しできて良い機会になりました!最後のmoznionさんのkeynoteの「量が質に転化する」という言葉に感銘を受け、帰ったらたくさんプログラム書いてこうという気持ちになりました🔥 企業のスポンサーブースもどの企業も工夫されてて全部回ってしまいました!コーヒースポンサーをされていたカケハシ社のコーヒーが美味しかったです☕︎ (torichan) 「今日から始める大規模言語モデルのプロダクト活用」のセッションは、業務デザイン、ナレッジなど自分自身がとても興味のある領域なので、ワクワクしました。また、LT ではクリエイティブコーディングの話が刺さったので、早速やっていくぞ〜という気持ちです。(micchie) まとめ わたしは「YAPC::Okinawa 2018 ONNASON」以来の久しぶりの参加となりました!そのときも沖縄科学技術大学院大学 OISTという大学が会場だったこともあり、個人的にすごくなつかしさを感じ、YAPCに戻ってきたなーという思いでいっぱいでした。 前夜祭で行われたアンカンファレンスだけではなく、どのセッションでも登壇者と参加者がコミュニケーションをとりながら意見交換をしており、全員で楽しみ、盛り上がっているカンファレンスであることを感じました。 最後に、YAPC::Hakodate 2024の企画運営、おつかれさま & ありがとうございました! また、次回を楽しみにしています!
こんにちは。株式会社メルカリ iOSエンジニアの kntk です。 8月22日から8月24日にかけて開催された「 iOSDC Japan 2024 」にメルカリはプラチナスポンサーとして参加しました。 本記事では、その参加レポートをお届けします! 登壇 株式会社メルカリからは私を含め2名のエンジニアが登壇しました。 SwiftのSIMDとその利用方法 (レギュラートーク 20分): kntk 座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか?」 (企画 40分): kntk (他4名) App Intentsの未来について研究しよう! (ポスターセッション): jollyjoester SwiftのSIMDとその利用方法 (レギュラートーク 20分): kntk CPUにはSIMD命令と呼ばれる高速演算機能が存在し、それを扱う為の型 SIMDが標準ライブラリに存在します。この型の全体像や使い方についてのセッションです。 実はこのSIMD型はvisionOS開発で用いるRealityKitで必要になることがあり 、Apple Vision Proが発表され少しずつvisionOS開発が始まっている今年が絶好の発表タイミングだと考えプロポーザルを提出しました。 visionOSで必要になるとはいえ、SIMD命令自体の説明という低レイヤ技術の話が含まれているセッションだったため、「あまり人は来てくれないのではないか」と考えていましたが、 想像していたよりも多くの方々が聞きに来てくださり驚きました。 visionOS開発をする際に、少しでも参考になれば良いなと思います。 座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか?」 (企画 40分): kntk (他4名) Swiftの次のメジャーバージョンであるSwift 6から導入される「Swift ConcurrencyのStrict Concurrencyチェックによる完全なデータ競合の防止」についての座談会企画です。座談会形式で私含め5名の登壇者がコメントし合う形でセッションが進行しました。 自分は個人アプリで経験した「Strict Concurrency対応をしたら、長らく原因が謎だったクラッシュが解消された」というエピソードを話しました。 データ競合の最も厄介なポイントは再現やデバッグ(そもそもデータ競合の存在に気づくこと)が難しい点にある と考えているため、静的にデータ競合を防止するStrict Concurrencyチェックは大きな意味があると思っています。 また、もう一つ 「Swiftチームも全てのデータ保護をActorで実現することを目指していない」 という大事なメッセージを伝えました。 Strict Concurrencyの導入ハードルを高く感じる大きな要因の一つとして「Strict Concurrency以前から存在したLock(やQueue)の仕組みの全てを絶対にactorに置き換えなければいけない」という誤解が存在すると考えています。 基本的にはActorが最適な選択肢ではありますが、Strict Concurrencyを段階的に対応する際やActorではカバーできないユースケースにおいては既存のLockの仕組みを使って良いとSwiftチームも考えており 、このメッセージはStrict Concurrency導入のハードルを下げるのに大きく影響すると思い座談会の場で共有しました。 セッション時間が40分だったため、登壇者の事前打ち合わせで議論した内容の半分も話せていないのですが、その反面40分に最低限必要な内容を詰め込んだ密度の高いセッションになったと思います。僕以外の登壇者が全員著名な方だったため、自分が参加して良いものかと正直不安でしたが、上記のエピソードなどが聴講してくださった方々のお役に立てたなら、登壇して良かったなと思います。 僕自身も事前の打ち合わせで登壇者の方々から多くの事を学ばせていただきました。 App Intentsの未来について研究しよう! (ポスターセッション): jollyjoester AppleプラットフォームにはApp Intentsと呼ばれるアプリの特定の機能をシステム(OS)に伝えることができる重要な機能があります。これにより、アプリ外(Siri・ショートカット・ウィジェット)からアプリの機能を利用することが可能になります。 また、iOS18からApple Intelligence (AI)が導入されることによってApp Intentsの重要性が高まることが予想されています。 App Intentsの概要から最小実装例の解説まで網羅したポスターセッションで、キャッチアップにピッタリな内容でした。 また、「こんな体験ができるアプリが良い!作りたい!」と想像を膨らませてワクワクできるセッションでした。 Swift コードバトル Swiftコードバトルはお題で指示された動作をするSwiftコードをより短く書けた方が勝ち、という競技です。 お題は決して難しいものではなく、少し練習すればSwiftプログラマであればどなたでも参加できる難易度を目指しています。 お題例1: 入力された文字列を逆順にして出力するプログラムを書いてください。 お題例2: 与えられた整数リストの要素の合計を計算するプログラムを書いてください。 Swiftコードバトル予選 の説明文から引用 私kntkも参加し、準優勝しました! iOSDC Japan 2024運営によるSwiftコードバトル解説記事 私は昔からSwiftが好きだったため、Swiftの標準ライブラリや”スマートな書き方”には多少自身がありました。競技プログラミングもSwiftで活動していたため、競プロ的な問題に対する経験値もありました。今回のコードバトルは自分のSwift力を試せる良い機会だったと思います。 コードバトル当日は「観客の前でライトに照らされ、顔と画面を配信されながら時間以内にプログラムを書く」という緊張感のある環境だったと同時に、回答を提出して観客の方々から歓声が上がった時はとても気持ちが良く、まるでスポーツでもやっている気分でした。 決勝では敗退してしまいましたが、対戦相手の方は予選の時からの強豪でしたし、決勝の問題が「競プロ的には簡単かつ文字数の工夫方法が多い」という決勝に相応しい・素晴らしい問題だったため、悔しいですが言い訳の余地のない結果だったかなと思っています。今回の学びを活かして今後も精進していきたいと思います。 また、 このコードバトルの良い点は解答の速さよりコードの短さが評価される点です。実際に、当日の試合データを見ると、全7試合中5試合の勝者は、10分以上経過後に提出した回答で勝利しています。 (Swiftに自信はあるが)速度に自信がない方でも十分に勝算があるということです。是非次回があれば、Swiftに自信がある皆さんに参戦していただきたいなと思います。 とても楽しい企画だったのでぜひ来年も開催してもらいたいです!そして次回はもっと多くの方に参戦してほしいです! メルカリスポンサーブース メルカリスポンサーブースでは「Engineering Office Hour」「チェキ撮影」「メルカリ iOSクイズ」の3つを出展しました。 また、ブースに来てくれた方にはノベルティとして メルカリの公式キャラクター が印刷されたコースターをプレゼントしました。 ( 亀の「ゼニー」猫の「ミケ」 うさぎの「ロップ」が印刷されたコースター) Engineering Office Hour Engineering Office Hourは、タイムテーブル形式でメルカリ社員と話せる企画です。簡単な自己紹介やトークテーマ、ブース滞在時間がまとまったポスターを掲載し、参加者が社員やトークテーマ狙いで遊びにいけるような設計を行いました。 実際に特定の社員に会いに来た方や、ある社員が専門とする技術領域の質問をしに来た方がいらっしゃり、とても良い企画だったなと思いました。 チェキ撮影 ブースでは参加者の方とメルカリ社員のチェキ撮影も行っていました!合計で90枚近くのチェキを撮影し、参加者の方にプレゼントしました。(「エモい」と好評でした!) メルカリ iOSクイズ iOS開発に関するクイズを3日分(合計18問)出題しました。3日合計で212件の挑戦をいただき、大変ありがとうございました!詳しい内容はぜひ iOSDC Japan 2024 メルカリブース iOSクイズ解説 をご覧ください! まとめ 今年のiOSDCはSwiftコードバトルや座談会など新しい試みが多く刺激的でした。またスポンサーブースでもEngineering Office Hour・チェキ・クイズに多くの方が参加してくださり、本当にありがとうございました。 最後に、iOSDC Japan 2024 の運営の皆様お疲れ様でした&ありがとうございました!また来年も参加したいなと思います! #iosdc #iwillblog
こんにちは。株式会社メルカリでiOSエンジニアをやっている kntk です。 8月22日から8月24日にかけて開催された「iOSDC Japan 2024」にメルカリはプラチナスポンサーとして参加し、会場ブース出展ではiOS開発に関するクイズを3日分(合計18問)出題しました! 本記事ではこのクイズの問題とその解説をお届けします! 総回答件数: 212件 作問者: kntk sae 前提 個別に記載がない限りXcode 15.4(Swift 5.10)・iOS 17.6.1上での実行結果を正解とします。 最適化オプションは-Oを正解とします。 Day0 前夜祭 (8/22) 回答者数: 26名 平均点: 2.04/6点 全問正解者: 0名 1. 次のプログラムを実行すると何が出力されるでしょう? func f(_ a: Int?) { print("Int?") } func f(_ a: Any) { print("Any") } let a: Int = 1 f(a) 選択肢 A) Int? B) Any 答え B) Any 解説 Swiftにはオーバーロードが存在します。 ある関数呼び出しに当てはまる複数の関数オーバーロードが存在した時、スコア規則というルールで解決する優先順位が決められています。 Intからの変換はInt?よりもAnyが優先です。 参考: Swiftのオーバーロード選択のスコア規則21種類#ランク7: SK_ValueToOptional 2. 以下の数字をカウントするアプリでCounterViewのボタンを5回押した後、RootViewのボタン (“toggle color!”)を1回押すと次のうちどの表示になるでしょうか? struct RootView: View { @State var condition = false var body: some View { VStack { CounterView() .if(condition) { $0.background(Color.red) } Button("toggle color!") { condition.toggle() } } } } struct CounterView: View { @State var count = 0 var body: some View { Button(count.description) { count += 1 } } } extension View { @ViewBuilder func `if`<Content: View>( _ condition: Bool, @ViewBuilder transform: (Self) -> Content ) -> some View { if condition { transform(self) } else { self } } } アプリの表示イメージ 選択肢 A)「5, 赤背景」 B)「5, 背景なし」 C)「0, 赤背景」 D)「0, 背景なし」 答え C) 「0, 赤背景」 解説 Stateの状態はView Identityという識別子によって管理されているため、View Identityが変わるとStateも初期化されます。また、ViewBuilderのif文はそれぞれの分岐のViewで異なるView Identityを持ちます。( Structural Identity ) 今回のプログラムでは、”toggle color”ボタンを押すとif文の分岐が変化し、違うView Identityに変わってしまうため、状態が初期化されcountが0に戻ってしまいます。 このコードの様なif modifierは、使用者側からView Identityが変わることが意識しづらいので注意が必要です。 参考: [SwiftUI] ViewのIdentityと再描画を意識しよう 3. Sendability違反 (Swift 6でのエラー) に該当する型を全て選択してください。 ※classのinitを省略して記載しています struct A: Sendable { let count: Int } struct B: Sendable { var count: Int } final class C: Sendable { let count: Int } final class D: Sendable { var count: Int } 答え Dのみ 解説 Sendableは「Isolation Domain(並列にアクセスが行われる単位)を安全に跨ぐことができる」を表すprotocolです。 structのみで構成されるstructはSendable。structは値型であり、スレッドを跨ぐ際に値のコピーが行われるため、(内部の変数がvarであっても)データ競合の危険性がありません。 classはfinalで内部の変数が全て「let」かつ「Sendable」ならSendable。それ以外はnon-Sendable。classは参照型であり、Isolation Domainを跨いで参照が共有可能ですが、変数がletであれば変数への書き込みが不可能であり、さらにSendableであれば安全にアクセスできることが保証されているため、data raceの危険性がありません。 変数がvarまたはnon-Sendableの場合は書き込みが可能でdata raceの危険性があります。 参考: 公式ドキュメント Sendable 4. nをCollectionの長さとした時、Array.countの計算量は「a」, String.countの計算量は「b」である。 選択肢 A) a: O(n) b: O(n) B) a: O(1) b: O(n) C) a: O(n) b: O(1) D) a: O(1) b: O(1) 答え B) a: O(1) b: O(n) 解説 Collection.countの計算量はO(n)です。ただしRandomAccessCollectionの場合はO(1)となります。 ArrayはRandomAccessCollectionですが、StringはRandomAccessCollectionではありません。 参考: 公式ドキュメント Collection.count 5. 次のプログラムを実行すると何が出力されるでしょう? class Counter { var count = 0 } var counter = Counter() let closure = { [counter] in print(counter.count) } counter.count += 1 closure() counter = Counter() closure() 選択肢 A) 0 0 B) 0 1 C) 1 0 D) 1 1 答え D) 1 1 解説 capture listsを用いて変数をキャプチャすると、クロージャ定義時の値でその変数が初期化されるため、変数の変更が共有されません。 参考: Swift 公式ドキュメント Capture Lists 6. 次のプログラムを実行すると何が出力されるでしょう? func log(info: String = "called: \(#function)") { print(info) } func main() { log() log(info: "called: \(#function)") } main() 選択肢 A) called: main() called: main() B) called: main() called: log(info:) C) called: log(info:) called: main() D) called: log(info:) called: log(info:) 答え C) called: log(info:) called: main() 解説 デフォルト引数における#functionは呼び出し元の関数名を生成する特殊マクロですが、特殊マクロはデフォルト引数のsub-expressionで用いる(例: 文字列展開で値を加工する)と呼び出し元の関数名を参照できなくなります。 参考: SE-0422 Expression macro as caller-side default argument Day1 (8/23) 回答者数: 116名 平均点: 2.98/6点 全問正解者: 4名 1. 次のプログラムを実行すると何が出力されるでしょう? func f() { print("sync") } @_disfavoredOverload func f() async { print("async") } func g() async { await f() } await g() 選択肢 A) async B) sync 答え A) async 解説 @_disfavoredOverload はオーバーロード解決の優先順位を下げるattributeです。 しかし「async関数内の関数呼び出しではasyncオーバーロードを優先する」という別のルールの方が優先度が高いため、この例では影響しません。 この優先度は「スコア規則」と呼ばれるルールで決められています。 参考: Swiftのオーバーロード選択のスコア規則21種類#ランク17: SK_SyncInAsync 2. 次のプログラムを実行すると何が出力されるでしょう? struct User: Hashable { var id: String var name: String func hash(into hasher: inout Hasher) { hasher.combine(name) } } let set: Set<User> = [ User(id: "1", name: "John"), User(id: "2", name: "John") ] print(set.count) 選択肢 A) 0 B) 1 C) 2 答え C) 2 解説 SetやDictionaryにおいて、基本的にはハッシュ値に基づいて保持する要素の管理が行われますが、ハッシュ値が一致した場合はEquatableによる同値比較を行い一意性を担保する仕様になっています。 この例は意図的にハッシュ値を衝突させていますが、ハッシュは仕組み上、元の値が異なってもハッシュ値が一致する可能性があるため、この仕様は重要な普段でも役割を果たしています。 3. 次のプログラムを実行すると何が出力されるでしょう? func doSomething() async -> Int { await withCheckedContinuation { continuation in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { continuation.resume(returning: 1) } } } let task = Task { let result = await doSomething() print(result) } task.cancel() await task.value 選択肢 A) 1 B) 出力なし 答え A) 1 解説 Task.cancel()はTask.isCancelledのフラグを立てるだけなので実際に処理を中止する処理は自分で実装しなければいけません。 今回の例は中止する処理が記述されていないのでTask.cancel()は影響せず3秒後に1が出力されます。 参考: [Swift] async関数とAsyncStreamのキャンセル 4. 次のプログラムを実行すると何が出力されるでしょう? func doSomethingAsyncStream() -> AsyncStream<Int> { AsyncStream { continuation in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { continuation.yield(1) continuation.finish() } } } let task = Task { for await result in doSomethingAsyncStream() { print(result) } print("finish") } task.cancel() await task.value 選択肢 A) 1 finish B) finish C) 1 D) 出力なし 答え B) finish 解説 Task.cancel()はTask.isCancelledのフラグを立てるだけなので実際に処理を中止する処理は自分で実装しなければいけません。 しかし、AsyncStreamに対するfor await inはTaskのキャンセル直後に処理を中止します。 AsyncSequenceに対するfor await inはAsyncSequence.Iterator.next()のシンタックスシュガーであり、AsyncStream.Iterator.next()は処理を中止する実装がされているからです。 ここで、for await inはTaskのキャンセル直後に処理を中止しますが、Task {}自体は処理を中止する処理が実装されていないので、print("finish")は実行されます。 参考: [Swift] async関数とAsyncStreamのキャンセル 5. Swift6.0から外部パッケージの型にprotocolを準拠させる際に表示される警告を消すattributeの名前は何でしょうか? extension Date: @attribute名 Identifiable { public var id: TimeInterval { timeIntervalSince1970 } } 選択肢 A) @conform B) @active C) @retroactive D) @foreign_conform 答え C) @retroactive 解説 (外部のパッケージの型に)後からprotocolを準拠させる機能は便利な一方で、フレームワーク提供側が意図しない利用方法であったり、後から提供側が同じprotocolを別の挙動で準拠させる可能性があると言う危険性があります。その危険性を可視化するための警告と、その警告を消すattributeが追加されました。 参考: SE-0364 Warning for Retroactive Conformances of External Types 6. 次のSwiftUIのコードに対応する表示は次のうちどれでしょう? Text("Hello, mercari!") .padding() .border(Color.red, width: 1) .offset(x: 50, y: 50) .border(Color.blue, width: 1) .overlay(Circle().fill(.green)) 選択肢 答え D) 解説 offsetはレシーバーの「表示位置」だけを変えるmodifierであり、周りのレイアウトやその後のmodifierに影響を与えません。 参考: 公式ドキュメント offset Day2 (8/24) 回答者数: 70名 平均点: 2.21/6点 全問正解者: 2名 1. 次のプログラムの出力が0になるような演算子はどれでしょう? let a: UInt8 = 255 let b: UInt8 = 1 print(a 演算子 b) // 0 選択肢 A) + B) ^ C) | D) &+ 答え D) &+ 解説 Swiftでは+, -, を用いた演算でオーバーフローが発生するとランタイムエラーになります。 一方、&+, &-, & はオーバーフロー演算子と呼ばれ、オーバーフローを許容して桁あがりする(.minに戻る)挙動になります。 参考: Swift 公式ドキュメント オーバーフロー演算子 2. 次のプログラムを実行すると開始から終了までに (約) 何秒かかるでしょう? func wait() async { try? await Task.sleep(for: .seconds(1)) } await (wait(), wait()) 選択肢 A) 1 B) 2 答え B) 2 解説 Swiftではawaitキーワードを一つにまとめたり、位置を変えることができます。 しかし、関数呼び出しの挙動に影響はないため、awaitを一つにまとめても並列に動作はしません。 並列に動作させる場合はそれぞれの呼び出しをasync letで定義する必要があります。 3. 次のプログラムの出力はSwift6未満で「a」Swift6以上で「b」である。 func f( _ a: (() -> Void)? = nil, _ b: (() -> Void)? = nil ) { if a != nil { print("forward") } if b != nil { print("backward") } } f { } 選択肢 A) a: backward b: backward B) a: forward b: backward C) a: backward b: forward D) a: forward b: forward 答え C) a: backward b: forward 解説 Swift6の破壊的変更の一つです。 Swift5.3未満はTrailingClosureはbackward scan (クロージャーを末尾の引数から当てはめる)のみでした。 Swift5.3からSwift5.10まではbackwardとforward両方のチェックを行います。backwardとforward両方候補になった場合は互換性維持の観点でbackwardを選択します。 しかし、Swift6.0(もしくは-enable-upcoming-feature ForwardTrailingClosures)からはforward scanのみとなります。 参考: SE-0286 Forward-scan matching for trailing closures 4. 次の呼び出しが当てはまる関数定義はどれでしょうか。 getUserInfo([Seller(), Buyer()]) protocol User { func getInfo() } struct Seller: User { func getInfo() { /* ... */ } func listItem() { /* ... */ } } struct Buyer: User { func getInfo() { /* ... */ } func purchaseItem() { /* ... */ } } 選択肢 A) func getUserInfo<U: User>(_ users: [U]) { /* ... */ } B) func getUserInfo(_ users: some Sequence<User>) { /* ... */ } C) func getUserInfo(_ users: [some User]) { /* ... */ } D) func getUserInfo(_ users: [any User]) { /* ... */ } 答え D) 解説 A: Conflicting arguments to generic parameter ‘U’ (‘Buyer’ vs. ‘Seller’) B: Cannot convert value of type ‘[Any]’ to expected argument type ‘[any User]’ C: Conflicting arguments to generic parameter ‘some User’ (‘Buyer’ vs. ‘Seller’) ジェネリクスを用いるとBuyerまたはSellerどちらか一方の型の配列として推論されるため、両方の型の値を持つ配列を受け取ることができません。 参考: A: Generics C: Opaque Argument Type (Aのシンタックスシュガー) D: Existential (Argument) Type 5. この表示に対応するプログラムはどれでしょうか? 選択肢 A) Image(systemName: "paperplane.circle.fill") .symbolRenderingMode(.palette) .foregroundStyle(.white, .blue) B) Image(systemName: "paperplane") .symbolVariant(.circle) .symbolVariant(.fill) .symbolRenderingMode(.multicolor) C) Image(systemName: "paperplane.circle.fill") .symbolRenderingMode(.hierarchical) .foregroundStyle(.blue) D) Image(systemName: "paperplane.circle.fill") .symbolRenderingMode(.monochrome) .foregroundStyle(.blue) 答え C) 解説 SF SymbolにsymbolRenderingModeを適応することによって、着色方法を変えることができます。(表示結果 左からA,B,C,Dの順) 参考: 公式ドキュメント symbolRenderingMode 6. 次の関数呼び出しが当てはまる定義をすべて答えてください。 class Super {} class Sub: Super {} let a: Sub? = Sub() f(a) func f(_ a: Any) {} // A func f(_ a: Any?) {} // B func f(_ a: Super?) {} // C 答え A, B, C(全部) 解説 A: Sub?からAnyへの変換。AnyはOptionalも当てはまります。 B: Sub?からAny?への変換。Optionalの要素SubがAnyに変換されています。 C: Sub?からSuper?への変換。Optionalの要素SubがSuperに変換されています。 まとめ 当日はたくさんの方に挑戦いただきありがとうございました! 躓きやすいSwiftやiOSの仕様をピックアップしたクイズになっているので、この記事の解説がSwiftやiOSへの理解を深める助けになれば嬉しいなと思います。#iosdc #iwillblog
こんにちは、Engineering Officeの yasu_shiwaku です。 2024年7月16日、一般社団法人日本CTO協会様主催の「 Developer eXperience AWARD 2024 」にて、「開発者体験ブランド力」調査の中で、 メルカリが昨年に引き続き3年連続で1位に選出されました。 日本CTO協会様のプレスリリースは こちら です。 今年もオフラインの会場で授賞式がおこなわれ、当日は執行役員CTO Marketplaceの 木村俊也 が出席し、受賞コメントを述べました(木村は7/17の パネルディスカッション にも登壇予定です) 昨年に引き続き、多くの方から高い評価を得られたことを嬉しく思います!社内外を問わず、ブログや登壇、イベントへの参加など多岐に渡って情報発信に貢献してくれているエンジニアたちのおかげです。 メルカリグループではエンジニアたちが主体的に発信し、コミュニティにその経験や知見を還元していくことで業界全体を活性化・成長させていくカルチャーを育てています。 またメルカリが利用させていただいているオープンソースコミュニティへの還元として、カンファレンスや プロジェクトスポンサー などの支援活動もおこなっています(メルカリの オープンソース に対する考え方はこちら。公開ソフトウェアは こちら ) メルカリグループは 「あらゆる価値を循環させ、あらゆる人の可能性を広げる」 のミッションのもと、エンジニアリング組織としても、新しいチャレンジや問題解決に向かい合っていく中でエンジニアリングの価値を循環させ、可能性を広げていくために、今後も社内外の開発コミュニティに向けて貢献できるよう、情報発信を続けていければと思います。 エンジニア向け発信媒体一覧 Mercari Engineering Website (本ポータルサイトです) Xアカウント( 英語 ・ 日本語 ) イベント関連 Connpass Meetup YouTubeチャンネル Mercari Gears Mercari devjp メルカリグループでどんな開発者体験ができるのか、またどんなカルチャーがあるのか興味がある方は、ぜひキャリアサイトを一度覗いてみてください! Software Engineer/Engineering Manager
こんにちは。MercoinでBackendエンジニアをしている goro です。 先月6月8日にAbema Towerで開催された「 Go Conference 2024 」にメルカリはSilverスポンサーとして参加し、会場にはブースを出展しました。今回はそのレポートをお届けします! Go Conference 2024 について Go Conferenceは年に1回行われる、プログラミング言語Goに関するカンファレンスです。今年は数年ぶりのオフライン開催で、20以上のGo言語に関するセッションを中心に、オフラインならではの多くのイベントが企画されました。 開催概要 開催日時 2024年6月8日(土) 場所 Abema Tower 当日の様子をご紹介 メルカリブース メルカリブースでは、Goエンジニアの参加者の皆さんに対するオープンクエスチョンと、私たちがアイディアを出し合って作成したクイズを準備しました。 オープンクエスチョンでは「あなたが一番好きなGoのライブラリは?」という質問をしました。 127票もの回答が集まり、1位「net/http」19票、2位「io」「fmt」6票、3位「gin」「echo」5票という結果になりました。投票していただいた方々、ありがとうございました。 またもう一つの企画でGoとメルカリに関するクイズを6問出題しました。 当日出題したクイズの正解に関してもこちらに記載しておきます。挑戦してくださった皆さんありがとうございました。 Q1. 次のコードはコンパイルできるでしょうか? A. No No: "undefined: T "というエラーになります。 Go Playground: https://go.dev/play/p/jQ-V9THYZLX Q2. 次のコードはコンパイルできるでしょうか? A.No No: error: "use of untyped nil in assignment to _ identifier"というエラーになります。 The Go Playground: https://go.dev/play/p/l2Dyg6JF2xV Q3. 次のGoのコードの出力はGo 1.22では何になるでしょうか? A. 1 Go1.22では1になります。 Go1.22で修正されたループの挙動によるものです。こちらに修正された内容が詳しく書かれています。 https://go.dev/blog/loopvar-preview The Go Playground: https://go.dev/play/p/26VrcKf2dXx Q4. 次のGoのコードの出力は何になるでしょうか。 A. timeout The Go Playground: https://go.dev/play/p/CuaQuBooOQL ※このコードは通常"timeout"と出力されますが、システムの状況によっては異なる結果になる可能性もあります。 Q5. 社内勉強会 Go Friday は現時点(6/7時点)で何回開催されたでしょうか? A. 363 Q6. Cloud Spannerのためにgithub.com/xo/xo をフォークして作成されたライブラリはどれですか? A. github.com/cloudspannerecosystem/yo 今回のイベントに合わせて、メルカリブースに遊びにきていただいた記念として様々なノベルティをお配りしました。ブースにお越しいただいた皆さま、ありがとうございました! セッションについて スポンサーブースにいる時間が長かったのですが、合間にいくつかのセッションを見ることができました。個人的には「 GoのLanguage Server Protocol実装、『gopls』の自動補完の仕組みを学ぶ 」というセッションが非常に興味深かったです。このセッションでは、普段特に意識することなく利用しているgoplsの仕組みを詳しく知ることができ、とても面白かったです。 メルカリからはメルコインVPoEのpoohさんが「 Go1.21から導入されたGo Toolchainの仕組みをまるっと解説 」というテーマでセッションを行いました。立ち見になる程盛り上がっていました。 発表資料はこちらをご確認ください。 https://speakerdeck.com/yamatoya/go1-dot-21karadao-ru-sareta-go-toolchainnoshi-zu-miwomarututojie-shuo/ 抽選会 運営の企画でスポンサーブースを回ってスタンプを集めると抽選会に参加できるというイベントがありました。めちゃくちゃ可愛いGopherのキーホルダーが当たり、家宝にします! まとめ 私にとっては数年ぶりのオフラインでのカンファレンスだったので、どれくらいの方がブースに来てくださるか最初は不安でしたが、実際には100名以上の方々にメルカリのブースへお越しいただけました。クイズやアンケートにお答えいただき、本当にありがとうございました。このメルカリのテックブログを参考にしているというお声もいただき、非常に嬉しく思いました。 最後に、Go Conference 2024の運営の皆さま、本当におつかれさまでした。また次回も参加させていただきます!
はじめに こんにちは、mercari.go スタッフの hiroebe です。 6月18日にメルカリ主催の Go 勉強会 mercari.go #26 を YouTube でのオンライン配信にて開催しました。この記事では、当日の各発表を簡単に紹介します。動画もアップロードされていますので、こちらもぜひご覧ください。 moq – gomockを使わないMock生成 1つめのセッションは @oinume さんによる「moq – gomockを使わないMock生成」です。 発表資料: moq – gomockを使わないMock生成 Go の interface からモックを生成するためのツールである moq について紹介しました。Go のモックライブラリといえば gomock が有名ですが、gomock は生成されるコードが Type Safe でなかったり、多機能ゆえに学習コストが高いといった問題があります。これに対して moq は生成されるコードが Type Safe であり、また機能が絞られているぶん学習コストが低いといった特徴があり、前述の gomock の問題点を解消してくれるツールとなっているそうです。機能が絞られているといっても必要最低限の機能は提供されていて、モックのメソッドが呼び出された回数や引数の値のチェックなどは実現できるとのことです。 個人的にも moq は気になっていたものの使ったことがなかったので、この機会に触ってみたいと思いました。gomock だと too much に感じている方はぜひ試してみてはいかがでしょうか。 Gobraで見る形式検証 2つめのセッションは @kobaryo さんによる「Gobraで見る形式検証」です。 発表資料: Gobraで見る形式検証 形式検証についての概要説明と、Go プログラムの検証器である Gobra の紹介を行いました。形式検証とは「プログラムが仕様を満たしていることを数学的手法を用いて証明すること」で、私たちが普段利用しているプログラムの「型」も一種の軽量な形式検証であると紹介されています。Gobra はアノテーション付きの Go プログラムを入力する検証器で、インターフェースや Goroutine といった Go の主要な機能にも対応しているそうです。発表では、これらの機能が Gobra によってどのように検証されるかについて、具体例を交えて説明されています。 形式検証というテーマは個人的にあまり馴染みのない内容だったので、とても新鮮でおもしろい発表でした。アノテーションの記述量の多さや仕様を定めることの難しさなど形式検証には欠点もあるそうですが、普段の開発でも活用できるような例があればぜひ知りたいと思いました。 gRPC Federation から見る WebAssembly の実践活用法 3つめのセッションは @goccy さんによる「gRPC Federation から見る WebAssembly の実践活用法」です。 発表資料: gRPC Federation から見る WebAssembly の実践活用法 メルペイではリリースからの時間経過に伴って BFF (Backends for Frontends) の肥大化が問題となっており、これを解消するために BFF を複数に分割し、個々の BFF を gRPC Federation を用いて構築することで開発・運用コストを抑えるというアプローチをとっています。gRPC Federation は Protocol Buffers 上で記述した DSL から gRPC Server を自動生成する仕組みで、これによって個々のサービスの開発者はサービスそのもののロジックに集中できるという利点があります。今回の発表では WebAssembly を用いたプラグインの仕組みに焦点を当てて、コンパイラ・ランタイムの選定や、ホストとクライアント間のデータのやり取りについて紹介されています。 WebAssembly の活用事例としてとても興味深い発表でした。gRPC Federation については goccy さんのブログ記事 gRPC Federation: gRPC サービスのための Protocol Buffers を進化させるDSL もぜひ併せてご覧ください。 おわりに 今回は Go のツール紹介や WebAssembly の活用事例など、幅広い内容の発表をお送りしました。普段の開発では触れる機会の少ないような内容もあり、運営としても非常に興味深く聞かせていただきました。 ライブで視聴いただいた方も録画を観ていただけた方も本当にありがとうございました! 次回の開催もお楽しみに! イベント開催案内を受け取りたい方は、connpassグループのメンバーになってくださいね! メルカリconnpassグループページ
こんにちは。メルカリのSoftware Engineerの @tanasho です。 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- の6回目を担当させていただきます。 メルカリ ハロのWebアプリケーションは複数存在し、Webフロントエンドチームが横断的に開発をしています。本記事では、その前提を踏まえ、スピードと品質をどのように両立させて開発しているかを紹介します。 プロジェクトの概要とWebフロントエンドの担当領域 メルカリ ハロは「あたらしい出会いを繋ぎ、信頼と機会をひろげる」がミッションで、いますぐ働き手が欲しいパートナー (事業者) と、いますぐ働きたいクルー(働き手)を繋げるサービスです。クルーは自身のスキルや時間を活用して働くことができます。 メルカリ ハロは複数のアプリケーションが存在し、そのなかでWebフロントエンドが関わる領域として以下の3つがあります: はたらくタブ 事業者管理画面 CS Tool それぞれのアプリケーションの概要は以下の通りです。 はたらくタブ 「はたらくタブ」は、メルカリアプリ内に用意されている、クルーを対象とした機能です。「はたらくタブ」は、スポットワークの経験がない人や、時々働きたいライトなユーザーに向けた機能で、この機能を使うことで、メルカリ ハロの専用アプリと同様に仕事を見つけて働くことができます。またすでにメルカリを使っているお客さまにも利用していただくことで、「メルカリ ハロ」の迅速な認知拡大が期待されています。 関連記事: 『メルカリ ハロ』の成長を加速させる最重要タッチポイント、「はたらくタブ」はこうしてつくられた この「はたらくタブ」はiOSおよびAndroidのメルカリアプリ内のWebView上で動作します。各OSごとに開発する必要がなく、Web開発のみで対応できるため、開発工数を削減できました。 また、リリース時にはアプリストアの審査が不要となるため、柔軟にリリースを行うことが可能です。 さらに、既存のメルカリアプリ内に存在するためアクセス数が多い特徴もあります。 事業者管理画面 事業者管理画面は、メルカリ ハロにおける求人や募集の作成など、パートナー向けのWebアプリケーションです。権限管理が必要な点や、フォームを使った登録・更新の機能が多い点が特徴です。 CS Tool 社内のメルカリ ハロのカスタマーサービス向けのWebアプリケーションです。 お問い合わせに対応するために、クルー検索などの機能が備わっています。 スピードと品質を両立して開発するための取り組み メルカリ ハロを展開するにあたって、連載の最初の記事でも紹介したように、開発のスピードを重要視しています。これはスポットワーク市場の拡大の波に沿って事業を推進するためです。また、最初から正しい仮説を持つことは難しく、仮説の立案、検証、改善のイテレーションを高速に回すことが大事だからです。 またスピード感をもった開発が求められる一方で、高い品質を保つことも重要です。 現在、メルカリ ハロのWebフロントエンドチームは約6名の少人数で「はたらくタブ」「事業者管理画面」「CS Tool」の3つのWebアプリケーションの開発を担当しています。 こうした少人数の構成の中でどのように工夫をして、開発スピードと品質の両立を達成しているかについてご紹介します。 柔軟なアサイン 「はたらくタブ」「事業者管理画面」「CS Tool」を開発するにあたって、特定のWebアプリケーションに優先度の高い機能開発の案件が集中することがあります。これに対応するため、Webフロントエンドチームでは特定のWebアプリケーションに担当を固定せず、横断的にアサインが可能な体制にしています。例えば、「事業者管理画面」に優先度の高い機能開発の案件が多くなった場合、その開発にリソースを集中させます。状況に応じて柔軟にアサインを変えられる体制にすることで、優先度の高い機能をスピード感もって開発することができました。 技術スタック・開発ルールを可能な限り統一 柔軟なアサイン体制の下で生産性と保守性を上げるために「はたらくタブ」「事業者管理画面」「CS Tool」の技術スタックやアプリケーションの構造、開発ルールなどは可能な限り統一しております。 ただし、技術的な問題、アプリケーションの仕様の違いなどにより、一部に差異が生じる箇所も存在しています。 「はたらくタブ」「事業者管理画面」「CS Tool」で扱っている主な技術スタックは以下の通りになります: Next.js (App Router) React TypeScript ESLint Apollo Client ※ Tailwind CSS Storybook React Hook Form Jest Playwright pnpm Datadog 統一の結果、普段担当していない別のWebアプリケーションにアサインされた場合でもスムーズに開発を行うことができました。例えば「事業者管理画面」を開発していたメンバーが「はたらくタブ」の開発を任された場合でも、ほとんど同じ技術スタックや開発の作法を活用できるため、追加の学習コストを抑えて開発に取り組むことができました。また、開発ドキュメントもある程度共通化することができるため、ドキュメントの保守性も向上しました。 さらに、メルカリ ハロではmonorepoを採用( 関連記事 )し、異なるWebアプリケーションを同一リポジトリで管理しています ※ 。これにより、Webアプリケーション間の行き来が容易になったり、lintのルールの設定などがpnpmのworkspaceを使って共有可能になりました。 ※ただし、CS Toolは除く ... ├── dart ├── go └── typescript ├── apps │ ├── partner-portal-web //事業者管理画面 │ ├── work-tab //はたらくタブ ├── libs //共通の関数やlintのルールの設定 ... これらの取り組みによって開発生産性と保守性が向上しただけでなく、誰かが開発で困ったときに他のメンバーが助けやすい体制が整い、チームとして支え合う文化が一層強まったことも大きなメリットだと思います。 以降、その統一された技術スタックでどのように開発しているか紹介します。 UI開発 UI開発において生産性を上げるために社内のメルカリデザインシステム( 関連記事 )を採用しています。 メルカリデザインシステムはFigmaで提供されており、デザイナーはデザインシステム上のトークンやUIコンポーネントを参照して画面を作成しています。フロントエンド開発では、Figma上で使用すべきコンポーネントや色が明示されているため、迷わず開発できるようになっています。また、 カスタムテーマ の設定で、メルカリのデザインシステムのトークンをTailwind CSSに統合しています。これによって、レイアウトの調整やデザインシステムが提供していないUIコンポーネントを作成する際に、簡単に一貫性のあるスタイルを適用することが可能です。 メルカリデザインシステムにもとづいて画面を作成することで、UIコンポーネントの開発工数を削減し、デザイナーと開発者間でUIの細かい挙動についての認識も一致させやすくなりました。 さらに、デザインレビュー時にはプルリクエストの段階でテスト環境にデプロイし、その環境で画面の動作を確認してもらうことができます。実際の画面を操作することで、デザイナーと開発者の認識を更に一致させることができました。 バックエンドとのやり取り メルカリ ハロのフロントエンドとバックエンドとのやり取りはGraphQLで行っており、そのAPI Clientとして社内で利用実績が多く、キャッシュ管理などの豊富な機能を持つApollo Client を採用しています ※ 。ここで取り入れた仕組みや活用している機能についていくつか紹介します。 ※ただし、CS Toolは除く Apollo ClientのHooksを自動生成 ページに必要なAPIのフィールドをGraphQLのスキーマファイルをもとに定義し、Apollo ClientのHooksを自動生成しています。 monorepoを採用しているため、同一リポジトリ内のフロントエンドの開発ディレクトリからバックエンドの開発ディレクトリに配置されているGraphQLのスキーマファイルを直接参照でき、それをもとにApollo ClientのHooksを自動生成するといった仕組みです。 これにより、バックエンドからデータを取得する際には、その自動生成されたApollo ClientのHooksを呼び出すだけで済み、開発効率向上につながりました。また、ページに必要なデータのフィールドのみを取得し、APIの呼び出しも一回にまとめられるため、ネットワークのコストを抑えることができました。 Apollo Clientの機能の活用 Apollo Clientはデータ取得以外にも多くの機能を持っています。一例として、キャッシュの機能です。実行したqueryのレスポンスデータがローカルのインメモリ上にキャッシュされるため、そのキャッシュを活用することができます。例えば、useQueryの fetchPolicy がデフォルトのcache-first の場合ですが、fetch時にメモリにキャッシュが存在していたらそのデータを返します。ただし、存在しない場合はサーバーから取得します。キャッシュを優先しているため速度を重視する場合には有効かと思います。またこのfetchPolicyの設定は用途に合わせて変更可能です。 「事業者管理画面」では、データの表示速度も重要ですが、募集の応募状況など最新の情報を求められることが多く、データの新鮮さも重要です。そのため、キャッシュを活用しつつ、同時にサーバーから最新のデータも取得・更新するcache-and-networkの設定をデフォルトでしています。 さらに、Apollo Clientには Link という機能があります。これはApollo ClientとGraphQLサーバー間のデータフローをカスタマイズするための仕組みです。特にサーバーからのエラーハンドリングにおいてLinkを活用しています。例えば、Unavailableのエラーを返した場合にメンテナンスページにリダイレクトしたり、認証が切れたらtokenをリフレッシュする処理を実装しています。 モック開発 バックエンドのAPIが開発中であっても、フロントエンドの開発を進めたい場合があります。 その際に、まず先にバックエンドからGraphQLのスキーマファイルを共有してもらい、そのスキーマをもとに MSW を利用してAPIをモックし開発しています。この結果、フロントエンドとバックエンドが並列して開発でき、生産性の向上につながりました。 自動テスト リグレッションの自動検知を目的に自動テストを導入しています。 導入にあたって自動テストに費やせる時間と人手では限られているため、効率的・効果的にリグレッションを防ぐためのテスト設計が必要でした。 そこで一つの観点として、他のチームよって品質が担保されているテストと被らないことを重要視しました。以下のテストに関してはすでに他のチームで行われていました。 UIコンポーネントのテスト 前述の通り、メルカリ ハロのUIコンポーネントはメルカリデザインシステムを使っており、そのチームよって自動テストが行われている E2Eテスト QAチームが手動および自動で(フロントエンドからバックエンドまでの疎通を含む)E2Eテストを行っている この観点のもと、Webフロントエンドでは以下の自動テストを設計・運用しました。結果として、他のテストでカバーできていないテストを書く運用になり、効率的に品質を担保することができました。 単体テスト Jestを利用 共通関数の詳細な挙動を担保するため、条件分岐を網羅したテストを実施 ページ単位の統合テスト Playwrightを利用 機能が仕様通り動いているかを主にページ単位でテストを実施 バックエンドへの通信を行わず、必要なAPIのデータはモックし、フロントエンドに閉じた形で実施 運用にあたって、単体テストについては、共通関数として切り出したときにテストを書く運用がチームメンバーに浸透していたため、特に問題はありませんでした。 一方でページ単位の統合テストに関しては、対象となるページ数が多く、様々なテストシナリオが考えられるため、どの統合テストを重視して書くべきかが課題でした。例えば、特定のレスポンスが返ってきた際のページの表示が正しいかどうか、入力したフォームのバリデーションが正しく動作するかどうかなどです。そのため、どのテストを優先して書けば大きな効果を発揮するか検討する必要がありました。 そこで「はたらくタブ」「事業者管理画面」「CS Tool」それぞれのアプリケーションの性質に合わせて統合テストの方針を決めました。 例えば「事業者管理画面」では、フォームをSubmitした後のAPIのリクエストが正しいかのテストを第一優先としました。これは管理画面の性質上、募集作成など重要なデータの作成機能が多いため、意図しないリクエストが送信されて登録されてしまうと大きな問題が発生するためです。 このように、他で品質が担保されていない箇所を単体テスト・統合テストで担保し、統合テストのなかでアプリケーションの性質に合わせて方針を決めることで、効率的・効果的にリグレッションを防ぐことができました。 おわりに スピードと品質を両立させるためのWebフロントエンドの取り組みについて紹介しました。 「はたらくタブ」「事業者管理画面」「CS Tool」のアプリケーション間で可能な限り統一した技術を扱い、フロントエンドチームメンバー内で状況に応じた柔軟なアサインによって、お客さまが求める重要な機能をスピードと品質を両立させて開発し続けられる体制になりました。また、同じチームで性質の異なる3つのWebアプリケーションを経験することは、エンジニアにとって楽しく、やりがいのある挑戦でもありました。 更にこの体制によって、誰かが開発で困った場合でも他のメンバーが助けて支え合う「All For One」な文化が一層強まってると日々感じております。 その「All For One」な文化はメルカリグループ全体からも感じています。メルカリのデザインシステムがあらかじめ用意されていたり、 monorepo戦略がその一例です。自チームにとどまらず全体最適になるよう取り組んでくれたおかげで、この開発スピードと品質の両立が実現できてると感じてます。 この取り組みは終わりではなく、今後もチームやプロダクトの状況を見ながら、前向きに改善し続けることが重要です。引き続き、チームと話し合いを重ねて改善に努めていきたいと思います。 Links 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- メルカリではメンバーを大募集中です。メルカリ ハロの開発やメルカリに興味を持った方がいればぜひご応募お待ちしています。詳しくは以下のページをご覧ください。 Software Engineer, Frontend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Backend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, iOS/Android (Flutter) – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Site Reliability – Mercari/HR領域新規事業 (Mercari Hallo) QA Engineer – Mercari/HR領域新規事業 (Mercari Hallo) Engineering Manager – Mercari/HR領域新規事業 (Mercari Hallo)
こんにちは。メルカリ ハロでSoftware Engineerをしている @atsumo です。連載『 Mercari Hallo, World! -メルカリ ハロ 開発の裏側- 』の第5回を担当します。 メルカリ ハロではメルカリアプリ内にある「はたらくタブ」とは別にクルー向けのアプリ(ストアで「メルカリ ハロ」と検索してみてください)を用意しています。本記事では、アプリ版のメルカリ ハロで使用している技術とその選定理由、さらにリリースまでの開発の進め方などを踏まえてご紹介したいと思います。 この記事で得られること メルカリ ハロのアプリの技術スタック その技術選定の理由と効果 開発の進め方とヒント 技術スタック メルカリ ハロ アプリで使っている技術スタックをいくつかピックアップして紹介できればと思います。 フレームワーク: Flutter 第2回の記事 の中のメルカリ ハロのモバイルアプリの技術選定でも記載がありましたが、メルカリ ハロは マルチプラットフォームフレームワークのFlutterを採用しています。導入決定までの決断などは第2回で触れられているのでそちらをご覧いただければと思います。そちらの記事の中でも妥当な決断であったとありましたが、リリース後にアプリ開発に関わっているメンバーで振り返りを行った際も、開発効率の向上とサービスとしての品質の担保の両方行うことができ、良い選択だったという意見が多かったです。 開発人数が少ない中、両プラットフォームを同じタイミングでリリースすることができたのはFlutterを採用したことがとても大きかったです。 CLI: Melos Melos は複数パッケージをもつDartプロジェクトの管理に使用されるCLIツールです。 メルカリ ハロではモノレポ(monorepo)を採用しており、下記のような言語ごとのディレクトリ構造になってます。 ├── dart │ ├── analysis_options.yaml │ ├── apps │ │ ├── widgetbook │ │ └── hallo_app (ハロ アプリ) │ ├── melos.yaml │ ├── packages │ │ ├── hallo_design_system (デザインシステム) │ │ ├── hallo_linter │ │ └── ... │ ├── pubspec.lock │ └── pubspec.yaml ├── go ... └── typescript 開発当初、Flutterのプロジェクト自体は1つで複数のパッケージはなかったのですが、後でデザインシステムや社内のシステムを使うものはパッケージに分けていく構想があったため、開発開始時点からMelosを導入していました。初期は主にコマンドをまとめるための用途としてしか使っていなかったのですが、リリース後少し落ち着いたタイミングでデザインシステムをパッケージ分割したり、社内のサービスを使うためのパッケージが増えていったりと複数パッケージを持つプロジェクトとなり、引き続き力を発揮してくれています。 通信: GraphQL モバイルアプリとバックエンドの通信にはGraphQLを採用しています。 メルカリ ハロのアプリでは GraphQLを使用するための packageとして graphql_flutter , graphql_codegen を使用しています。 バックエンド側で定義した graphqlのschemaとアプリ側でアクセスするgraphqlファイルを元に graphql_codegen でGraphQLのサーバーにアクセスするためのファイルを自動生成しています。 サーバリクエストとレスポンスデータのキャッシュはgraphql_flutterが行ってくれるため、次にお話しする状態管理において構成を単純化するのに大きく寄与しました。 状態管理について 公式ドキュメント Differentiate between ephemeral state and app state に ephemeral state(一時的な状態管理) と app state(それ以外の状態管理) と分かれて記載されているのでそれに沿って説明します。 Ephemeral state (一時的な状態管理) Widget内やScreen内で完結するようなステートには flutter_hooks を使用しています。具体的には StateWidgetでのStateにもたせていたようなものには useStateを使用しています。画面を開いた際に画面の閲覧のログなどを送信したい場合には useEffectなどを使用しています。さらに必要に応じてカスタムHooksを作成しています。 App state (サーバリクエストとレスポンスデータ / グローバルステート) サーバサイドリクエストやキャッシュ GraphQLでのやり取りをするために、graphql_flutterを使用しています。graphql_flutterはApollo Clientをモデルとしたgraphqlのpackageです。 Apollo Client はローカルとリモートデータの両方をGraphQLで管理することができる包括的な状態管理のライブラリです。ただ現在、Apollo ClientはFlutter版のpackageがないため、その代わりとして Apollo Clientをモデルとして作られている grahql_flutter を選択しました。 GraphQL自体の仕組みや graphql_flutter が持つキャッシュの仕組みによって、クライアント側で独自の管理をする必要をなくし、状態管理の複雑性を削減しています。 ※ graphql_codegenが生成する hooksのメソッドを使用しています グローバルステート 画面を構成するうえで必要な情報はgraphql_flutterの情報から取得するのですが、複数画面に使用されるものや認証情報などに関しては Riverpod を使用してグローバルステートとして管理しています。 デザインシステム メルカリ ハロのアプリでは、メルカリのデザインシステムではなく、独自のデザインシステムを採用しています。ここでお話するデザインシステムは主にデザイントークンやUIコンポーネント、そのデザインデータや実装されたコンポーネントなどを指しています。 基本的にコンポーネントをベースに画面のUIデザインが行われています。立ち上げ時にはひたすらUIコンポーネントを実装してWidgetbook(後述)で確認し、一通りコンポーネントを実装し終わったあとに画面の実装に入っていきました。 Figma上のComponent propertiesでBooleanやTextだけでなくInstance SwapやVariantを用いてデザインしてもらっていたので、実装する際も他のWidgetがプロパティとして入ることが想像でき、デザインデータとして変更できるプロパティと実装上コンポーネントのプロパティのギャップが少ない状態を作ることができました。 各画面のデザインは主にコンポーネントを使った構成になってます。実装時にはコンポーネントを配置し、プロパティの値にFigmaのものを入れていくというシンプルな作業になり、生産性が非常に高く保たれたと感じています。 画面が増え、機能が増えることで、当時は想定できていなかった見せ方が必要になり、コンポーネント自体のアップデートであったり、新たなコンポーネントを追加することもありますが、初期の段階からある程度コンポーネントができていて画面実装に入れたのはとてもありがたかったです。このようにコンポーネントを先行して実装していく進め方は、他のプロジェクトでも参考にしていただけるのではないでしょうか。 開発したコンポーネントは、次に触れるWidgetbookを使用してカタログとして閲覧できるようにしています。 UIカタログ: Widgetbook Widgetbook はフロントエンドでよく使われるStorybookのFlutter版のようなツールで、デザインシステムで定義したUIコンポーネントや実装したWidgetなどをカタログのように表示することができます。 デザインシステムとして用意しているUIコンポーネントをカタログのように見れることで、途中からチームに参画したメンバーもUIコンポーネントとして用意してあるものを一覧で見ることができたので、画面実装する際もスムーズに行うことができたという声もありました。 画面実装が進むにつれて実際の画面で確認することなどが増えましが、特定条件でのみしか表示されないようなUIに関してはWidgetbookを使用して素早く確認することができとても重宝しております。 現在Widgetbook 3を使用していますが、Widgetbook 4では Goldenテスト を再利用することができる仕組みやモノレポ(monorepo)での使い勝手などを考慮した改善などが検討されているようなので、次のバージョンを期待しています。 まとめ メルカリ ハロのアプリ開発では、いくつかの技術選定が開発を進める上で大きな役割を果たしてくれました。 Flutterの採用により、少人数でのスタートにも関わらず、iOSとAndroidの両プラットフォームを同時にリリースすることができた。 将来的なパッケージの分割も見据えて、モノレポ(monorepo)でのプロジェクト管理にMelosを導入したことで、複数パッケージの管理が容易になった。 バックエンドとの通信にはGraphQLを採用することでサーバーとのやり取りがスムーズになり、状態管理の構成をシンプルに保つことができた。 開発当初からデザインシステムの構築を最優先に進めたことで、デザイナーとの共通言語ができ、Widgetbookを使ってカタログ化することで開発効率を大幅に向上させることができた。 私たち開発チームにとって、これらの技術選定と開発方針が、メルカリ ハロのアプリ開発を支える大きな力になったと感じています。 さいごに メルカリ ハロアプリの開発では、FlutterやGraphQLなどをはじめとする多様な技術を採用しましたが、あくまで現在のチーム状況やサービス内容において適した選択でした。まだまだサービス的にもシステム的にも改良の余地があるので、常により良いものを探求していきたいと思っています。 プロジェクトごとにフィットする技術や方法は異なると思いますが、今回ご紹介したメルカリ ハロでの事例が、みなさんにとって最適解を見つけるためのヒントになれば幸いです。 Links 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- メルカリではメンバーを大募集中です。メルカリ ハロの開発やメルカリに興味を持った方がいればぜひご応募お待ちしています。詳しくは以下のページをご覧ください。 Software Engineer, Frontend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Backend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, iOS/Android (Flutter) – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Site Reliability – Mercari/HR領域新規事業 (Mercari Hallo) QA Engineer – Mercari/HR領域新規事業 (Mercari Hallo) Engineering Manager – Mercari/HR領域新規事業 (Mercari Hallo)
こんにちは。メルカリのQAエンジニアリングマネージャーの @____rina____ です。今回は、連載『 Mercari Hallo, World! -メルカリ ハロ 開発の裏側- 』の第4回を担当します。 本記事では、メルカリ ハロのサービスローンチまでのQAプロセスを通じて、私たちはどのようにして安心・安全なプロダクトを迅速にリリースするための戦略を実行したか、具体的な方法とともに詳述しています。 この記事を通じて、以下の点についての理解を深めていただけることを目指しています: QAの役割とプロジェクト概要 効率的なQAアサイン戦略 成果物の透明性と管理ツールの効果的な活用方法 また、この記事を書くにあたり、私自身が学んだことや得た教訓についても触れています。これらの経験は、今後のプロジェクトにおいて更なる品質向上と効率化を目指す上で非常に貴重なものとなりました。 プロジェクト概要とQAの役割 「メルカリ ハロ」は、事業者や店舗である”パートナー”と、働き手である”クルー”を結びつけるサービスです。クルーは自身のスキルや時間を活用して働くことができます。「メルカリ ハロ」はメルカリのWorkチームという組織で開発をおこないました。また、このサービスにはWorkチーム以外にも多くのチームが密接に関わっています。例えば、事業者アカウントの作成やKYC、給与振込などはメルペイが担当し、経理はホールディングスのチームが、問い合わせ対応はメルカリが担当しています。 このサービスにおけるQAの役割は、サービスローンチまでに開発を最短かつ安心・安全に進めるための戦略を実行することです。具体的には、品質保証アプローチを駆使し、プロダクトの全体的な品質を高めるとともに、スムーズなリリースを実現することです。 QAエンジニアのアサイン戦略 サービスローンチの当初の計画では、私がすべてのQA活動およびテスト実行を担当する予定でした。しかし、組織や開発の規模が想定以上に大きくなったため、追加のメンバーが必要となりました。その結果、QAエンジニアの採用に加え、業務委託と外部ベンダーの協力を得て、QA活動を以下の3つの主要な役割に分けて進行することにしました。 なお、メルカリアプリにある「はたらくタブ」の開発はメルカリのメンバーが開発しました。 メルカリ(Workチーム)のQAエンジニア WorkチームのQAエンジニアは、テストプロセス全般に加え、以下のタスクを担当しました: オンボーディング資料の作成:これから参画するQAエンジニアやチームのために、オンボーディング資料を作成しました。 テスト設計:各ユーザーストーリーにAcceptance Criteriaを追加しました。Acceptance Criteriaについては、こちらのブログもご参照ください。参照: QAがAcceptance Criteriaにテストしたい項目を追加して、みんなでいつ何をつくるのか考えたよ プロジェクト進捗把握:QAの進捗だけでなく、開発の進捗も管理することで、スムーズなリリースを支援しました。開発の進捗はJIRAで把握したかったので、チケットも率先して作成しました。 各種ドキュメント作成:テスト計画書、テスト完了報告書などを作成しました。 他カンパニーのQAとの連携:今回のサービスでは他のカンパニーとのシステム連携が多かったため、障害を未然に防ぎ、スムーズに開発を進めるためにQA間での連携が必要不可欠でした。定期的なミーティングやドキュメントの認識共有、進捗確認などを通じてコミュニケーションを密に行うことを心掛け、カンパニー間で連携の齟齬が発生しないよう努めました。 各詳細については、後で詳しく説明します。 アルムナイの業務委託のQAエンジニア メルカリのことをよく知るアルムナイ(元社員)のQAエンジニアに業務を委託しました。本業との兼ね合いで時間が限られていたため、時間を最大限に活かしてもらう必要がありました。主に以下の役割を担いました。: オンボーディング資料の更新と作成:新しいチームメンバーがスムーズに参加できるよう、オンボーディング資料を作成および更新しました。 エピック単位のテストと探索的テスト:エピックとは、機能のまとまりを表す単位です。私たちはそれぞれのエピックにユーザーストーリーを紐づけます。通常はユーザーストーリーごとにテストを行いますが、業務委託のメンバーは稼働時間が限られているため、彼らの能力を最大限に活かすために探索的テストを中心に実行しました。 この際、エピックごとにテストを実施し、エピックに対応するユーザーストーリーのテストを行いました。こうして、各機能がうまく連携して動作するかを確認しました。また、Acceptance Criteriaに縛られずに、探索的にテストすることで予期しないバグや問題を早期に発見することを目指しました。 リグレッションテストの作成:サービスローンチ前に多くの機種やOSでも正常に動作することを保証するためにリグレッションテストを実施する必要があります。また、開発中は一貫した開発環境が整っていなかったため、リリース前に一貫した環境で動作することも目的としています。サービスローンチ前の状況では、機能開発に追われてリグレッションテストのケースを作成する時間が取れません。また、記載に一定のルールがないと、テスト実行が難しくなってしまいます。業務委託のメンバーはアルムナイのため、メルカリ時代の知見があり、機能開発に引っ張られずにリグレッションテストの方針を理解し、作成することができます。これにより、多くの人が一緒にテストを実行できる体制を整えました。 テスト設計レビュー:Acceptance Criteriaのレビューおよび修正を行いました。 ベンダーのQAエンジニア 以前メルカリに参画していたメンバーを中心に、外部ベンダーに参画していただき、以下の活動を行いました: テスト設計からテスト実行:テスト設計から実行を担当し、特定の分野における専門知識を活用しました。今回は開発期間もタイトであったため、ユーザーストーリー単位に実行できるものからテスト実行しました。また、他カンパニーとのシステム統合テストもキャッチアップからテスト実行まで実施しました。 リグレッションテストの設計から実行:システム全体の安定性を維持するためにリグレッションテストを実施しました。 以上のように、多様なメンバーと役割分担を用いたQA戦略を採用し、組織の規模と開発の複雑さに対応しました。QA活動の効率化と品質の確保を両立させるために、このような工夫を取り入れました。 成果物の透明性と管理ツールの利用 ここでいう成果物とは、QA活動において作成したドキュメント類を指します。今回は主に以下の成果物を作成しました。 テスト計画書 オンボーディング資料 テスト設計時のAcceptance Criteria リグレッションテストのためのテストケース QAダッシュボード プロジェクト管理ダッシュボード テスト完了報告書 これらの管理はJIRA、Confluence、TestRail、およびGoogle Spreadsheetsを使用して行いました。すべての成果物はクラウド上に保存され、社内で誰でも参照できるようにしました。これにより、現在の状況などにアクセスしやすくし、いつでも参照できる環境を整えました。 このようにして、成果物の透明性を高め、プロジェクト管理ツールを効果的に利用することで、プロジェクト管理の効率化と円滑なコミュニケーションを実現しました。 次にそれぞれの成果物について具体的に紹介します。 テスト計画書 テスト計画は、サービスローンチの成功に不可欠です。前述のとおり、QAに関しても共通して参照できるドキュメントが必要でした。そのひとつがテスト計画書です。 テスト計画書の作成には、国際標準である ISO/IEC/IEEE 29119のPart3 を参考にしました。ISO/IEC/IEEE 29119はソフトウェアテストの国際規格で、ソフトウェアテストに関するプロセス、ドキュメント、技術などを定義しています。Part3はドキュメントについて定義しています。今回は、主に以下の構成で作成しました: 用語集: プロジェクト内で使用される専門用語や略語の明確な定義を提供し、プロジェクト関係者間の誤解を防ぐためです。社内で使用されている他の用語集にないQA特有の用語も含めました。 テストの成果物: テストケース仕様、テストスクリプト、テストレポートなど、生成されるすべてのドキュメントをリストアップしました。 テスト環境: 「メルカリ ハロ」はリリース前ということもあり、開発中のコードはmainにマージして、開発(=テスト)環境でテスト実行をしました。一方で、事業アカウントや給与振り込みを担当するメルペイはすでにリリースされているサービスで、各開発チームで使用する環境を設定する必要がありました。そのため、「メルカリ ハロ」とのシステム統合テストで利用する環境は厳密に設定する必要がありました。そこで、テスト計画書に「いつからどの環境を使うか」を明確に記載しました。これにより、テスト環境の設定を誤ることのないようにしました。 テストの完了条件: テストが正式に完了するための具体的基準を定義しました。特にメルペイとのシステム統合テストにおけるテスト計画書は、多くのチームメンバーが参照する重要なドキュメントとなりました。 オンボーディング資料 オンボーディング資料はいくつかのパートに分けて作成しました。ドキュメントは「オンボーディングクエスト」という名前で、参画したメンバーが各自で進められるようにしています。また、ドキュメントが古くならないように、参画したメンバーがいつでも更新できる仕組みを整えました。オンボーディングクエストについては以下も参考にしてください。参考: Notionを活用したエンジニア向けオンボーディング オンボーディング資料には以下の種類があります: 全社共通のオンボーディング資料:会社全体で必要となる基本的な情報を提供します。 プロダクトに関わるメンバー用のオンボーディング資料:各プロダクトに関する詳細な情報を提供します。 QAエンジニア用のオンボーディング資料:QAエンジニアがテスト実行に使うための設定ページや、不具合発生時のチケット起票ルールなどを含みます。 QAエンジニア用の資料には、主に以下の内容が含まれています: 設定ページ:テスト実行に必要な設定方法を詳細に説明します。 チケット起票ルール:不具合発生時のチケット起票方法やルールを説明します。 テスト設計時のAcceptance Criteria テストケースはAcceptance Criteriaとして、すべてストーリーチケットに直接記載しました。これにより、エンジニアがセルフチェックに利用できるほか、ストーリー完了後のより大きなまとまり(エピック)単位でのテスト実行にも活用できます。テスト実行が完了した後には、QAレビューを行いますが、ストーリー単位でのレビューが可能なため、効率的に進めることができました。 リグレッションテストのためのテストケース 今回QAでは、ストーリー単位でも、エピック単位でも、それぞれでテスト実行をしてきましたが、サービスローンチ前にさまざまなOSやOSバージョンでの動作も担保する必要がありました。 そのため、基本的なシナリオに基づいたテストを実施する必要があり、これを達成するためにリグレッションテストケースを作成しました。このリグレッションテストケースを用いることで、多くの環境での一貫性と安定性を確保しながら、テストを効率的に実行することができます。 QAダッシュボード QAダッシュボードでは、進捗やバグの発生、解決状況を把握することを目的としています。このダッシュボードはJIRAのダッシュボード機能を使用して作成しました。主に以下の内容を表示しています: ユーザーストーリー毎のテストのステータス状況 システム統合テストの進捗状況 ユーザーストーリー毎の不具合対応状況 システム統合テストの不具合対応状況 プロジェクト管理ダッシュボード 全体の進捗状況や各タスクの進捗はJIRAのチケットに記載し、ダッシュボードも作成しました。このダッシュボードでは、全体の予定工数、現在の開発ステータス、週ごとの完了工数などを表示できるようにしています。さらに、これらのデータをGoogle SpreadSheetsでシンプルなグラフに変換し、全社ミーティングで進捗を報告するようにしました。このようにすることで、各ロールの進捗を把握するだけでなく、他のロールの予定や課題も共有しやすくなりました。 テスト完了報告書 テスト完了報告書はConfluenceで作成しました。テスト完了報告書はリリース判定会議でも使用されます。リリース判定会議とは、新しいサービスや大規模なプロジェクトの際に行われる会議で、私はプロダクトの品質を機能面で評価し、報告する役割を担っていました。最終的な承認はエンジニアリングのVPが行います。事前に承認条件を設定し、テスト完了報告書を使用して適切な評価を提供することで、スムーズに承認を得ることができました。 テスト完了報告書の項目の選定には、ISO/IEC/IEEE 29119のPart3を利用しました。この標準を採用することで、評価項目が適切に盛り込まれ、全体のテストプロセスの一貫性と透明性が確保されました。また、この報告書はJIRAと連携し、データが自動更新される仕組みを取り入れることで、報告の正確性とタイムリーな情報更新を実現しました。 おわりに 今回紹介した取り組みにより、「メルカリ ハロ」は大きな問題や遅延もなくサービスローンチをすることができました。今回の活動を通して、短い期間で様々な環境の変化に耐えうるQAの戦略を取ることができたのは大きな収穫でした。QAの役割は単なるテストの実施に留まらず、プロジェクト全体の品質と効率を向上させるための重要な貢献を果たしています。 また、今までのQA活動の経験から得た知識に加えて、JIRAを駆使することでタスクの管理と進捗の可視化を効果的に行うことができました。そして、今まできちんと取り組んだことがなかったISO/IEC 29119標準を用いたドキュメント作成を行い、運用に支障をきたさずに混乱もなくプロジェクトを遂行できたことも大きな学びとなりました。このようなツールと標準の活用が、開発の成功に寄与したと確信しています。 サービスリリース後、QAの役割は少しずつ変化していきました。各QAエンジニアは特定の領域を担当するようになり、Acceptance Criteriaの読み合わせを開始しました。 Acceptance Criteriaの読み合わせを行うことで、PM、デザイナー、エンジニア、QAの間で、Specレビュー(要件レビュー)よりもさらに詳細な議論が可能になります。また、UIのエンドツーエンド(E2E)テストの自動化にも取り組み始めています。 今後も、今回得た経験や学びを今後の開発に活かし、さらなる品質向上と効率化を目指して取り組んでいきたいと思います。 Links 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- メルカリではメンバーを大募集中です。メルカリ ハロの開発やメルカリに興味を持った方がいればぜひご応募お待ちしています。詳しくは以下のページをご覧ください。 Software Engineer, Frontend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Backend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, iOS/Android (Flutter) – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Site Reliability – Mercari/HR領域新規事業 (Mercari Hallo) QA Engineer – Mercari/HR領域新規事業 (Mercari Hallo) Engineering Manager – Mercari/HR領域新規事業 (Mercari Hallo)
はじめに こんにちは。メルカリ ハロでSRE TLをしている @naka です。 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- の3回目を担当させていただきます。 この記事では、メルカリの新規事業立ち上げにおけるSREの働きや役割に関して、紹介します。 メルカリでは、Platform Engineeringが提供するツールや仕組みを活用して、サービスを立ち上げていきます。新規事業立ち上げのチームだけで、完結するわけではありません。今回は、Platform Engineering時代の新規サービス立ち上げにおけるSREの役割と具体的な動きを、メルカリ ハロを例に取り上げて紹介します。SREが、Platform Engineeringとプロダクト開発チームと一丸となって「All For One」に動いてきた取り組みが少しでも臨場感を持って伝えられればと思います。 Platform Engineering x メルカリShops爆速立ち上げの知見 具体的な活動について触れる前に、まずは全体の背景について説明します。 今回のメルカリ ハロの立ち上げは、ソウゾウ時代の爆速立ち上げの経験とメルカリグループ全体の技術スタックを最大限活用する挑戦でした。 ソウゾウの爆速立ち上げの成功に寄与した技術やプロセスを活かしつつ、そこでの学びを踏まえメルカリグループ全体の知見の蓄積である共通の技術スタックを取り入れることで、新規事業の立ち上げスピードを最大限上げました。 メルカリのPlatform Engineering メルカリでは、Platform Engineeringがプロダクト開発チームを支え、その成果を最大限引き出すための環境を提供しています。メルカリのPlatform Engineeringの詳細は、 こちら を御覧ください。 メルカリグループでは、Kubernetesを基盤に使っており、基本的にすべてのマイクロサービスはKubernetesクラスタ上で動いています。開発チームが、Kubernetesクラスタを簡単に使えるためのツールがPlatformチームによって整えられています。 メルカリ ハロ初期開発メンバー メルカリハロの初期メンバーは、 爆速で「あたらしい出会いを繋ぐ」を創った、メルカリ ハロのエンジニアリング でも紹介があったように、私を含め、ほとんどが株式会社ソウゾウに所属していました。ソウゾウでは、メルカリグループとは異なる技術スタックを採用しており、この知識や経験も活かしてメルカリハロの開発を進めました。ソウゾウで開発していたメルカリShopsの技術スタックに関しては こちら をご覧ください。 ソウゾウではアプリケーション開発寄りの技術スタックにMonorepo、Go、GraphQL、Ent、PostgreSQL、Nextjsなどが採用されており、高いレベルの開発者体験が実現できていました。一方で、基盤に関しては、メルカリグループ全体で用いられているものとはやや異なる技術スタックが採用されていたため、全体として得られた知見や経験を活かしにくいこと、また技術スタックの違いにより人員配置における障壁となっていたことが課題として認識されていました。 役割と活動 「インフラ、ネットワーク、セキュリティ周りはお願いします」と言われて始まったSREとしての役割や活動は、多岐にわたります。今回は、その中でもメルカリらしく、「All For One」や「Be a Pro」を体現している部分をいくつか紹介したいと思います。 アーキテクチャ設計 初期メンバーであり、現在Engineering Headである @napoli と密に連携を取りながら、まずはアーキテクチャ設計やツール選定をしました。 技術スタックの詳細は、 第2回のnapoliの記事 で取り上げられていますが、大まかには、メルカリの技術スタックにアラインしつつ、スピード重視で開発ができるような技術スタックを選択してきました。 その中でも特にインフラ構成やツール選定などに関してはSREも積極的にリードしてきました。 最初のアーキテクチャ決めでは、チーム外との定例ミーティングを開き、メルカリ ハロの要件をまとめてソリューションのPros/Consを議論したり、ミーティング後持ち帰り調査をして、最終的なアーキテクチャが決まるまで、複数部署と連携しながら、メルカリグループの技術スタックのキャッチアップに注力しました。 冒頭で書いたように、もともと主な初期メンバーはソウゾウメンバーだったため、メルカリグループで使われている最新の技術スタックにあまり詳しくない部分もあるので、高速のキャッチアップが必要でした。 ここで、大きな助けとなったのが組織的なサポート体制です。メルカリ ハロのプロジェクトは、グループ内でも優先度が高く設定されたので、各Platformチームから手厚いサポートを受けることができました。 また、Platform DX (Developer Experience) チームからメンバーが1名メルカリ ハロのプロジェクトにアサインされたので、毎週の1on1やSlack上で、素朴なPlatformへの疑問や今のメルカリ ハロでの課題などざっくばらんにディスカッションする機会を設けてもらい、毎週新しい学びを得ながら確実に進捗する事ができました。 また、具体的な要件に応じて、関連するチームのメンバーに声をかけて、ディスカッションしてアーキテクチャ設計をしました。Web Platform、Network、Architect、SRE、Platform DX、IDPなど、本当に多岐にわたりました。 メルカリ ハロのアーキテクチャ設計やインフラ構築を通して30人近くのメンバーと一緒に働くことができ、メルカリの「Be a Pro」なPlatform Engineeringの「All For One」のサポートを最大限活かせたと感じています。 環境構築 アーキテクチャ設計や技術スタックの決定と同時並行で、プロダクトの開発は進んでいました。 この時、スピーディな開発環境構築はメルカリ ハロチーム全体にとってとても重要でした。 「機能実装はないものの全体が動くようになっていきている」というのを、なるべく早い段階でチーム全体に共有することで、さらに前進しようとする強い気持ちを後押しするためです。 Platform Engineeringで提供されているツールの中身を理解しながら、1から環境を設定していきました。現在広く使われている自社製ツールの導入に加えて、Platform Engineeringで新しく開発している将来スタンダードになるであろう新ツールの導入も積極的に行いました。今後、グループ全体で移行するPlatformツールをEarly Adoptorとしていち早く採用することで、Platform側のサポートをより多く受けることができると同時に、Platform側にとっても実際のユースケースからのFeedbackを得ることができるので、メルカリ ハロの立ち上げにとっても、メルカリグループ全体にとっても大きな意味を持つ決定だったと思います。各所のサポートや協力を取り付けることができ、開発環境の構築を無事に完了することができました。 一方、メルカリ ハロでは、一部の技術スタックはメルカリグループでも実績が多くないものもあります。例えば、Cloud SQL for PostgreSQLは、メルカリグループでも使用されているケースはまだ稀です。 こういったケースでは、ソウゾウ時代の知見や、今回新たに時間を割いて検証した結果を用いて、より安全、且つシンプルで使いやすい設定やツールを選定しました。 具体的には、IAM DB 認証のデータベースユーザを採用し、DB userをパスワードなしで管理することで、よりセキュアな設定にしました。 また、DBのSchema 変更時に、意図しない変更の適用によるインシデントを未然に防ぐために、atlasという Database schema migration ツールを導入しました。 atlasの導入により、毎回Schema変更からSQL fileを生成し最終的にApplyされるSQLをReviewerがPR上で確認できるので、より安全にDB schema変更を行うことができるようになります。 ドキュメント整備 アーキテクチャ設計、環境構築、ツール選定などの際には、ドキュメントに経緯を残すことを大事にしてきました。 新規事業の立ち上げは少人数でスタートするのと、スピードを重視するために、最初の設計時に考えたことや設定した作業記録がドキュメントに残らなかったり、ドキュメントはあるが分散してしまっているために、あとから入ってきた人が背景を理解するのが難しいという課題に直面することがあると思います。 例えば、アーキテクチャを一つとっても、最初は全員が認識できるくらいのシンプルなものから始まる事が多く、特にドキュメントにしなくても全員が頭の中で同じものを描く事ができます。しかし、開発人数が一気に増え、開発のスピードもあがると、全体のアーキテクチャがどうなっているのか、詳細に関しても、当初なぜこの選択をしたのかを全員が把握することがとても難しくなります。 そこで、アーキテクチャ設計や開発環境構築の作業と同時並行で、Wikiの初期構成を考えたり、今後のドキュメントの構成の枠組みを作ったりしました。 SREとして何か作業が必要になったものに関しては、基本的にすべて手順を残すようにしたり、アーキテクチャ設計時の細かい議論やSlack上でのやりとりも、なるべくあとから入った人が経緯を知れるように、Referencesにリンクを集約したりと、将来の生産性への投資を初期段階から行ってきました。 あとからJoinしたSREのメンバーも過去にやってきたことにキャッチアップしやすかったと言ってもらえて、ドキュメント整備は最初からやって良かったと思います。 Production Readiness Check 最後に、もう一つ今回のメルカリ ハロのリリースに関して、SREが積極的にリードしてスムーズなリリースに関与した任務を紹介します。 メルカリでは新しいサービスをリリースする前には、事前にProduction Readiness Checkというチェックを通過する必要があります。(参考: Production readiness checklist used for Mercari and Merpay microservices ) このProduction Readiness Check (以下 PRC)の項目は、Applicationの実装上の要件、Kubernetes、Database、Storageなどの基盤の設定項目、セキュリティなど100以上の項目に上ります。 チェックの結果は、新しいサービスをリリースするために通過しなくてはいけないリリース判定の中の一つの項目の提出物として取り扱われています。つまり、PRCの項目がすべて完了していることが、本番環境構築完了の印になります。逆に言うと、PRCを完了していないとリリースができません。 PRCをすべて完了するためには様々なチームメンバーに協力してもらう必要があります。初期段階で専任のSREは一人しかいませんでしたが、途中からはバックエンドエンジニアと Marketplace事業のSREの方にも、兼務としてメルカリ ハロのSREの業務に加わってもらいました。 3名体制になったあとは各メンバーの担当項目を決め、担当者が各項目で関連するメンバーにアプローチしながら、同時並行で進めました。バックエンドエンジニアと兼務のメンバーはバックエンド周りの項目をリードし、Marketplace側のSREメンバーにはモニタリング周りの整備を進めていただき、Web Platform チームにはWeb周りのロードテストを行ってもらいました。 また、全体の進捗を確認するために、三人で定期的に進捗状況を共有し、具体的にやることが明確になっていない項目に関しては、一緒に議論しNext Actionを決めました。 全員で何が何でも完了するぞという強い気持ちと落ちているボールは気づいた人が拾うという精神で、リリース期日までにすべての項目を完了することにつながったと思っています。 三人一丸となって完遂したプロジェクトとして、ここでも「All For One」を強く感じることができました。 学びと課題 SREとして新規事業にゼロから携われたのは、個人としても学びが多く素晴らしい経験となりました。また、チームとしても、Platform Engineeringとプロダクト開発者の距離を近づけ、より早く価値をお客さまに届けることに貢献できたと思っています。 一方で、今回の立ち上げで課題も多く見つかりました。新しいサービス立ち上げのために、ゼロから環境構築を完了するまでに3ヶ月近くかかりました。Platformに対する理解のキャッチアップ、チーム内での要件の確定、他チームとの議論や意思決定など様々な不確実性の高い課題を突破していくために時間がかかってしまった部分や、Documentationの不足や複雑な手順など改善の余地がある程度明確な部分も多々ありました。 今後、メルカリ内で新規事業立ち上げの際にはもっとスピーディに立ち上げられるように、Platform全体の改善、Platform EngineeringへのFeedback、リリース前のプロセスの改善など新規事業立ち上げを経験した私達だからこそ、Platformチームと一緒に改善していきたいです。 リリース後、熱が冷めないうちに、次の新規事業のために既存のプロセスを改善している真っ最中です。 まとめ メルカリ ハロの爆速開発の裏側でどのようにSREが動いていたかを一部ではありますが、知っていただけたでしょうか。「新規サービス立ち上げ期にSREはこう動くべきだ」という明確な責務はないと思っています。だからこそ、柔軟に自分ができることなら何でもやるぞ!くらいのスタンスでプロジェクトに携わってきました。 リリース後の安定稼働を担保するために、リリース前の立ち上げ期にSREが様々な面でプロダクト開発に携わるのは、助走路として開発メンバーと同じ方向を向いて飛び立つことができ、とても有効だったと思っています。 今回は、メルカリグループ全体からのサポートがあってこそのメルカリ ハロのリリースだったと心から感じています。この環境に自分がいれたことにとても感謝しています。そしてその感謝の思いを存分にメルカリ ハロ、そしてメルカリグループ全体に還元していきたいです。 今回新しいサービスをリリースしただけではなく、メルカリグループとして今後もっとスピーディに新しいサービスを立ち上げていけるように組織全体を変えていく任務を担っていると感じています。 これからも、まだまだ改善したいところは山程あります。こんな熱いメルカリ ハロで一緒に働くSREメンバーを募集中です!!!!!! Links 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- メルカリではメンバーを大募集中です。メルカリ ハロの開発やメルカリに興味を持った方がいればぜひご応募お待ちしています。詳しくは以下のページをご覧ください。 Software Engineer, Frontend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Backend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, iOS/Android (Flutter) – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Site Reliability – Mercari/HR領域新規事業 (Mercari Hallo) QA Engineer – Mercari/HR領域新規事業 (Mercari Hallo)
こんにちは、メルカリ Engineering Office チームの@yuki.tです。 メルカリでは「誰もが高い基準を志しながらお互いに成長できる組織」を目指し、メンバーが相互に学び合う仕組みや機会を大事にしています。 その仕組みの一つとして、社内技術研修「DevDojo」があります。 DevDojoでは、社内の有志によってメルカリで使用されている技術に関する研修を新卒エンジニアの入社タイミングに合わせて毎年提供しています。 そしてDevDojoの一部コンテンツは Learning materials Website で外部公開しています。 今年も4月に様々な研修が提供されました。このブログでは今年の研修の一部をご紹介します。 あらたに提供を開始したコンテンツもありますので、ぜひご覧ください。 技術研修DevDojoとは 新卒エンジニアのオンボーディングは、ビジネスマナーなどの働くうえで必要なことを学ぶ共通研修と、開発に関する技術的なことを学ぶ研修の2つで構成されています。 新卒研修の全体像については こちらのブログ でも紹介されています。 メルカリでは新卒オンボーディングのうちの技術研修をDevDojoと呼んでいます。これは技術開発を学ぶ場として「Development」と「Dojo(道場)」をかけ合わせて名付けられた、完全In-houseの社内研修シリーズです。 DevDojoでは、メルカリ・メルペイのエンジニアが講師として、社内で使用されている技術についてトレーニングやオンボーディングを提供しており、新卒エンジニアは、自分の配属や技術領域に関わらず、プロダクトに関する技術を幅広く学ぶことができます。 メルカリでは、プロダクトに情熱を持って改善するためには、自分の技術領域だけでなくプロダクト全体の理解が必要という考えから、各自の技術領域に限定せずに研修を受講してもらい、研修の実施には組織全体で優先度高く取り組んでいます。 また、研修は社内のメンバーであれば誰でも受講できるようにオープンにしており、こちらも技術領域や職務に関わらず興味のある内容に参加できます。 公開コンテンツはこちら Learning materials Website では、DevDojoで提供されている研修から一部のセッションを公開しています。 今年は新しいテーマのセッションが2つ追加されました。 どちらも、新しくエンジニアとしてのキャリアを歩み始めたメンバーにとって、大事にしてほしい視点や考え方に関する内容となっています。 そのほかの公開セッションもアップデートされています。 メルカリのエンジニアリング組織は、半数以上が海外籍社員のため、いくつかのセッションは英語で提供されています。研修には同時通訳が入り、語学のサポートをしています。 こちらが今年のメルカリ、メルペイの研修コンテンツです! Problem Solving ソフトウェアエンジニアリングを純粋な問題解決として考え、問題の認識から解決までをステップに分け、過去のプロジェクトを参考にしながら解説します。 DevDojoシリーズで初となる、 Principal Engineer によるコンテンツです。 Slide Ship Code Faster 様々なTech Companyで使用されている生産性指標を取り上げ、開発開始から機能リリースまでの時間を短縮するための開発およびエンジニアリングの実践方法について説明します。キャリアをスタートしたばかりのエンジニア向けに、キャリアの進展に関する具体的なステップも提供します。 Slide英語 Mercari Design Doc プロダクト開発に必要なDesign Docの基礎知識を解説します。また、良いDesign Docの書き方やメルカリでDesign Docをどのように使っているかについても説明しています。 Slide英語 Mercari Quality Assurance 安心安全に早い開発サイクルでサービスを持続的に提供していくためには、Quality Assuaranceは非常に大切です。メルカリでどのようなQAのプロセス、ツール、テクニックを使って問題を迅速に特定し、解決しているのかを解説します。 Slide英語 Merpay Quality Assurance メルペイでのQuality Assuaranceの考え方と重要性、そしてQAプロセスとして、開発プロセスのなかでのQAエンジニアの関わり方を解説します。QAエンジニアだけでなく、開発に関わる全員が品質について注意をはらうための取り組みも紹介します。 Slide日本語 / Slide英語 Mercari Incident Management メルカリにおけるインシデントマネジメントとそのベストプラクティスを紹介します。「インシデント前、インシデント中、インシデント後」の3つのフェーズを含む、インシデントジャーニーを説明します。また、インシデントレビューをどのように行い、レトロスペクティブの質を高めているのかについても取り上げています。 Slide英語 [Basic] Machine Learning メルカリではAIを使い、メルカリAIアシストなどユニークな機能を提供しています。このコンテンツでは、一般的な機械学習の考え方や、AI・MLの基礎知識について解説しています。また、メルカリでは実際にMLをどう実装しているのか、実際のプロジェクトについても紹介しています。 Slide英語 Mercari Mobile Development より使いやすいサービスを迅速に提供していくため、メルカリのモバイル開発はリリースサイクルや運用プロセスのルール化を行っています。メルカリのモバイルアプリ開発において実際に運用している開発サイクルとプロセスについて解説します。 Slide英語 Mercari Design System for Mobile 持続的に一貫したサービス体験をお客さまに提供できるよう、メルカリではDesign Systemにとても力を入れています。このコンテンツでは、モバイルにおけるDesign Systemの基礎知識から、メルカリで実際に行っているデザインの作り方、運用方法について解説します。 Slide英語 Auth Platform Onboarding メルカリグループが管理しているサービス間で安全に通信を行うために、認証と認可は切り離せません。本セッションでは、この認証基盤の基礎として、アクセストークンの役割や利用方法等について紹介します。 Slide英語 最後に メルカリでは「 Trust & Openness 」と「 Open Organization 」の企業文化に基づき、オープンなコラボレーションを奨励しています。 この考えのもと、新卒エンジニアには社内の有志エンジニアによってトレーニングやオンボーディングが提供されており、社内だけでなく社外にも組織や技術の情報を共有することで、業界全体へ貢献することを目指して研修コンテンツを公開しています。 今年は2つの新しいテーマのセッションを追加して公開することができましたが、研修の実施と公開には、多くのエンジニアの方々、チームメンバー、関係チームが協力し取り組んでいます。 今後も引き続き、DevDojoシリーズのアップデートを行い、公開していきます。 最後に、メルカリグループでは、積極的にエンジニアを採用しています。ご興味ある方、ぜひご連絡お待ちしております! Open position – Engineering at Mercari
こんにちは。メルカリ ハロのSoftware Engineer (Engineering Head)の @napoli です。 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- の2回目を担当させていただきます。 2024年3月上旬にメルカリハロという新しいサービスが公開されました。メルカリ ハロは好きな時間に最短1時間から働ける「空き時間おしごとアプリ」です。 この記事ではメルカリ ハロを作るにあたり、どういった技術スタックやアーキテクチャを選定したのか、さらにその背景と意思決定をご紹介したいと思います。 この記事で得られること メルカリ ハロで採用されている技術スタックやアーキテクチャの全体像 その意思決定の理由とプロセス これから新規サービスを立ち上げるうえでのヒント 主な技術スタック メルカリ ハロで利用されている主な技術スタックは以下のとおりです。 バックエンド Go Google Cloud Platform (GKE, Cloud SQL for PostgreSQLなど) GraphQL gqlgen ent. フロントエンド React / TypeScript Next.js Apollo Client (React) モバイルアプリ (メルカリ ハロ専用アプリ) Flutter / Dart また、バックエンドのアーキテクチャとしてはモジュラーモノリスを、リポジトリの管理方法としてはmonorepoを採用しています。 モジュラーモノリス (Modular monolith) メルカリグループとして「スポットワーク領域」と呼ばれる領域に参入するため、2023年4月頃に新しいチームを立ち上げました。発足当初はあくまで「PoC (Proof of Concept)」という立ち位置で、この領域でメルカリならではの価値を提供できるかどうか、その検証をしてからサービスを成長させていくという戦略で進められたため、サービスを少ない人数で急速に立ち上げることが求められました。(最初期はエンジニアが1~2名ほどしかいませんでした) こういった背景を踏まえ、バックエンド(サーバ)はモジュラーモノリスのアプローチを採用しました。メルカリグループの主力サービスであるフリマアプリ「メルカリ」は成長過程でモノリスからマイクロサービスに進化してきました。モジュラーモノリスはこれら2つのアプローチの中間に位置する戦略で、端的に言うとモノリスシステムの中にマイクロサービスの戦略を統合する考え方です。結果的に、この選択は正解だったと思います。 容易なサービス間連携 モジュラーモノリスではひとつのサーバにAPIサーバとして求められるすべての機能を含めます。すべての機能は同じプログラムで動いていますが、サーバの中では実際には「モジュール」という単位で機能は独立しています。そして各モジュールが連携することでAPIとしての機能を提供しています。(「モジュール」はメルカリ ハロでは実際には「サービス」と呼んでいます) ここでいうひとつとは「サーバプログラムとしてひとつ」という意味です(「デプロイの単位としてひとつ」とも言えます)。ひとつのサーバにすべてのサービスが実装されているため、当然ながらサービス間のRPC (Remote Procedure Call)は不要です。ひとつのAPIを提供するために複数のRPCを利用する可能性のあるマイクロサービス アーキテクチャとは異なり、プログラム内で関数を呼ぶことで機能を完結させることができます。サービス間のプロトコル定義やネットワークエラー時のハンドリングなどを考慮する必要がなくなり、実装量や設計の難易度を大幅に減らすことができます。 単一のデータベースとトランザクション バックエンドがモジュラーモノリスであることに加え、メルカリ ハロではメインとなるデータベースのインスタンスはひとつです。この構成の場合、データベースのトランザクション機能をフル活用することができます。メルカリ ハロにおいてもお給料に関する情報など、データの整合性が重要なケースがあります。その点でデータベースのトランザクション機構はやはり非常に強力で、マイクロサービスアーキテクチャで大きな悩みごとであったサービス間のデータ不整合の問題の大部分を気にしなくて良くなります。こちらも実装量と設計の難易度を大いに減らしてくれました。 少ないインフラの記述量 メルカリ ハロではIaaSとしてTerraformを採用しています。モジュラーモノリスは基本的に1つのサーバで運用するためマイクロサービスと比較してインフラ関連の記述量を減らすことができます。APIなどアプリケーションに専門性を持つエンジニアにとってはインフラの設定やその動作確認は思ったよりも時間が掛かることが多いと思います。少ない記述量でAPIの実装に集中できることはメルカリハロのクイックな立ち上げにおいて大きなメリットがありました。 気をつけるべきこと モジュラーモノリスはメルカリ ハロにおいて良い選択肢だった一方で、気をつけるべきこともあります。 大きな懸念のひとつは初期設計の難易度が上がりやすいという点です。何も考えずに作ってしまうと、単なるモノリスなシステムになってしまう可能性が大いにあります。正確に言うとモノリスであること自体が問題なのではなく、システムのモジュールやサービスが適切な責任範囲で分離されていないことが大きな問題になり得ます。適切な分離が行われていないシステムでは機能の再利用が難しかったり、一部の改修が思ってもみないところに影響を及ぼしたりします。複雑に相互依存した(絡み合った)システムは理解も難しいですし、テストも難しくなります。理解もテストも難しいということは障害が発生する可能性も高くなるということです。結果として時間が経てば経つほどスピーディな機能開発が困難になってきます。 マイクロサービスアーキテクチャの利点の一つは、この「モジュール/サービスの分離」がインフラレベルで「強制される」点だと思います。プログラムの単位が違うことに加え、データベースも一般的にはマイクロサービスごとに独立していることが多いため、あるサービスの変更は別のサービスには直接影響を与えません。もちろんどの粒度でマイクロサービスを分けるかにも依りますが、開発者は半ば強制的に「モジュール/サービスの適切な単位」を考えなければいけません。サービスとして独立しているため、責任範囲が明確にもなりやすいです。大規模組織にも相性が良く、例えば「このマイクロサービスはこのチームがオーナーを持つ」と言った戦略を取りやすくなります。 一方で、モジュラーモノリスではこの「強制」が良くも悪くもありません。ですが「モジュール/サービスの適切な分離」はマイクロサービスと変わらず重要な関心事です。モジュラーモノリスでは初期の設計者が慎重に設計を行い、各開発者が節度と理解を持って機能を実装していく必要があります。この点に難しさがあると思っています。 とはいえ、メルカリ ハロのように一定規模の新規プロダクトをクイックに開発するうえではモジュラーモノリスはおすすめできるアプローチだと思います。最初から大規模になることが約束されたプロダクトを作るのであればマイクロサービスアーキテクチャのような分散システムを採用するのも効果的だと思いますが、ほとんどの場合、システムを物理的に分離させるのはビジネス的にもプロダクトの規模が大きくなってからでも遅くはないでしょう。 なお、メルカリの別プロジェクトにおいてもモジュラーモノリスの採用例があります。メルカリ ハロの事例とは異なる「既存のモノリスからモジュラーモノリスへ移行する」というアプローチですが、こちらも参考にして頂ければと思います。 メルカリの取引ドメインにおけるモジュラーモノリス化の取り組み monorepo メルカリハロではmonorepoを採用しています。monorepoは、バックエンドやフロントエンドなど、システムを構成する複数のコンポーネントの独立性を保ちつつ、全てのコンポーネントをひとつのリポジトリで管理する手法です。 monorepoを採用してよかったと思う点をいくつか挙げてみます。 システム全体の見通しの良さ monorepoはシステムに必要なコンポーネントが全てひとつのリポジトリに集約されていることが大きな特徴です。これはシステム全体の見通しがとても良くなります。メルカリ ハロの立ち上げ期は開発メンバーの数が少なかったため、ひとりのエンジニアがバックエンドやフロントエンド、モバイルアプリを横断して実装することもありました。その際にコードが集約されていることは開発のしやすさに大きなアドバンテージがあります。「フロントエンドの仕様どうなっているんだろう?」といった確認を行いときに、自身のIDEやEditorのファイル検索機能を使えばすぐ該当の実装に辿り着きます。「別リポジトリに切り替えて、git pullして、ウインドウを切り替えて…」といったことをする必要がありません。言語の違いによる理解の難しさは当然ありますが、素早くシステム全体を調査することができます。 コードレビューのやりやすさ ひとつのGitHubリポジトリ上にプルリクエストが集まるため、異なる職種間でもレビューがしやすくなります。専門的な実装に関しては専門の知識を持つメンバーがレビューしたほうが良いですが、簡単な修正なら他の職種でも可能なことが少なくありません。メルカリ ハロではmain branchへのマージはプルリクエストへのApproveを必須としているので、レビューの速さは重要です。mainにマージできるまでの時間が短いとコンフリクト解消に掛ける時間も減り、QAもしやすく、本質的な作業に集中しやすくなります。もちろん複数レポジトリ(Multi Repository)でもできないことはないですが、monorepoのほうがやりやすいのは間違いないと思います。 GraphQLスキーマファイルの共有 メルカリハロではバックエンドとフロントエンド/モバイルアプリとの通信に(後述する)GraphQLを採用しています。バックエンド側で生成したGraphQLのスキーマを同一のリポジトリで共有できるため、それをもとにフロントエンドのGraphQLのクライアントコードを自動生成したりなど、楽に連携をすることができました。GraphQLスキーマファイルに限らず「必要なファイルをリモートから取得する必要がない」というのは何かと便利ですし、開発環境が安定します。 一緒に開発している感 急にふんわりとした話になってしまいますが、バックエンドやフロントエンドといった職種間でもなんとなく「一緒に開発している感」が出るような気がします。「システム全体の見通しの良さ」にも繋がる話ですが、他の職種の人達がどういった頻度、温度感で日々作業しているかがわかりやすくなります。この感覚は密にコミュニケーションが必要な開発では意外と重要だと思います。目には見えなく数値化しづらいですが、メルカリ ハロの開発体制では良い効果をもたらしていたと思います。 monorepoのリポジトリ構成 メルカリ ハロではGo, dart, TypeScriptが主な言語として使われており、リポジトリルートの直下に各言語ごとにディレクトリを配置しました。これによりエコシステムやCI/CDの管理をしやすくなります。また普段の開発においても、例えばバックエンドを開発する人間は基本的にgoディレクトリ以下のみに絞って作業することができ、同じリポジトリでも独立した環境のように開発できるメリットがあると思います。 独立したビルド環境 メルカリ ハロのmonorepoではバックエンドやフロントエンドなどの各コンポーネントでのビルドの手段は基本的に独立しています。 Bazel のようなビルドを一元的に管理できるツールもありますが、メルカリ ハロでは採用していません。幸いなことにメルカリ ハロの立ち上げ期には各職種で専門性の高いメンバーが居たため、馴染みのある(標準的な)ビルドの手法を採用していました。各メンバーにとっては追加の技術を学習するコストがない分、スムーズにビルド環境を構築できたと思います。運用面でも今のところ大きく困ったことはありません。コンポーネント間でビルドを一元的に管理する手法はメリットもありつつ、かなりの難しさもあるため、明確な理由がなければ各コンポーネントごとに独立してビルド環境を構築するアプローチのほうがおすすめできるかなと思います。 — monorepoについて、いくつかのメリットやメルカリ ハロでの具体的な構成例を挙げました。monorepoの一般的なデメリットはリポジトリサイズが大きくなりやすいところですが、昨今のネットワーク環境やローカル環境を踏まえると相当に大規模なサービスにならない限りほぼ気にすることは無いかなと思います。他にも細かいデメリットはありますが、総じてメリットのほうが大きく上回っていると感じます。新規サービスの立ち上げにおいてはおすすめできるアプローチだと思います。 インフラの全体像 メルカリ ハロではインフラストラクチャにGoogle Cloud Platformを全面的に採用しており、簡単な全体像は以下のようになっています。 バックエンドとなるGraphQLサーバ(Go)はGoogle Kubernetes Engine (GKE)を利用してひとつのサーバとして動いています。同じくNext.jsと、APIの前段となるGateweyもGKE上で動いてます。データベースはCloud SQL for PostgreSQL、メモリストアにはRedisを利用しており、CDNはFastly、 画像最適化(変換)サービスにはCloudflareを採用しています。 Google Kubernetes Engine (GKE) メルカリ ハロではバックエンドのインフラにGoogle Kubernetes Engine (GKE)を採用しています。主に下記の2つの理由で選定を行いました。 メルカリグループでの実績とノウハウ メルカリグループでは多くのサービスをGKEにて運用しており、Platformと呼ばれるチームが運用・保守を行っています。一般的にGKE(kubernetes)はインフラを専門としないエンジニアにとっては難解なことも多いですが、メルカリグループでは初期設定や開発を効率的に行うためのツールやドキュメント、ベストプラクティスが充実しており、Platformチームからの手厚いサポートを受けることもできるため、この点において困ることは比較的少なかったと思います。 エコシステムとの統合 メルカリグループの多くのサービスがGKEを採用しており、同じクラスタ内でgRPCによるサービス間通信を行っています。これにより、既存のサービスをセキュアかつ効率的に利用することが可能です。メルカリ ハロはいくつか既存のメルカリのマイクロサービスを利用する必要があったため、簡単かつセキュアにサービスを利用できることには大きなアドバンテージがありました。 他の選択肢 上記のとおり、メルカリ ハロではグループ内のサポートが充実していたことと、既存のエコシステムとの連携が重要だったためGKEを選択しました。ただ、やはり構築と運用において難易度の高さはあるため、ゼロから独立したサービスを作る場合や、専門のエンジニアが居ない場合はCloud Runのような構築や運用が簡単なServerless環境も選択肢としては大いにありだと思います。 バックエンド / Go バックエンドの実装はGoを採用しています。メルカリグループでの標準的な言語であり、メルカリ内で開発を行う上ではノウハウやリソースアロケーションの面で多言語と比べ圧倒的な優位性があります。また、API開発に適した言語であり、実行速度が速く、Go routineによる並列処理は非常に強力です。 シンプルで読みやすいことも良い点で、そのシンプルさゆえに誰が書いても同じようなコードになります。これは多人数開発ではメリットが大きく、実装の理解やコードレビューの負担を大きく下げてくれます。コードに問題がある場合も比較的気付きやすいと思います。 複雑なコードは、たとえ自分自身が書いたコードでも何を意図してそうしたのか、時間が経つとすぐ分かりづらくなります。そういった意味では「書き手より読み手にやさしい」言語かもしれません。読み手に優しいことは長期的にサービスを運営するうえで大きなアドバンテージになります。個人的にもGoは好きな言語なので、仮にメルカリ以外でAPIを開発するとなっても当分は最有力候補になると思います。 Cloud SQL for PostgreSQL / ent. / atlas DatabaseにはCloud SQL for PostgreSQLを採用しています。メルカリグループではGoogle CloudのSpannerを採用するケースが多いのですが、以下の理由でCloud SQL for PostgreSQLを採用しました。 学習コストの低さ PostgreSQLのようなRDBMSの知識や経験を持つエンジニアは多く、そういったエンジニアとっては新たに学習すべきことは少ないです。それはつまり新しいメンバーが開発に入りやすく、即戦力になりやすいということに繋がります。 充実したエコシステム PostgreSQLは歴史が長く、サードパーティ製のツールやライブラリが豊富です。ツールやライブラリが豊富であることは効率的な開発に繋がりやすく、小さくないアドバンテージとなります。高機能なGUIツールも提供されているため、データを直接調整しながらデバッグを行いたいときなどに非常に役に立ちます。 ポータビリティ性 PostgreSQLはDockerイメージとして提供されており、ローカルのDocker上で簡単に動かすことができます。そのためデータベースを利用するユニットテストもやりやすくなりますし、ローカルでもリモートの開発サーバと近い環境を構築しやすくなります。 メルカリ ハロのサービス特性 メルカリ ハロのサービスの特性上、Readが多く、Writeは比較的少ないです。そのため単一のインスタンスでも相当な期間、問題なくWriteを捌けるだろうと判断しました。ReadにおいてはRead replicaを必要に応じて増やしていくことでかなりのトラフィックを捌けるだろうと考えています。 — 一般的にはこれらに加えて「初期コストの低さ」もメリットになりうると思います。メルカリ ハロでは当初から一定以上の規模のお客さまを想定していたためあまり判断の基準にはなりませんでしたが、多くの新規サービスにとっては重要な観点ではないでしょうか。 ORM ORM(Object-Relational Mapping)としては ent. を採用しています。Go言語向けの強力なORMフレームワークであり、メルカリグループ内でもいくつか採用事例があることから採用しました。コードファーストのアプローチを取っており、高度なクエリ生成機能もあるので、効率的にGoからDatabaseの操作を行えていると感じます。 一般的にはORMを採用すると最適化された柔軟なクエリを書きづらくなりますが、メルカリ ハロでは基本的に非常にシンプルなクエリの組み合わせでAPIを実現しています。その分クエリの発行数が冗長になることもありますが、一方で「理解しやすく、実装しやすい」という大きなメリットがあります。冗長なクエリが多いと心配なのはパフォーマンスですが、基本的にReadの処理はRead replicaを増やすことでスケールしますし、アクセスの頻度が相当多いAPIで無い限り、インデックスさえ適切に貼られていればクエリの数を多少増やしても問題になることはほぼありません。シンプルなクエリは正しくインデックスを貼るのも楽です。実際においても、いまのところメルカリ ハロはデータベースのパフォーマンスは大きな問題にはなっていません。 データベース マイグレーション データベースのマイグレーションには atlas を採用しています。ent.もauto migrationの機能を持っており、差分のDDLを自動で適用してくれたりしますが、いざサービスの運用が始まるとent.のauto migrationだけでは要件を満たさないケースが多く、基本的にはatlasによる管理に統一しています(本番環境においてent.のauto migrationはOffにしています)。atlasはent.と連動してスキーマの差分を自動的に生成してくれるなど、強力な機能を持っており、効率的にmigration作業を行うことができます。DMLにおいてもatlasを使って一部migrationしています。 GraphQL モバイルアプリ含むフロントエンドとバックエンドとの通信にはGraphQLを採用しています。GraphQLはAPI開発においてモダンな選択肢のひとつであり、世の中の多くのサービスでも採用されています。フロントエンド側でフェッチするデータを動的に制御することができ、フロントエンドの仕様が変更になった際もバックエンド側の修正が不要になるケースもあります。クエリをネストすることができるため、フロントエンドはその画面において必要な情報の多くをひとつのAPI Callで取得することができ、不要なAPI Callを減らすことができます。また、静的な型システムを持つスキーマによってI/Fが定義されるため、バックエンドとフロントエンド間で厳密なデータのやりとりすることができます。IDE/Editorによる補完が効きやすいところも嬉しいポイントです。 gqlgen バックエンド側ではGoのGraphQLサーバ実装のひとつである gqlgen を採用しています。シンプルかつ必要十分な機能が揃っており、学習コストも低く使いやすいと感じます。 gqlgenはスキーマファーストのフレームワークであり、基本的にひとつのスキーマファイルでQuery/Mutationを定義するスタイルのため、メルカリ ハロでもひとつのSchemaファイルにすべてのQuery/Mutationが集約されています。そのため現時点でもかなり行数の多いファイルになっており若干の扱い辛さを感じるときもありますが、graphql-eslintを導入してファイルを自動整形したりアルファベット順にType/Query/Mutationを自動ソートするなどして、できるだけメンテナンス性が落ちないように工夫しています。 一方で、スキーマがひとつのファイルに集約されているメリットも多いと思います。見通しが良く、コード自動生成もしやすく、他チームにメルカリ ハロが持つAPIを紹介する際も「このスキーマファイルを見てください」と言えば済むこともあります。 シンプルで扱いやすいPlaygroundが提供されている点も大きいと思います。Playgroundを使うと実装したGraphQLのQuery/Mutationを実際に試すことができます。入力の補完が効いたりQuery/MutationのAPI Reference(Document)も自動で生成してくれます。これが非常に快適で、デバッグや動作確認でとても役立っています。gqlgenでは複雑な設定もなく簡単にPlaygroundを構築することができます。 一方で、gqlgenに限らずですが、GraphQLサーバ実装においてはいわゆるN+1問題に気をつける必要があります。この点においてはRESTなど比べて学習コストと実装コストはやや増えますが、GraphQLを採用する上でそこまで大きなデメリットにはならないかなと思います。対処法としてはdataloaderの採用が一般的で、メルカリ ハロでは graph-gophers/dataloader を採用しています。 なお、メルカリグループで広く採用されているプロトコルにProtocol Buffers(gRPC)があり、こちらも優れた機能を持っています。ただ一般的なWebサービスを作るうえでは、フロントエンドとバックエンド間の通信プロトコルとしてはGraphQLのほうが総合的に扱いやすいのかなと感じます。(もちろんどういったサービスを作るかにも依りますが) あとはRESTも候補になり得ますが、今の時代、明確な理由がない限り敢えてそれを選択するメリットは少ないかなと思います。 React / TypeScript / Next.js メルカリ ハロではWebベースのフロントエンドも実装されています。メルカリアプリ内の「はたらく」タブはWebViewで提供されており、事業者様向けの「事業者管理画面」もPC向けにWebベースで提供されています。 メルカリアプリ内「はたらくタブ」と事業者管理画面 Reactの採用はすぐに決まりました。メルカリグループの他のプロジェクトで採用されているという点で、Vueも候補に挙がりましたが、初期の開発メンバーがReactに慣れていたことと、作りたいサービスに対して十分な機能を備えており、効率的に開発を進められるだろうと判断して採用しました。業界的なトレンド、人材のプールという意味でもReactにアドバンテージがあるだろうと判断しました。 TypeScriptについても迷うことはありませんでした。フロントエンドにおいても昨今の開発では静的型付けは必須と言って良いですし、開発を効率的に進めていく上でさまざまなメリットがあります。Javascriptと比較して若干の難しさはあるかもしれませんが、いまや情報も豊富ですし、一定以上の知識や経験がある開発チームにおいてはほぼ問題にならないでしょう。 Next.jsはメルカリグループでの利用実績や使いやすさ、Reactをベースとしていること、パフォーマンスの良さなどから採用を決めました。 「はたらく」タブに関しては、メルカリアプリとして高いレベルの体験の良さが求められるので、必然的に描画速度も求められます。いまはまだそこまでフル活用されていませんが、Next.jsはパフォーマンス向上のための設定が柔軟にできるため、今後必要に応じて積極的に活用していきたいと思います。 GraphQLのClientとしてはApollo Clientを採用しています。Web フロントエンドにおける人気のフレームワークのひとつで、優れた機能を豊富に持っており、効率的に開発を進めることができます。社内でも採用実績があったため今回も採用しました。Reactとの統合にはReact Hooksを利用しています。 Flutter / Dart メルカリ ハロはメルカリアプリ内のサービスだけでなく、iOS/Android向けの独立したモバイルアプリも提供しています(ストアで「メルカリ ハロ」と検索してみてください)。 メルカリとは独立した「メルカリ ハロ」アプリ その基盤としてFlutter / Dartを採用しています。開発初期の検討時点では他にも以下の選択肢がありました。 iOS / Androidネイティブ (Swift/Kotlin) React Native WebViewベースのアプリ モバイルアプリの技術選定にはWebフロントエンドに比べかなり時間がかかりました。それぞれ同じくらいメリット/デメリットがあり、特に1. 2.についてはメンバーや立場によって意見が様々で、メルカリ ハロのチームだけでなく、メルカリグループ横断的に議論を重ねる必要があったため、決断が難しかったです。主に以下のような点が論点となりました。 開発コスト メンバーの習熟度 パフォーマンス 社内的なリソースアロケーション サードパーティ製のライブラリを含む、エコシステムの充実度 採用のしやすさ メルカリグループとしてのノウハウの集約/分散 色んな論点はありましたが、その中でもやはり「開発コスト」はとても大きな関心事でした。開発初期のチームのメンバーは少なかった一方で、やはり昨今の市場状況を踏まえてクイックなリリースが求められていました。iOS/Android両方をネイティブで開発するとなると単純に考えて2倍近くのコストが掛かりますし、両プラットフォームを同じタイミングでリリースできるとも限りません。チームとしては独立した専用のモバイルアプリの提供と、iOS/Android同時リリースはなんとしても達成したかったため、ネイティブでの開発はスケジュール面でのリスクが大きいと感じていました。 一方で(メルカリ ハロアプリではなく)メルカリアプリはiOS/Androidネイティブで実装されています。リソースアロケーションという意味ではメルカリグループ内では圧倒的な優位性がありました。社外にも当然ながら開発できる人は多いです。しかし前述のとおり初期メンバーは数が少なく、当時は様々な事情により他のチームからメンバーを確保できる保証もありませんでした。(なお、US版のメルカリはReact Nativeで実装されており、こちらもノウハウの面で優位性がありました) パフォーマンスについて懸念する意見もでました。この点についてはiOS/Androidネイティブが一番優れていることに議論の余地はありません。「Flutterのようなクロスプラットフォームで作っても結局ネイティブで作り直す必要があるのではないか」といった指摘もありましたが、現時点では最高のパフォーマンスを追求するよりもまずはリーズナブルにサービスを立ち上げ、お客様に利用してもらうことが何より大事だと判断しました。幸いにもサービスの特性上、iOS/Androidのパフォーマンスをフルに求められるケースは今のところ多くありません。なお、「メルカリアプリ」もサービス開始してから4年ほど経った頃にフルスクラッチでアプリを再開発しています。まずはサービスがそこまで軌道に乗ることが大事ですし、数年後にやってくるかもしれない再開発のタイミングで必要があればiOS/Androidネイティブに切り替えるでも良いだろうという判断になりました。 最終的にはこれらの論点を総合的に踏まえ、Flutterがメルカリ ハロにとっては一番マッチしそうという判断をしました。 メルカリ ハロというサービスだけでなく、メルカリグループとしてみたときにこの判断が本当に正解だったかは今も分かりません。ただ現状から考えると開発コストやパフォーマンスなど、総合的にバランスの取れた開発環境になっていると感じるので、妥当な決断だったのかなと思っています。 おわりに メルカリ ハロで使われている技術スタックやアーキテクチャ、その意思決定に至るプロセスについて、ほんの一部ではありますが簡単にご紹介しました。 新規サービスを立ち上げるうえで技術選定は非常に難しいことだと思います。ビジネスを成功させるために様々な観点から意思決定を行う必要があります。さらに一度決めたものをあとから変更するのは現実的に不可能なことがほとんどであるため、責任は重大です。ただ、同時に多くのエンジニアにとって「楽しく、やりがいのある瞬間」でもあるはずです。 会社の規模やそれぞれの状況によって判断の基準が変わってくるため正解はありませんが、メルカリ ハロではこのように技術選定してきました。これからサービスを立ち上げるみなさんにとって少しでも参考になれば幸いです。 Links 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- メルカリではメンバーを大募集中です。メルカリ ハロの開発やメルカリに興味を持った方がいればぜひご応募お待ちしています。詳しくは以下のページをご覧ください。 Software Engineer, Frontend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Backend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, iOS/Android (Flutter) – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Site Reliability – Mercari/HR領域新規事業 (Mercari Hallo) QA Engineer – Mercari/HR領域新規事業 (Mercari Hallo)
こんにちは。メルカリのVPoE Workの @godriccao です。『 連載:Mercari Hallo, world! -メルカリ ハロ 開発の裏側- 』の1回目を担当させていただきます。スピードがクリティカルであるメルカリ ハロの展開を支えるエンジニアリングを紹介いたします。 実現したい世界は「新しい出会いを繋ぎ、信頼と機会をひろげる」 メルカリのミッションは「あらゆる価値を循環させ、あらゆる人の可能性を広げる」です。フリマアプリ「メルカリ」でのモノの循環に始まり、これまで「お金」「信用」「暗号資産」を循環させてきました。 今年3月には空き時間おしごとサービス「メルカリ ハロ」を一都三県で提供を開始し、わずか1ヶ月で登録者数250万人に到達、 4月16日より全国展開 しました。いつもお使いのメルカリに「はたらく」タブが追加されると共に、「働く」機能に特化した専用アプリもリリースし、「時間・スキル」を循環の輪に追加しました。 メルカリ ハロのミッションは「新しい出会いを繋ぎ、信頼と機会をひろげる」です。スポットワークの新しい働き方で、「人、場所、おしごと」と新しい出会いを作り、社会課題を解決しながら、新しい価値の循環を広げたいと考えています。 とにかくスピードが重要 スポットワーク市場はネットワーク効果が働いています。おしごとを探す側とおしごとを提供する側、数が多ければ多いほど需給のマッチング効率が上がり、おしごと成立のチャンスが増えます。 またスポットワーク市場は発展途上で、すでに一定のプレイヤーが存在しています。この市場では、ネットワーク効果の強さによって、初めて成長できるプレイヤーが勝つことができます。だからこそ、スピードが最も重要です。 新しいサービスを展開するとき、完璧に作ることよりも、お客さまの声を聞きながら高速にイテレーションするほうが大切です。 爆速開発スピードと品質を両立したメルカリ ハロのエンジニアリング メルカリ ハロのサービス開発に着手したのは2023年の4月からです。2023年10月までは極少人数でサービスの基礎をしっかり作り、10月からチームの人数を一気に増やしました。その後、「メルカリ ハロ」アプリ、事業者管理画面、カスタマサポートツールの機能をさせると同時に、フリマアプリ「メルカリ」の6つ目のタブである「 はたらくタブ 」も開発を鋭意に進めました。2024年3月6日には、上記のすべてのコンポーネントがメルカリ ハロサービスのローンチとともにリリースされ、更に多くの機能を追加し、4月16日に全国展開しました。 2200万超のMAUを持つフリマアプリ「メルカリ」のお客さま基盤をメルカリ ハロで活用するためには、機能面と性能面で一定以上の品質を担保することが必要です。では、なぜ爆速なリリースを実現しながらも品質を担保できたのでしょうか。 メルカリグループの All For One な総力戦 メルカリ ハロの成功は、メルカリグループ全体の総力戦によるものです。各チームの協力がなければ、短期間で高品質なサービスを提供することはできませんでした。具体的には、以下のチームが重要な役割を果たしました。 Architectチーム :開発初期のアーキテクチャー選定とリリース時の品質保証(PRC)に協力。彼らの専門知識と経験により、堅牢なシステム設計と高品質なリリースが実現しました。 Platform、Network、SREチーム :インフラ構築やトラブルシューティングに協力し、スケーラブルで信頼性の高いインフラを提供。これにより、サービスのパフォーマンスと可用性が確保されました。 メルペイとメルカリのFoundationチーム :IDP、KYC、加盟店基盤、決済基盤、- Growth基盤などのFoundation系サービスを提供。これにより、複雑なシステムも短期間にメルカリ ハロにインテグレートでき、品質も保証されました。 これらのチームからの手厚い且つプロフェッショナルなサポートと、しっかり整えられた基盤サービスの提供があったからこそ、短期間で高い品質の意思決定とサービスレベルの担保が可能となりました。 メルカリ ハロ組織の Move Fast と All For One メルカリ ハロの初期メンバーは、ほとんどが元々株式会社ソウゾウのメンバーです。ソウゾウのメンバーは、ベンチャー精神を誰よりも持ち、「 Move Fast 」というバリューを大切にしています。個々のエンジニアがオーナーシップを持って意思決定できるよう、ソウゾウのメンバーはメルカリ ハロのProduct、Design、Backend、Frontend、Mobile、QA、SREチームの骨組みを作り上げました。 メルカリ ハロ組織全体は初期から一丸となり、異なる職種間でも密に連携し、成功のためにあらゆる行動を取りました。エンジニアチーム内やProductチームとの連携はもちろん、Marketing、Customer Support、Sales、Partner Successチームとも密に連携できました。Customer SupportからのVoice of Customer、Partner SuccessからのVoice of Partnerを毎日シェアし、ソリューションを一緒に考えながら、開発の優先度を柔軟に調整してきました。 さらに、Salesの事業者商談にエンジニアも参加し、デモで企業のお客さまの心を捕まえた事例も、この組織の日常的なものになっています。このように一丸となることで、高速なイテレーションが実現しました。 No “Major” Incident メルカリ ハロはリリース後、2200万超のMAUを持つフリマアプリ「メルカリ」のユーザーにリーチ可能なサービスです。これだけの規模でサービスを提供するためには、一定以上の品質が求められます。品質の低いリリースは、ネガティブインパクトも非常に大きくなります。 そこで、私たちはリリース目標として「No “Major” Incident」を掲げました。この目標の背後には、大きなインシデントを発生させないという意図もありますが、同時に、スピードと両立するために、あえて「小さなインシデントは起こしても良い」という方針をチームに宣言しました。これは、スピードを重視しつつ、重大な問題を未然に防ぐための戦略です。 この方針により、チームは細かいトラブルを通して学び、システム全体の可用性を高めることができました。結果として、リリース後には大きなインシデントは発生せず、一定の品質を維持することができました。 メルカリ ハロエンジニアリングのこれから サービスローンチしたばかりなので、やりたいことはたくさんあります。 技術面から言うと、アルゴリズム、ML、LLMなどの技術を含めてうまく活用し、今まで存在しなかった感動的な「人、場所、おしごと」との「出会い」を作りたいと考えています。メルカリの強みであるお客さま基盤とデータをうまく活用し、メルカリ ハロで蓄積した新しいデータを含め、「新しい信頼と機会」をひろげたいと思います。 組織面から言うと、開発のボリュームと複雑度が指数的に増えている中、事業成長スピードと共にスケールする組織体制を作ることが個人的に一番楽しめる課題です。 長期的には、「働き方」や「雇用の方法」そのものが変わっていく転換期に突入していくと感じています。スポットワークを始め、「はたらく」という概念の転換を牽引するサービスになりたいです。そして、個人的にはメルカリ ハロ事業の海外進出の可能性にも期待しています。 最後に メルカリ ハロ開発チームから、より具体的なエンジニアリングエピソードをこれからお送りいたしますので、お楽しみにしてください! Links 連載: Mercari Hallo, world! -メルカリ ハロ 開発の裏側- メルカリではメンバーを大募集中です。メルカリ ハロの開発やメルカリに興味を持った方がいればぜひご応募お待ちしています。詳しくは以下のページをご覧ください。 Software Engineer, Frontend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Backend – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Site Reliability – Mercari/HR領域新規事業 (Mercari Hallo) Software Engineer, Machine Learning Leader – Mercari/HR領域新規事業 (Mercari Hallo) QA Engineer – Mercari/HR領域新規事業 (Mercari Hallo)
こんにちは!メルカリのQA Engineering Managerの @____rina____ です。先日、3月6日にメルカリグループの新規事業「 メルカリ ハロ 」がオープンされました。 メルカリ ハロは好きな時間に最短1時間から働ける「空き時間おしごとアプリ」です。仕事を探して、働いて、給与をもらうすべてをスマホで簡単に行うことができます。 またメルカリ ハロは、2023年4月に立ち上がったメルカリWorkチームという、メルカリグループの中の独立した組織で開発が行われています。システムもメルカリのメインのシステムからは独立したかたちで構成されており、メルカリグループとしての技術基盤を活かしつつ、さまざまな技術的なチャレンジを積極的に行っています。 そんなメルカリ ハロのオープンまでの約1年間の開発の裏側を、これから毎週公開していきます! 初日は、 @godric が執筆予定です。 公開に関しては、メルカリ公式DevX(旧Twitter) @mercaridevjp jでも随時お知らせします。ハッシュタグ #メルカリハロ開発の裏側 で検索してみてください。 メルカリの新しい事業での技術的チャレンジを広く届けられたらと思っていますので、どうぞお楽しみに!
こんにちは。メルペイ Engineering Engagement チームの mikichin です。 メルカリは、5月25日から開催される技術書典16にゴールドスポンサーをしています! メルカリ技術書典部では、有志メンバー5名による業務や趣味の技術についてまとめたものとメルペイ立ち上げ当時に戻れたらどんな技術選択をしていたかを振り返るインタビューをまとめた、ここでしか手に入らない2つの新刊を準備しています。 本記事では、新刊とメルカリ技術書典部が販売している本の購入方法についてご紹介します。 技術書典 について ITや機械工作とその周辺領域について書いた本を対象にした同人誌即売会。 技術者たちの「コミケ」とも言われています。 メルカリでは、技術書典3からスポンサーをしており、直近3連続ゴールドスポンサーです。また、有志メンバーで構成されたメルカリ技術書典部では、定期的に新刊を販売しています。 技術書典 16 オンライン開催:5月25日(土)〜6月9日(日) オフライン開催:5月26日(日)池袋・サンシャインシティ 展示ホールD(文化会館ビル2F) 新刊について 技術書典16では、メルカリ技術書典部は新刊を2冊準備しています。 Unleash Mercari Tech! vol.3 有志メンバー5名による業務や趣味の技術についてなどをまとめた一冊。 それぞれが非常に濃い内容となっているので、どれか1つでも興味を持っていただけるとうれしいです。 〈目次〉 第1章 AI時代にひねくれた選択? – 業務委託から正社員への変化球 第2章 CUEでBrainfuckインタープリターを作る 第3章 Goのエラーハンドリングを考える 2024 第4章 真剣に商業登記簿APIを作った話 第5章 OAuth 2.0 ClientをTerraform Custom Providerで宣言的に管理してみた URL: https://techbookfest.org/product/4JE8riJdXX5y1vBEYq7v8L Unleash Mercari Tech! vol.4〜メルペイ立ち上げ当時に戻ったら?〜 メルペイがリリースしてから丸5年。その当時、最善の選択をし開発をしてきていますが、メルペイのサービス拡充はもちろん、ビットコインが売買できるメルコインができるなど、(おそらく)想定していなかった状況に発展してきています。 そこで、「今の知識を持ったまま、メルペイ立ち上げ当時に戻るとしたらどうしてたか?」をテーマにインタビューを行い、まとめました。 各技術領域で、自社サービスの拡大や開発する上で提供されている機能のアップデートなど今の状況を踏まえ、今だったらあのときの開発をどのように進めていたかを振り返っています。 〈目次〉 第1章 Payment Platform編 第2章 iOS / Android編 第3章 Engineering Manager編 第4章 Platform Engineering編 第5章 SRE編 第6章 Architect編 第7章 元メルペイCTO編 URL: https://techbookfest.org/product/uVmfrDWUZd5JD5wJkPndxL オフライン会場で販売します! 5月26日、オフライン会場新刊含め、メルカリ技術書典部の本を販売いたします!入口入ってすぐの「協04」でお待ちしてます。 オンラインでも購入が可能です。 https://techbookfest.org/organization/47710001 執筆したメンバーの多くはブースにいる予定です。オフラインでご参加される方は、ぜひ会場でお会いしましょう!
search infra teamのmrkm4ntrです。我々の運用するElasticsearchにはFunction Score Queryを使ったリクエストが送られてきます。Function Score Queryはサブクエリのスコアに任意の関数を適用できるというもので、とても便利な機能ですが、同時にTop K(スコアが大きいものからK個を取得する場合)クエリ処理の最適化の恩恵を受けられなくなるという欠点もあります。この記事では、Function Score Queryに用いる関数の性質を利用し、Function Score QueryとTop Kのクエリ処理の最適化を両立させる方法について説明します。本記事は読者が検索エンジンの仕組みにある程度詳しいことを想定しています。 Top Kのクエリ処理の最適化 Elasticsearchの検索機能を提供しているライブラリLuceneには、Top Kを取得する際に、Top Kに入る見込みのないもののスコア計算をスキップすることで、パフォーマンスの最適化を図る機能が存在します。 例えば( ”search” OR “engine”)のようなクエリがあり、”search”というtermに対応するposting listの最大スコアが5.0、”engine”というtermに対応するposting listの最大スコアが3.0だとします。 BM25 ( https://ja.wikipedia.org/wiki/Okapi_BM25 ) にて各termに対するドキュメントはスコア付けされるため、indexの構築時に最大スコアが決まります。両方を含む文書のスコアは5.0 + 3.0 = 8.0になります。 ここでスコアの高い順にTop 10を検索することを考えます。この時大きいものから10番目のスコアがmin competitive scoreと呼ばれるものになります。つまり、このmin competitive scoreよりも大きいスコアをとりえない文書はスコア計算する必要がありません。 仮にmin competitive scoreが3.0より大きい値とします。この場合”engine”のみを含む文書のスコアはmin competitive scoreより大きくはならないので”engine”のみを含む文書のスコア計算はスキップできます。本来ならばORはそれぞれのtermのposting listを全て走査する必要があるのですが、”search”のposting listに存在する文書のみのスコア計算をすれば良いことになります。このような手法によりクエリのパフォーマンスを向上させることができます。 Function Score Query Function Score Queryは以下のようなクエリです。クエリを実行した結果はそのまま使うのではなく、サブクエリを実行した結果にfunctionsで指定された関数の戻り値を結合したスコアを最終スコアとして利用します。デフォルトの結合方法は乗算ですが、 score_mode の値によって動作を変更できます。 { "query": { "function_score": { "query": { … // サブクエリ }, "functions": […], "score_mode": … } } } 上記のTop Kクエリ処理の最適化は最大スコアがindex構築時に決まることが前提でした。Function Score Queryを使った場合、サブクエリのスコアに任意の関数の戻り値を結合することができるため、文書の最終スコアがクエリの実行時に決まることになります。このような状況下では先ほど説明したTop Kクエリ処理の最適化を使うことができません。 Function Score Queryを使うとTop Kクエリ処理の最適化がされていないのは、コードを確認すると明白です。min competitive scoreをLuceneのScorerに伝えるには setMinCompetitiveScore というメソッド( https://github.com/apache/lucene/blob/releases/lucene/9.10.0/lucene/core/src/java/org/apache/lucene/search/Scorable.java#L48-L57 ) を使うのですが、ElasticsearchのFunction Score QueryのScorerである FunctionFactorScorer においては setMinCompeitiveScore を呼んでいません。これによりTop Kクエリ処理の最適化がされていないことがわかります。 https://github.com/elastic/elasticsearch/blob/v8.13.2/server/src/main/java/org/elasticsearch/common/lucene/search/function/FunctionScoreQuery.java#L371-L487 TopKクエリ処理の最適化が可能な関数 確かに任意の関数に対してTop Kクエリ処理の最適化を実現するのは不可能です。しかし、利用する関数によってはTop Kクエリ処理の最適化の恩恵を受けれるものも存在します。例えばよく使われる、作成日時からの時間経過でスコアを指数関数的に減衰させる以下のようなFunction Score Queryです。 { "query": { "function_score": { "query": { … // サブクエリ }, "functions": [ { "exp": { "created_time": { "scale": "10d", "decay": 0.8 } } } ], "score_mode": "multiply" } } } このクエリでは、最終スコアは (指数関数的減衰 * サブクエリのスコア) となります。 重要なのはこの減衰は現在日時と作成日時の差において単調減少であるということです。 Luceneでは、posting listをあるフィールドの値で構築時にソートすることができます。posting listを作成日時の降順にソートすることで、上記の減衰関数を用いる際に、posting list内の前のドキュメントよりも必ず減衰値が大きくなることが保証できます。 これにより、サブクエリのみのmin competitive scoreが5.0、今の評価しているドキュメントの減衰が0.7だとすると、実質min competitive scoreを 5.0 / 0.7 = 7.14として扱うことができます。これは単にTop Kクエリ処理の最適化が使えるだけではなく、min competitive scoreがposting listを進むたびに増幅していくことになり、より多くのドキュメントの評価をスキップできる可能性が高まります。 PoCの実装とLuceneへの貢献 それをふまえて、前述の減衰関数を受け取りTop Kクエリ処理の最適化を実現するElasticsearchの新しいクエリをElasticsearchのpluginとして実装しました。基本的にはElasticsearchのFunction Score Queryと同じですが、サブクエリのScorerの setMinCompetitiveScore を適宜呼び出す部分が異なります。 実装自体は簡単でしたが、いざ動作確認すると全くパフォーマンスに変化がありませんでした。リモートデバッグで確認したところ、サブクエリ内のBoolean QueryにTop Kクエリ処理の最適化に必要な WANDScorer ( https://github.com/apache/lucene/blob/releases/lucene/9.10.0/lucene/core/src/java/org/apache/lucene/search/WANDScorer.java ) や BlockMaxConjunctionScorer ( https://github.com/apache/lucene/blob/releases/lucene/9.10.0/lucene/core/src/java/org/apache/lucene/search/BlockMaxConjunctionScorer.java ) が使われておらず、代わりに使われていたのは、それぞれ DisjunctionSumScorer ( https://github.com/apache/lucene/blob/releases/lucene/9.10.0/lucene/core/src/java/org/apache/lucene/search/DisjunctionSumScorer.java ) と ConjunctionScorer ( https://github.com/apache/lucene/blob/releases/lucene/9.10.0/lucene/core/src/java/org/apache/lucene/search/ConjunctionScorer.java ) でした。 調べてみたところ、 WANDScorer や BlockMaxConjunctionScorer はオーバーヘッドが大きいためトップレベルの句の場合でしか利用されないようです。つまり ((A AND B) OR (C AND D)) のようなクエリの場合はORは WANDScorer が使われますが、ANDには ConjunctionScorer が使われることになります。サブクエリをトップレベルの句と認識させるためには、新しく追加したクエリからサブクエリの ScorerSupplier の setTopLevelScoringClause メソッド( https://github.com/apache/lucene/blob/695c0ac84508438302cd346a812cfa2fdc5a10df/lucene/core/src/java/org/apache/lucene/search/ScorerSupplier.java#L46-L54 ) を呼ぶ必要があります。そのように修正したところ、無事に WANDScorer と BlockMaxConjunctionScorer が使われるようになりました。 これでパフォーマンスが改善するかと思われましたが、相変わらず変化がありません。さらに調べると、 WANDScorer の下の ConjunctionScorer (ORの下のAND)と BlockMaxConjunctionScorer の下の DisunctionSumScorer (ANDの下のOR)が最大スコアとしてInfinityを返していました。 Luceneの実装を見ると確かにInfinityを返すようになっています。何故Infinityを返すのか全く意図が掴めずに頭を抱えましたが、トップレベルの句以外は最適化をしないという修正( https://github.com/apache/lucene/pull/12490 ) での漏れだということがわかりました。そこで、それぞれ最大スコアに正しい値を返すように修正したところ、ようやくパフォーマンスが大きく改善することが確認できました。同様にprofile APIにおいても修正漏れがあったため、以下のプルリクエストをLuceneのupstreamに送りました。今は全てmergeされています。 https://github.com/apache/lucene/pull/13031 https://github.com/apache/lucene/pull/13043 https://github.com/apache/lucene/pull/13066 上で実装したpluginを使って我々のワークロードを模したパフォーマンステストを実施したところ、既存のクエリの95pが約35%、99pが60%下がりました。さらにコストも約1/3削減できることがわかりました。 ただし、この最適化の恩恵を受けるためにはリクエストパラメータのtrack_total_hitsをfalseにする、もしくは十分小さい値(1,000以下)に設定する必要があります。というのも最低でもこの件数はヒットするドキュメントを検索する必要があるため、スキップ対象が少なくなるからです。歴史的経緯により、この値をすぐに小さくすることは難しく、また最近この最適化ができない形のクエリがテストされているため、この最適化を実際に我々の本番環境に適用できるかは検討中です。 さいごに この記事ではElasticsearchのFunction Score Queryに使われる関数の単調減少性を利用してTopKスコア処理の最適化の恩恵を受ける方法について述べました。このタスクでLuceneのスコアリング周りの内部実装についての理解が深まりました。また、仮説を実装し、何度もうまくいかない原因を潰していくサイクルは大変でしたがエキサイティングでした。もし仮にこのような最適化を適用できるクエリを利用されている場合は、試してみていただけると幸いです。
*Security & Privacy Divisionの原動力となっているバリュー、それは「By design, by default and at scale(設計で叶える、デフォルトに組み込む、スケールに対応する)」です。 Oktaのユーザーアクセス権の棚卸し作業をPlatform Security Teamに率いてほしいという依頼が寄せられました。このプロジェクトを進める中、私たちは過去の設定や慣習と向き合わなければなりませんでした。なぜなら古いやり方が残っていることで「by design」と「by default 」な管理が難しい状態だったからです。そのような状況にも関わらず、私たちは「at scale」で組織全体を網羅した検査を実施する必要がありました。 この記事では、これらの課題に私達がどのように挑んだかを説明します。 使用したテクノロジー: Neo4j: https://neo4j.com/ Okta: https://www.okta.com/ Slack: https://slack.com 概要 メルカリでは、従業員のSaaSへのアクセスのほぼすべてをOktaを使って認証しています。アクセス権とは、許可するのは簡単ですが取り消すことが難しいものです。 不要なアクセス権を一掃するため、Neo4jを使用して組織とアプリケーションへのアクセスをグラフ化し、ユーザーインターフェースにはSlackを使って調査を実施しました。 全社的に提供しているアプリケーション以外で、現在付与されているアクセス権がすべて必要なものかを全社員に聞き取り調査。 その後、各マネージャーにそられのアクセス権がそれぞれの職責と照らし合わせて妥当かを確認。 情報を集約後、自己申告に基づいて不要なアクセス権をOkta APIを通じて直接削除。 これらをこれらを実装することで社内全体を対象とした大規模な検査を行うことができました。 これまでの道のり メルカリは今年創業11年を迎えました。今でこそ中堅企業に成長したものの、多くの10代が思春期を通過するように、成長痛に似たいくつもの苦労を乗り越えてきました。会社の拡大に伴い新たな従業員の入退社を経験し、アクセス管理に関するニーズも変化していきました。新たに導入されるサービスもあれば、廃止されるサービスもありました。過去にアクセス権の付与を決定した根拠や理由も、現在に至る過程で失われてしまいました。 メルカリはSaaSに大きく依存しているため、IDを管理するソリューションとしてOktaとGoogle Workspaceを使用しています。今回、アクセスレビューのプロジェクトに着手した時点で、Oktaのみですでに約8000のユーザー、500のアクティブアプリケーション、1400のグループが存在していました。アクセス権の削除は退職のケースであれば比較的簡単です。しかし、社内異動の場合は細心の注意を必要とする作業です。また勤務年数の短い従業員であればアクセス権の整理も比較的簡単にできますが、勤務年数が長い場合は長年の間にアクセス権が増えてしまっており見直しが大変な場合もあります。その結果、秩序が失われ、そのせいで複雑さも増してクリーンな状態にするのが難しくなっていました。 プロジェクトの目標 Security teamの最終的な目標は、アクセス権の乱用よって引き起こされる潜在的な被害を可能な限り減らすことです。 アクセス権のクリーンアップによりさまざまな副次的効果が期待できます。 認証システムにおける無秩序さを減らす 各従業員/チームがどのようなシステムを使用しているか、より明確に理解できるようになる システムオーナーにその人のアクセスがまだ必要なのかについてヒアリングし、その調査結果をドキュメント化するというSecurity teamメンバーのストレスを軽減する どのように管理されているのか、それはなぜなのかについて理解するための時間を減らす もう必要ない可能性のあるSaaSを特定する クリーンな状態に基づいて、より優れたアカウントライフサイクル管理のパターンを作成する その他 考えうる戦略 「最小特権の原則」は、事故やインシデントのリスクを軽減する最善の方法のひとつであるものの、その適用と維持には相当な労力が必要であることが予想できました。 「最小特権の原則」を適用して最終目標を達成できるということは、私たちが以下のことを理解している(または把握している)という意味でもあります。 社内にどのようなシステムがあるか それらのシステムオーナーと管理者は誰か 誰がどのアクセス権を使ってこれらのシステムにアクセスできるか 各システムが処理し保存しているデータの種類は何か これらシステムが使用される可能性のあるビジネスプロセスは何か 各社員とシステム、また取り得る行動とそれに伴う結果との間にあるつながり Oktaのデータをもとに簡単に計算してみましょう。アプリケーションは500個あり、ユーザー数は8000です。それらが直接割り当てられている、または1400のグループを通じて割り当てられています。各アプリケーションには複数のユーザーがおり、各グループにも複数のユーザーがいます。アプリケーションによっては複数のグループが存在するものもあり、それを組織体制と全ユーザーにリンクさせると、メルカリ社内には20万を超える関係性が存在するという計算になります。この段階では、各ユーザーのアクセスレベル、各システムで処理・保存されるデータの種類、ユーザーにとってどのようなアクションが可能かすらも分かりません。 仮にOktaから得られる情報のみを起点としましょう。1秒間で判断を下すために必要な情報はすべて揃っているという前提の下、1件の関係性につき1秒かかるとします。それでも前述の20万件の関係性をレビューするには丸々55時間かかってしまいます。したがって、一人の人間が全員分のアクセス状況を見ることは明らかに合理的ではありません。 では、他にも実践できそうな方法はないか見ていきましょう。 戦略1:重要なシステムのみにスコープを絞る 重要なシステムはどれなのか?どのような条件に従って決めるのか?これらの条件を定義しようとすると、考えうる要素が多すぎて誰もが容易に迷子になってしまいます。でも魔法なんて存在しないのですから、どこかしら複雑さが残るのもやむを得ないことです。もし、重要なシステムや機密性の高い情報を含むシステムを特定するという方法を選んでも、誰か(またはどこかのチーム)がすべてのシステムに目を通し、それらが何に使われ、どのようなユーザーがアクセスすべきかを理解して分類しなければなりません。 ただ同時に、私たちは社内にあるシステムを大体把握できています。とりあえず手をつけて始めてみたほうが、一通り情報をかき集めてから目の前にそびえ立つ到底登れそうにない頂に絶望するよりも理にかなうはずです。そうでもしないと、いざ山頂に辿り着いたとしても、全員が疲れ果てているか、すでに会社を辞めた後かのどちらかになっていることでしょう。 もうひとつの問題は、このレビューを行っている間も社内の環境は変化し続けるということです。レビューが完了するまでの間に新たなシステムが導入され、そこにユーザーが追加され、それらシステムは新たなユースケースのために使用されることでしょう。川の流れを止め、その間に魚を数えるようにはいかないのです。 戦略2:フルスコープ、システムオーナーに依頼する システムオーナーに依頼するというのはどうでしょう?アプリの数は500。ユーザー数は1人の場合もあれば全従業員+業務委託が含まれる場合もあります。各システムオーナーが平均10システムを担当するとしても、50人がそれぞれ約4000件のアクセスを確認し、職務内容やサービスの性質、アクセスされるデータに基づき、これらユーザーがアクセスすべきか否かを判断をしなければならないことになります。どこかの時点で、少なくともいくつかの重要なシステムにおいては必要かもしれませんが、秩序のない初期の状態においては有効なアプローチとは言えません。 また、システムオーナーの多くはマネージャーやディレクターです。彼らの時間は貴重です。時間のない人は優先順位を意識するため、この業務はどんなに重要でも後回しにされる可能性が高いでしょう。 戦略3:まずユーザーに質問し、マネージャーにその回答を確認してもらう 他の誰かに聞く前に、まだシステムへのアクセスが必要かどうかをユーザー本人に質問することは可能です。 今回採用した方法はまさにこれで、まずは従業員に以下のように聞いてみます。 これらシステムすべて対してまだアクセス権は必要ですか?はい/いいえ/分からない 回答が集まったら(または期限が過ぎたら)、彼らのマネージャーに質問します。 各メンバーの役割と責任を考慮した上で彼らの回答をレビューし、それらアクセス権が適切かどうかを確認してください。 今回はそこまで実施しなかったものの、3段階目のレビューとしてシステムオーナーへの質問も考えました。 これらのチームはあなたの管理するシステムを使用しています。このシステムの用途を考えると、彼らがアクセスすることに問題はありませんか? この方法の場合、アクセス権を維持するか取り消すかの判断をアクセス権を実際に使用する人に委ねることになります。また、権限の確認を全従業員に割り振ることができるという利点もあります。残念ながらマネージャーにはメンバーがアクセスの必要性を主張しているアプリケーションをすべて確認してもらわなければなりませんが、求められているのは確認だけなので検査は比較的早く終わるはずです。妥当かどうかを簡単に確認するだけなら、通常ひとり5分もかかりません。場合によってはもう少しかかるかもしれませんが、DM(ダイレクトメッセージ)で確認することが可能です。 このプロセスを通して私たちは「Security TeamのAさんが給与システムのアクセス権を持っている」といった、本来であれば例外的なケースを発見したいと思っていました。もし本人が「必要だ」と言ったとしても、少なくともマネージャーにその妥当性を確認してほしいからです。 このプロセスを実施している間、「このアクセス権が付与されているなんて知らなかった」「そもそもこのサービスってなに?」といったコメントが数多く寄せられました。 Oktaの使われ方からして、今回選択した方法が完璧とはいえないことは分かっています。ですが、私たちはOktaでアプリケーションのアクセス権を付与しています。メルカリの場合、アプリケーション内で権限を付与することはほとんどありません。そしてこれはシステムオーナーに委ねられています。このようなやり方のため、そもそも最初からアクセスできる対象を制限することでかなりの違いが出てきます。さらに追加のクリーンアップは後からでもできます。その時に、いくつか重要なシステムを優先的に対応することも可能です。 プロセスの実施方法 さて、ここまでに「なぜ検査を行うのか Why 」、「どのシステムを対象とするか What 、「誰が回答し、誰がレビューするのか Who 」が明確になりました。次は、「どうやって全員に質問し、回答を集めるのか How 」です。 スプレッドシートでの検査(現実的ではありません) すべてのユーザー/グループ/アプリを含めると20万行になってしまいGoogleスプレッドシートには収まらないし、全員に開いてレビューするようお願いするのもばかげています。シートの完全性を確保することは可能ではあるものの、さらに多くの作業が必要となります。 Webベースでの検査(現時点では見送る) うまくいくとは思いつつも、少なくともこの段階では検査を実施するためのウェブページは作らないことにしました。 OktaのIdentity Governance Access Certificationキャンペーン機能(我々には有効ではありません) Oktaには Identity governance access certification という機能があります。Oktaが将来的にアクセスレビューとして使用されることを承知の上で一から設定されているのであれば、この機能を利用する方法はうまくいくでしょう。ここではオーナーは特定グループに割り当てられ、そのグループはアプリケーションに割り当てられます。キャンペーンを実施している間、グループオーナーはグループのメンバーがアクセス権を所有すべきかを確認するよう依頼されます。この方法は、グループオーナーがそのユーザーがアクセス権を持つべきかを判断できることを前提としています。グループは多くの場合チームを意味するため、メンバーの管理はマネージャーに委ねられるでしょう。そのチームグループは、アプリケーションオーナーから必要なアプリケーションに割り当ててもらう必要があります。しかし、Oktaには(現時点では)アプリケーションオーナーを定義する属性がありません。 通常のケースはこの方法で問題ないのですが、例外ケースの場合は他のグループを通じて管理する必要があり、その例外を理解できる人に割り当てる必要があります。 私たちの今の状態で考えると、グループ=チームではなく、通常(必ずではないものの)アプリケーションへのアクセスを許可するために使われているので、この方法は有効な策ではありませんでした。この状態は、これらグループにオーナーが割り当てられていないという意味でもあります。システムオーナーに Slack + バックエンド + Neo4j(選んだ方法) 私たちはユーザーインターフェイスとしてSlackを、バックエンドデータベースとしてNeo4jを使うことに決めました。バックエンドにグラフデータベースを使うことで、チーム、メンバー、そのマネージャーに対する問い合わせと、彼らがどのグループを通じてどのようなアクセス権を持っているかを(比較的)簡単に照会できるからです。とりあえず今回は、アプリケーション内で付与されたアクセスのレビューは対象外にすることも決めました。 このブログ記事の残り部分では、私たちが実施したプロセスを説明します。 検査を進めるためには、いくつかのステップを経る必要がありました。 組織構造を復元する Okta上のアプリケーション、グループ、ユーザー、すべてのメンバーシップとそれらの関係を復元する 組織とアクセスを記したグラフを作成する 各チームと従業員向け:Slackのフォームを作成し、どのアクセスがまだ必要かの確認を依頼する ユーザーからの回答を集める 各マネージャー向け:Slackのフォームを作成し、メンバーが必要だと申告しているアプリに同意するかどうか質問する。ユーザーからの応答がない場合はマネージャーに決めてもらう マネージャーからの回答を集める 妥当性の確認:明らかにおかしな回答がないかレビューする Okta APIを通じてアプリケーションへのアクセスやグループメンバーシップを取り消す 変更を記録する ステップ8を除く上記のすべての操作はコードを通じて行います。そうすればこのプロセスを確実に再現することができるからです。 組織構造とアクセス権をデータベースで表す Oktaのユーザーは、チームとマネージャーを示す属性を持つように設定することができますが、いくつか実際の組織構造との相違点が見られたため、最終的には別のソースから完全な構造を抽出し、その構造をOktaのユーザーとリンクさせなければなりませんでした。組織構造をグラフ化することで、Okta上の関係ではなく実際の組織構造を明らかにすることができたので非常に便利でした。 その後、Oktaから特定の組織単位や チームにおけるアプリ、グループ、ユーザー間の関係を抽出することができました。 イメージ1:Oktaと人事データをNeo4jグラフデータベースに統合し、Mermaid.jsで可視化 スキーマ:組織、チーム、マネージャー、メンバー、グループ、アプリケーション間の関係性 オーバーエンジニアリングを防ぐために、少なくとも最初のうちはいくつかショートカットを採用し、各従業員の単位としてOktaUserノードを使用することにしました。現実はもっと複雑な権限が付与された対象を特定する必要があるのですが、この段階ではこれで十分でした。 イメージ2:Mermaid.jsを使って視覚化したデータベース内における関係性の概略図 Neo4jデータベースへの書き込みが終わると、組織、チーム、各チームが使用しているアプリケーションを照会できるようになりました。組織構造のグラフはこのような様子でした。 イメージ3:Neo4jのウェブインターフェイスを使って作成したメルカリの組織構造図 以下のクエリは以下のような意味を表します: 「Platform Security」チーム直下のメンバーで、有効なOktaアプリにアクセス権があるすべてのメンバーに対して: マネージャーを取得する 直近90日間にこれらのアプリを使用したかどうかを取得する ユーザー・アプリ間の関係性のOrgノード、マネージャーノード、関係性のプロパティ、最終使用のプロパティ、およびアプリノードを返す これを元に再度、グループメンバーシップによるアプリへのアクセスを考慮します。 // Team: Platform Security MATCH (o:OrgUnit {name: "Platform Security"})<-[:IS_MEMBER_OF]-(u:OktaUser)-[r:HAS_ACCESS_TO]->(a:OktaApp {status: "ACTIVE"}) WITH o, u, r, a MATCH (u)-[:IS_REPORTING_TO]-(m:OktaUser) WITH o, m, u, r, a OPTIONAL MATCH (u)-[p:HAS_USED]->(a) RETURN o, m, u, PROPERTIES(r) AS r, PROPERTIES(p) AS p, a MATCH (o:OrgUnit {name: "Platform Security"})<-[:IS_MEMBER_OF]-(u:OktaUser)-[r:IS_MEMBER_OF]-(g:OktaGroup)-[:HAS_ACCESS_TO]->(a:OktaApp {status: "ACTIVE"}) WITH o, u, r, g, a MATCH (u)-[:IS_REPORTING_TO]->(m:OktaUser) WITH o, m, u, r, g, a OPTIONAL MATCH (u)-[p:HAS_USED]->(a) RETURN o, m, u, PROPERTIES(r) AS r, PROPERTIES(p) AS p, g, a Query1:Neo4j Cypherを使用して、特定のチームのアプリケーションとグループのアクセスリストを取得 プロセスを開始する コントローラー(アプリ)はユーザーを特定するためにチームのリストを使用しています。チームの再帰的リストは、次のようなクエリでNeo4jデータベースから簡単に抽出できます。 MATCH (t:OrgUnit)-[:IS_PART_OF*]->(o:OrgUnit) WHERE o.name = "Security & Privacy" AND t.status = "active" RETURN t.name AS team, t.orgId AS orgId, o.name AS orgName クエリ2:Neo4j CypherでSecurity & Privacyカテゴリの再帰的チーム階層を復元 ここからスコープ内のチームリストに基づいて、コントローラーから検査開始がマネージャーに通知されます。各チームメンバーにが作成され、SlackのDMで調査フォームが送信されます。 メンバーに調査フォームを送信する Image 4: Sequential flow chart detailing the member campaign process, illustrated with Mermaid.js. The assessment form sent to members is kept simple and is meant to be quick to fill. A user can click on the application name to connect to the app and confirm if they still need access to it, then select “Access needed” or “No need anymore”. イメージ4:メンバーのキャンペーンプロセスのフローチャート 回答収集用のバックエンド 調査フォームが送られたら、あとは回答を待つだけです。バックエンドで回答を受け取り、その回答に従ってNeo4jデータベースを更新する準備は整っています。 イメージ6:調査フォームからの回答を収集する手順のフローチャート 調査を実施している間、手動でマネージャーに進捗状況を送信し、未回答の場合はチームメンバーに確認してもらうよう依頼することができます。 マネージャーによる回答のレビュー 回答の回収が済んだら、未回答・未完了のメンバーがいたとしても、マネージャーにアクセスのレビューを依頼します。メンバーからの回答は一目瞭然であり、チームに関係するアプリケーションもよく知られているはずなのでこのステップは通常すぐに終わります。 マネージャーが対応しない場合は、その上司に進捗がないことを報告することができます。 マネージャーのレビューの流れは以下です: イメージ7:Mermaid.jsを使用して、マネージャーのレビュー作業のシーケンス図 マネージャーに送信されるフォームはユーザーに送られるフォームと似ていますが、必要だと回答されたアプリだけが表示されています。マネージャーはメンバーの回答を確認し、メンバーによりアクセスが必要だと判断されたアクセス権に対して、保持か削除を選択することができます。 イメージ8:Slack内のマネージャーレビューフォームのインターフェイスの例 不要なアクセスのクリーンアップ この段階では、メンバーからの回答が集まり、マネージャーからの確認も回収済みです。個別のアクセスレビューではなく、チーム単位でのアクセス権付与に同意するかの確認をシステムオーナーに依頼することもできましたが、これは今後の検査に回すことにしました。 Okta APIによるアクセス取り消しフローは比較的シンプルです。 イメージ9:アクセス取り消しメカニズムに関するステップのフローチャート まとめ 今回のプロジェクトを通して、従業員やマネージャーが正直に回答してくれると信じることで、従業員がどのアクセス権を持ち、どのアクセス権を必要としているかをレビューすることができました。多くの規格、フレームワーク、規制、ベストプラクティスでは、企業が定期的にこういったレビューを実施することが求められています。しかし、得てして複雑な組織構造や歴史的背景がからみあい、こういったレビューはすぐに手に負えなくなるものです。そこで、従業員とアプリケーション間の複雑な関係性をグラフデータベースに移行し、まず従業員にアクセス権が必要かどうかを質問することで、会社の規模に応じて検査の規模を拡大することができました。また、今回の検査は、システム分類作業に長い時間をかけることなく実施することができました。Oktaに大きく依存しているからこそ、Oktaに焦点を当てることで、大半のシステムをカバーすることができたのです。 このフローにもまだまだ改良の余地はあり、他のシステムへの拡張も可能だと考えています。アクセス許可のルールと確認をより厳格にし、プロビジョニングプロセスに組み込むこともできるかもしれません。 一方、今回私たちはすでに、アクセス中断のリスクを負うことなく、不要となった膨大な量のアクセス権を削除することができました。これは、アクセス権を取り消すか否かを判断する際に、こちらで決めたルールを使用するのではなく、従業員とマネージャーの回答に基づいて行ったからです。
こんにちは、メルカリの生成AIチームで ML Engineer をしている ML_Bear です。 以前の記事[1]では商品レコメンド改善のお話をさせていただきましたが、今回は、大規模言語モデル (LLM) やその周辺技術を活用して30億を超える商品のカテゴリ分類を行なった事例を紹介します。 ChatGPTの登場によりLLMブームに火がついたということもあり、LLMは会話を通じて利用するものだと認識されている方が多いと思いますが、LLMが有する高い思考能力はさまざまなタスクを解決するためのツールとしても非常に有用です。他方、その処理速度の遅さや費用は大規模なプロジェクトでの活用にあたっての障壁となり得ます。 本記事では、こうしたLLMの課題を克服するためにさまざまな工夫を施し、LLM及びその周辺技術のポテンシャルを最大限に引き出して大規模商品データのカテゴリ分類問題を解決した取り組みについて説明します。 課題 まずは今回のプロジェクトの背景と技術的な課題を簡単に説明します。 メルカリは2024年にカテゴリリニューアルを行い、階層構造を見直すとともに商品カテゴリの数を大幅に増やしました。しかしカテゴリ数やその階層構造がかわるということは、それに紐づく商品のデータも変更する必要があります。 通常であれば商品のカテゴリ分類は機械学習モデルやルールベースモデルを利用します。しかし今回のケースでは過去の商品に対する「新しいカテゴリ階層での正解カテゴリ」がわからないため、機械学習を使用した分類器を作成することができませんでした。また、カテゴリ数が非常に多いため、ルールベースモデルの構築も困難でした。そこで、この課題に対してLLMを活用できないかというアイディアが出てきました。 解決策: LLMとkNNによる2ステージ構成の予測アルゴリズム 結論としては以下のような2ステージ構成のアルゴリズムを組むことで今回の課題に対応しました。 ChatGPT 3.5 turbo (OpenAI API[2])で過去商品の一部の正解カテゴリを予測する 1.を学習データとして過去商品のカテゴリ予測モデルを作成 全てをChatGPTで予測できれば楽だったのですが、メルカリの過去商品は30億商品を超えるため[3]、全てをChatGPTで予測するのは処理時間的にもAPIコスト的にも不可能でした。そのため、紆余曲折を経てこのような2ステージのモデル構成としました。(すべての商品をChatGPT 3.5 turboで分類するとコスト見積もりは約100万ドル、処理時間見積もりは1.9年という非現実的な数字でした) 以下にモデルの内容を簡単に説明します。詳細については「工夫した点」で述べるため、一旦はシンプルな解説に留めます。 1. ChatGPT 3.5 turbo (OpenAI API)で過去商品の一部の正解カテゴリを予測する まず、過去に出品された商品を数百万点サンプリングし、ChatGPT 3.5 turboにその商品の「新しいカテゴリ構成での正しいカテゴリ」を予測させました。 具体的には、各商品の商品名や商品説明文、元のカテゴリ名をもとに新しいカテゴリの候補を10個程度作成し、その候補の中から正解を答えさせました。 2. 1.を学習データとして過去商品のカテゴリ予測モデルを作成 次に、1. で作ったデータセットを正解データとして、シンプルな kNN モデル[4] を作成しました。 具体的には、まず、1.で正解カテゴリを予測した商品のEmbeddingと正解カテゴリをベクトルデータベースに保存しておきます。その後、予測したい商品のEmbeddingを元に、ベクトルデータベースから類似商品をX個抽出し、そのX個の商品の最頻カテゴリを正解カテゴリとしました。 Embeddingは各商品の商品名、商品説明文、メタデータ、元のカテゴリ名などを連結した文字列をもとに計算しました。より複雑な機械学習モデルも検討しましたが、シンプルなモデルで及第点の性能が出たためシンプルなモデルを採用しました。 工夫した点 さて、ここからは今回のプロジェクトで工夫した点をご紹介します。以下のような点を工夫したので、ひとつづつ説明します。 OSSのEmbeddingモデルの活用 Sentence Transformers ライブラリによるMulti-GPUの活用 Voyager Vector DBによるCPU上での高速な近傍検索 max_tokensとCoTの活用によるLLM予測の高速化 Numba・cuDFの活用 1. OSSのEmbeddingモデルの活用 第2ステージのモデル (kNN) では商品のEmbeddingの計算が必要でした。自前でニューラルネットワークを組むことも可能でしたが、OpenAI Embeddings API ( text-embedding-ada-002 ) [5]で十分な精度が出ることが確認できたので、当初はこのAPIを利用する方針としていました。 しかし、試算してみたところ、すべての商品にOpenAI Embeddings APIを利用するのは処理時間的にもコスト的にも少し厳しいということがすぐにわかりました。 そんな中、MTEB[6]やJapaneseEmbeddingEval[7]を眺めていると英語以外の言語でもOpenAI Embeddings APIに匹敵するOSSのモデルが多数あることに気づきました。自分たちで評価用データセットを作って試してみたところ、OpenAI Embeddings API同等の精度が出たためOSSのモデルを利用することにしました。 私たちがこのプロジェクトを行なっていた2023年10月時点のデータでは、以下のモデルが高い精度を示しており、最終的には計算コストと精度のバランスを鑑み intfloat/multilingual-e5-base を利用しました。(MTEBのランキングは常時入れ替わっているため、2024年4月現在はもっと強いモデルがあると思います) intfloat/multilingual-e5-large [8] intfloat/multilingual-e5-base [9] intfloat/multilingual-e5-small [10] cl-nagoya/sup-simcse-ja-large [11] このように、OSSでも非常に高性能なEmbeddingモデルが存在しているため、Embeddingを利用するプロジェクトを行う場合は、シンプルな問題を作成して、OSSでも十分な性能を持つモデルがあるかどうかを確認してみることをお勧めします。 2. Sentence Transformers ライブラリによるMulti-GPUの活用 OSSモデルを利用することで OpenAI Embeddings APIに比べて飛躍的に処理速度が上がったものの、数十億商品を処理するにはもう少し改善が必要でした。 A100などの強力なGPUを利用できれば話が早かったのですが、世界的なGPU枯渇の影響を受けてか、プロジェクト実施時の2023年11-12月時点では強いGPUを掴むことはなかなか困難でした。(現在もあまり状況は変わっていないかと思います) そのため、V100やL4などのGPUを複数台並列で利用して対応することにしました。幸いなことに、Sentence-Transformers[12]ライブラリを利用すると以下のようなシンプルなコードで複数台のGPUを簡単に並列化できたため、非常に助かりました。 from sentence_transformers import SentenceTransformer def embed_multi_process(sentences): if 'intfloat' in self.model_name: sentences = ["query: " + b for b in sentences] model = SentenceTransformer(model_name) pool = model.start_multi_process_pool() embeddings = model.encode_multi_process(sentences, pool) model.stop_multi_process_pool(pool) 強力なGPUを大量に使えれば理想的ですが、それが難しい状況でも工夫次第で処理を高速化することができます。Sentence-Transformersのようなライブラリを活用して、限られたリソースを最大限に活用することが重要だと感じました。 3. Voyager Vector DBによるCPU上での高速な近傍検索 kNNを利用する際にはベクトルデータベースが必要でした。サンプリングしたとはいえ数百万商品の学習データになったため、GPUのメモリに載らない状況でした。A100 80GBなどの大きなメモリを持つGPUを使えば載ったかもしれませんが、前述の通り強力なGPUは確保が困難だったので試すことすらできませんでした。 そんな折、Spotify社製のVoyager[13]がCPUでも高速に動作すると聞いたので試してみたところ、実用に足る速度を簡単に実現できました。Embedding計算に比べると近傍探索の時間はそれほど影響が大きくなかったため厳密に他のプロダクトと比較していませんが、十分な速度を出すことができていました。 Voyagerにはメタデータ管理機能がなかったので自分たちでクライアントを書く必要がありましたが、それでも全体的には良い選択だったと思っています。 4. max_tokensとCoTの活用によるLLM予測の高速化 今回のプロジェクトでは ChatGPT 4 はコスト面から利用できず、ChatGPT 3.5 turboを使わざるを得ませんでした。ChatGPT 3.5 turboはコストの割に賢いとは思いますが、精度には少し不安がありました。そのため、Chain of Thoughts[14]を利用して説明を生成させることで精度向上を図りました。 皆さまもご存知かと思いますが、ChatGPTに説明を行わせるとずっと喋り続けることもあり、処理時間が問題となりました。そこで、 max_tokens パラメータを利用して長い話を途中で打ち切ることで処理時間の短縮に努めました。 回答を打ち切ると(Function Callingの) JSONが壊れるので、LangChain[15]のllm.stream()を利用したり、もしくは自分でJSONを復元してパースする必要があり少し手間がかかります。厳密な比較はしていないものの、この手法によって処理時間短縮と精度向上の良いバランスを取れたと感じています。 以下がLangChainの llm.stream() を利用した場合のサンプルコードです。 from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from typing import Optional from langchain_core.pydantic_v1 import BaseModel, Field class ItemCategory(BaseModel): item_category_id: int = Field(None, description="商品説明から予測したカテゴリID") reason: Optional[str] = Field(None, description="このカテゴリIDを選択した理由を詳しく説明してください") system_prompt = """ 与えられる商品情報を元に、商品のカテゴリを予測してください。 商品のカテゴリは候補から選んでください。選んだ理由も説明してください。 """ item_info = " (商品データと新カテゴリ候補などを入れる) " llm = ChatOpenAI( model_name="gpt-3.5-turbo", max_tokens=25, ) structured_llm = llm.with_structured_output(ItemCategory) prompt = ChatPromptTemplate.from_messages( [ ("system", system_prompt), ("human", "{item_info}"), ] ) chain = prompt | structured_llm # streamingの最後の要素だけ取り出す # - 通常、max_tokensで回答を打ち切るとjsonが壊れてパースの処理が必要 # - langchainのstreamで実行すると常にjsonを完成させてくれるため、 # max_tokensで回答を打ち切ってもjsonをパースする必要がない for res in chain.stream({"item_info": item_info}): pass print(res.json(ensure_ascii=False)) # res: ItemCategory # {"item_category_id": 1, "reason": "商品名に「ぬいぐるみ」が含まれ"} 5. Numba・cuDFの活用 数十億商品を処理する際は些細な処理でも処理速度が気になるため、可能な限りすべての処理をcuDF[16]およびNumba[17]で高速化しました。 正直なところ Numba を書くのは苦手だったのですが、Pythonの素のコードをChatGPT 4に見せると書き直してくれるため、ほとんど自分で書く必要がなくコーディング工数を大幅に削減することができました。 まとめ ChatGPTは会話形式で利用されることが多く注目を集めていますが、その高い思考能力を活用することで、これまで面倒だったり不可能であったタスクを簡単に解決できるようになります。私たちのプロジェクトでは、膨大な商品データを新しいカテゴリに短期間で分類し直すという面倒な課題を、ChatGPTを活用することで解決することができました。 また、OSSのEmbeddingモデルやマルチGPUの活用、高速な近傍検索が可能なベクトルデータベースの採用、ChatGPTでの予測の高速化、Numbaを用いた処理の高速化など、様々な工夫を行うことで、限られた時間とリソースの中でも最大限の成果を出すことができました。 今回の事例が、ChatGPTをはじめとする大規模言語モデルの可能性の一端を示し、皆様のプロジェクトの参考になれば幸いです。ぜひ、様々な場面でLLMを活用し、これまでは難しかった課題にチャレンジしてみてください。 Refs 協調フィルタリングとベクトル検索エンジンを利用した商品推薦精度改善の試み OpenAI API フリマアプリ「メルカリ」累計出品数が30億品を突破 k-nearest neighbors algorithm OpenAI Embeddings API Massive Text Embedding Benchmark (MTEB) Leaderboard JapaneseEmbeddingEval intfloat/multilingual-e5-large intfloat/multilingual-e5-base intfloat/multilingual-e5-small cl-nagoya/sup-simcse-ja-large Sentence-Transformers Voyager Chain-of-Thought Prompting Elicits Reasoning in Large Language Models (Wei et al. 2022) LangChain rapidsai/cudf Numba: A High Performance Python Compiler
Merpay Engineering Productivity Team の goccy です。 gRPC Federation は、gRPC で通信する複数のサービスから得た結果を合成して返すようなサービスを簡単に作成するための仕組みです。DSL ( Domain Specific Language ) を Protocol Buffers 上で記述することで利用します。まずは、 GraphQL(Apollo) Federation の gRPC 用のものだと考えるとわかりやすいと思います。2023年8月に OSS として公開し、先日 Public Roadmap を公開しました。2024/6月末を目標に Version 1.0 ( GA版 ) をリリースする予定です。また、最近は Protocol Buffers のエコシステムに参加しました。 Protobuf Global Extension Registry への登録 や Buf Schema Registry への登録 、 Buf Plugin のサポート が終わり、既存のエコシステムに従って gRPC Federation を利用できます。 本稿では、Version 1.0 を目前に控えた gRPC Federation をどのような思想のもとで設計したかを説明し、現在の gRPC Federation の表現力やプラグインシステム、周辺ツールなどの機能について触れ、今後の予定を紹介します。 2023年8月の Merpay & Mercoin Tech Fest で紹介したもの から多くのアップデートがあります。ぜひ、新しいアーキテクチャを考える際の材料にしてください 設計方針 Protocol Buffers を進化させる gRPC Federation は DSL を Protocol Buffers 上に記述することで利用します。本項では、私たちがこの選択を選んだ理由を説明します。 従来、Protocol Buffers は主にAPIやデータ構造を定義する設計用途で利用されてきました。コード生成と組み合わせることで、設計に対応した実装を生成でき、設計と実装を乖離させることなく保守・運用できることが強みです。さらに、プラグインの仕組みとそれを利用したツールによって、Protocol Buffers 上で定義されたAPIやデータ構造に対してカスタムオプションを利用して付加情報を与えることができ、これにより多様な自動生成が可能になっています。 gRPC Federation はこの点に着目し、gRPC サービスを動作させるために必要十分な実装を DSL として Protocol Buffers 上で記述できるようにしました。これによって、Protocol Buffers は自身のもつ情報だけで gRPC サービスを構築できる言語へと進化します。 DSL を Protocol Buffers 上で記述すべきか、別の専用のファイルで記述すべきかは議論を重ねました。DSL を専用のファイルで記述する場合、言語のシンタックスを自由に調整でき、書き味を向上させやすいメリットがあります。しかしその反面、独自の言語を利用する場合は Parser の実装が必要になり、ソフトウェアの複雑度が飛躍的に増加する懸念や、専用のファイルをどのように管理すべきか考える必要があります。Protocol Buffers と分離することで、設計と実装を乖離させることなく保守・運用できるという恩恵を受けづらくなるともいえます。 また、開発者が普段慣れ親しんだ汎用プログラミング言語でコードを書くことに比べて、gRPC Federation のような DSL を利用する効果とは何かについても考えました。 DSL を利用することで必要最小限の記述でやりたいことを表現できるという側面はあります。ですが、DSL 自体に学習コストがあるため、慣れ親しんだ言語で書いた方が効率よく開発できそうな気がします。また、定型化できる部分をライブラリなどで提供すれば、より少ない記述で実装することもできそうです。 私は、DSL のメリットは「多様な表現ができない」こと自体にあると考えています。DSL を利用する以上、汎用プログラミング言語のように自由にコードが書けるわけではありません。逆を言えば、DSL を利用して制約のある中で生成するコードはすべて予測可能で、知らないミドルウェアやサービスにアクセスしたり、ファイルシステムにアクセスするようなことはありません。これは DSL を利用して作成されたサービスのビルドやデプロイを管理する立場からすると重要な意味を持ちます。例えばビルド時に特別な依存がないことが保証されている場合、より高速にビルドしたりビルドプロセスを自動化したりといった手段が選択できます。同様にデプロイに関しても、アプリケーションが動作するために必要十分な環境を用意しやすい、動作環境をセキュアに保ちやすいといった側面があります。 こうした理由から、私たちは Protocol Buffers 上で DSL を書く方法を選択しました。シンタックスの融通が効かないデメリットを差し置いても、すでに Protocol Buffers 上で定義されている API やデータ構造をそのまま Protocol Buffers 上で参照できるメリットは大きいと考えています。また、gRPC Federation の利用過程でサービス間の依存関係が明示されることで、サービスの循環参照の有無や、問題発生時の影響範囲の特定、APIレベルでの実行コストの計算といった様々な解析を行うことが Protocol Buffers だけでできるようになります。 DSL の限界とプラグインシステム gRPC Federation を作る上で、「DSL でどこまで表現できれば十分か」を考えることが一番難しい点でした。様々な機能をサポートしていく過程で DSL の表現力は向上していきますが、どこまでいっても DSL では実現不可能なロジックは存在します。また、DSL で表現できる範囲だったとしても、再実装せずに、すでにある3rd party製のライブラリを利用したい場合も考えられます。そこで私たちは、DSL には限界があることを理解した上で、Protocol Buffers 上で最低限記述すべき内容を決め、それ以外は DSL の外で実装する選択ができるようにしています。 Protocol Buffers 上で最低限記述すべき内容は「gRPC メソッド呼び出しの記述」としました。gRPC Federation の機能を簡潔に書くならば、「gRPC メソッドを呼び出す」ことと「メソッド呼び出しの結果を加工する」 ことを Protocol Buffers 上で書くことです。このとき、「どのgRPC メソッドを呼び出しているか」が Protocol Buffers 上に書かれなければ、Protocol Buffers を見ただけではどのサービス(のどのメソッド)に依存しているのかわからなくなってしまいます。私たちは経験上、マイクロサービス開発においてサービスの依存関係を把握することがとても重要であることを知っています。そのため、最低限「gRPC メソッド呼び出しの記述」は Protocol Buffers 上で行い、Protocol Buffers を解析するだけでサービス間の依存関係を把握できるようにしています(下図)。 DSLの外で実装する手段として、いくつかの方法を用意しています。まず、gRPC Federation では DSL で表現できない部分だけを Go 言語によって実装することができます。しかし Go で実装する部分が多くなると Protocol Buffers と Go で実装が分離し、あまり嬉しくありません。そこで、もうひとつの選択肢としてプラグインの仕組みを提供しています。Go で書く場合と違う部分は、DSL で式の評価に利用している CEL( Common Expression Language ) の API を拡張できる点です。この仕組みを利用することで、Protocol Buffers 上で独自の API を使った表現が記述でき、Go で書く場合に比べて Protocol Buffers 上に実装を集中させやすくなります。また、複数の Protocol Buffers ファイルから共通の処理を利用したい場合にも有効です。 gRPC Federation の活用場面 gRPC Federation を利用することでサービス間の依存関係が明確になり、Protocol Buffers 上で把握できる情報を増やすことが可能です。また、gRPC Federation によって生成されたコードを利用することで、サービス開発における定型化された作業に割く時間を大きく減らし、ビジネスロジックの実装に集中できるようになります。 そのため、複数のマイクロサービスの結果を合成して返すことが主な責務である BFF ( Backends For Frontends ) や Public API のような toB 向けのサービスは gRPC Federation を採用する例として最も適していますが、通常のマイクロサービス開発でも十分に利用できると考えています。 gRPC Federation がもつ表現力 次に、現状の gRPC Federation の表現力について、重要な機能をいくつか簡単に紹介します。 gRPC Federation では service / message / field など Protocol Buffers上の各要素に対して専用のオプションを用意しています。簡単な例を利用した説明は こちらに記載しました 。 本稿では、長くなりすぎてしまうので基本的な使い方については省略しますが、各項目の例を見ていただければ、なんとなく何ができるのか理解していただけると思います。 公式リファレンスはこちらです 。 変数定義と式の評価 gRPC Federation の開発を進めていくにあたって、変数や式の評価を行う仕組みが必要になりました。式の評価には、 Kubernetes の Custom Resource Definition でも利用されるようになった 、Common Expression Language (CEL) を採用しました。 こちらに言語仕様がとまっています 。 CEL は式を評価することに特化した言語で、小さくかつ洗練された仕様と豊富な拡張性をもっています。四則演算や論理演算、三項演算から関数、マクロまで様々な機能がある他、gRPC Federation では独自に CEL の機能を拡張し、例えば google.protobuf.Timestamp に対して Go の time ライブラリの機能を適応 したり、 reduce や first といったマクロ を使用できるようにしています。CEL は Protocol Buffers と親和性高く設計されており、gRPC Federation のように Protocol Buffers 上の定義を CEL の中で利用したい場合に適しています。ですが、CEL は変数の定義ができないため、gRPC Federation の仕様として 「CEL の評価結果を変数に代入できる機能」と「定義済みの変数をCELの評価式の中で参照できる機能」を追加しました。 次のように、 def キーワードを利用して式を評価した結果に名前を付けることで変数を定義できます。 grpc.federation.message option で定義された変数は grpc.federation.field option で参照することができ、次のように参照した変数の値をそのままフィールドに代入することができます。 message M { option (grpc.federation.message) = { def [ { name: "t" // 2024/4/01 00:00:00+0 by: "grpc.federation.time.date(2024, 4, 1, 0, 0, 0, 0, grpc.federation.time.UTC())" }, { name: "sum" by: "[2, 3, 4].reduce(accum, cur, accum + cur, 1)" }, // sum = 10 { name: "v" by: "[1, 2, 3, 4].first(cur, cur % 2 == 0)" } // v = 2 ] }; google.protobuf.Timestamp time = 1 [(grpc.federation.field).by = "t"]; int64 sum = 2 [(grpc.federation.field).by = "sum"]; int64 first = 3 [(grpc.federation.field).by = "v"]; } このように、message option の中でフィールドに割り当てる値を作り、 field option でその値を参照して代入するというのが基本の使い方です。 現在 gRPC Federation で利用可能な CEL API は こちらにまとめました 。 gRPC メソッドの呼び出し 必ず Protocol Buffers 上に記述してもらいたい、gRPC メソッドの呼び出し方法について説明します。 リファレンスはこちらです 。 使い方の前に、呼び出し対象のメソッドが次のように定義されているとします。 メソッドへの FQDN は foopkg.FooService/GetFoo となり、メソッドを呼び出すためには GetFooRequest メッセージの内容を作る必要があります。返り値は GetFooResponse です。 package foopkg; service FooService { rpc GetFoo(GetFooRequest) returns (GetFooResponse); } message GetFooRequest { FooParam param = 1; } message FooParam { string x = 1; } message GetFooResponse { Foo foo = 1; } message Foo { string bar = 1; } このとき、メソッドを呼び出すには、次のように call{} を記述します。 message M { option (grpc.federation.message) = { def { name: "res" call { method: "foopkg.FooService/GetFoo" request { field: "param" by: "foopkg.FooParam{x: 1}" } } } def { name: "f" by: "res.foo" } // f = foopkg.Foo{} }; string result = 1 [(grpc.federation.field).by = "f.bar"]; // assign foopkg.Foo.bar field to result field. } method に呼び出したいメソッドの FQDN を記述し、 request で GetFooRequest メッセージの各フィールドの値を指定します。ここでは CEL を使って foopkg.FooParam の内容を作成しました。 メソッドの呼び出し結果は res 変数に格納します。 次の変数定義で res 変数の foo フィールドへアクセスしているので、 foopkg.Foo の値が変数 f に代入されます。最後に、フィールドバインディング時に変数 f を参照し、 bar フィールドの値を取り出して result フィールドに代入しています。 メッセージへの依存 メソッドを呼び出した結果を欲しい形に加工する上で重要になるのが、メッセージ間に依存関係を作る機能です。 リファレンスはこちらです 。 あるメッセージは別のメッセージに依存することができます。依存関係は gRPC Federation のオプションを利用して明示的に記述することができます。例えば、 次の例にある M というメッセージを構築することが目標である場合、 M メッセージのフィールドに存在する Dep メッセージの値を作る必要があります。ここで、 Dep メッセージが GetFoo メソッドの呼び出し結果の値を利用することで作れるとすると、次のように記述することができます。 message M { option (grpc.federation.message) = { def { name: "res" call { method: "foopkg.FooService/GetFoo" request { field: "param" by: "foopkg.FooParam{x: 1}" } } def { name: "dep" message { name: "Dep" args { name: "f" by: "res.foo" } } } }; Dep dep = 1 [(grpc.federation.field).by = "dep"]; } message Dep { string bar = 1 [(grpc.federation.field).by = "$.f.bar"]; } message{} を利用することで他のメッセージの値を作ることができます。メッセージの値を作る際は args{} を利用して自由に依存先のメッセージに対して引数を渡すことができ、 name で名前を指定することで、依存先のメッセージ側で $. というプレフィックスを付けて引数にアクセスすることができます。 この例では、 res 変数から取得した foo フィールドの値に対して、 f という名前の引数を作って Dep の値を作っています。 Dep メッセージ側では、CEL の評価式の中で $.f と記述することで引数にアクセスしています。 バリデーション サービスを実装する上で、メソッドを呼び出した結果に対するバリデーションは常に意識しなければいけません。バリデーションの結果、エラーを返す場合は gRPC の慣習に従ってエラーを作る必要もあります。Protocol Buffers でバリデーションと聞くと、 protovalidate が有名だと思います。これはリクエストパラメータのバリデーションに利用するものですが、 gRPC Federation の場合はリクエストに限らず、参照可能なあらゆる変数に対して行うことができます。また、gRPC エラーを返すために特化した機能も用意しています。 リファレンスはこちらです 。 例えば次の例のように、 GetFoo メソッドを呼び出した結果が期待値かどうかを確認することが可能です。エラーは google.rpc.Status を作るようになっており、 error_details.proto で定義されているものがサポートされています。加えて、独自のメッセージを作ってエラーに含めることも可能です。 例えば Go 言語では、 errdetails パッケージを使って grpc.Status を作る処理に該当します。 message M { option (grpc.federation.message) = { def { name: "res" call { method: "foopkg.FooService/GetFoo" request { field: "param" by: "foopkg.FooParam{x: 1}" } } } def { validation { error { if: "res.foo.bar != 'xxx'" code: FAILED_PRECONDITION message: "'unexpected foopkg.Foo.bar value'", } } } }; } ここで紹介した機能は全体のごくわずかです。gRPC Federation は他にも多くの機能が存在するので、お時間のある際にぜひ見てみてください。 WebAssembly を利用したプラグインシステム gRPC Federation では、DSL 中に記述する CEL API や gRPC Federation がもつコード生成パイプラインを WebAssembly を利用して拡張することができます。プラグインを WebAssembly として実行することで、WebAssembly ランタイム側で制約を設けることができます。これにより、例えばネットワークやファイルシステムへのアクセスを禁止することで、プラグインによる予期しない動作を防止しています。 DSL からコードを生成する際に、Logger や gRPC Interceptor など、ドメイン固有の実装を同時に生成したい場合があります。そのような場合にコード生成パイプラインをプラグインによって拡張することで、gRPC Federation がもともとコード生成に使用している情報と全く同じものをプラグインで受け取り、自由にコード生成を行うことができるようになります。 Protocol Buffers からコード生成を行って gRPC サーバをビルドするまでの過程とプラグインの関係を図にすると次のようになります。 周辺ツール DSL を提供する上で、周辺ツールの整備も重要だと考えています。今回は Protocol Buffers のプラグインとして動作するため、 protoc のプラグインを用意するのはもちろんですが、他にも専用の Linter や Language Server 、コード生成ツールを用意しています。今回はこの中から、Language Server と コード生成器について紹介します。 protoc-gen-grpc-federation: protoc プラグイン grpc-federation-linter: Linter grpc-federation-language-server: Language Server grpc-federation-generator: コード生成器 Language Server DSL を書いてもらう上で当初から Language Server の提供は必須だと考えており、 専用の Language Server を提供しています。専用といっても、通常の Protocol Buffers の開発で最低限必要な Syntax Highlight や コードジャンプなどは実装済みなので、Protocol Buffers の Language Server としても利用することができます。 コードエディタによって Language Server の利用方法は様々ですが、VSCode では利用しやすいように、 すでに Extension を公開しています 。他の IDE 向けの対応も現在進めていますので、どうぞご期待ください。 Language Server によって Syntax Highlight された Protocol Buffers は次のようになります。文字列中の CEL の式などが適切にハイライトされているのが確認できると思います。 コード生成器 コード生成に関して、 protoc を利用した方法 以外に、 Buf を利用する方法 や、 gRPC Federation 独自のコード生成ツールによる方法 をサポートしています。 独自のツールを作った背景には、Protocol Buffers を編集した瞬間に gRPC サービスが立ち上がるような開発体験を提供したいという思いからでした。独自のツールには -w オプションを付けることで Protocol Buffers の変更を検知して即座にコンパイル、コード生成を実行する仕組みがあります。この機能と Air などのホットリローダを組み合わせることで、コード生成された側から Go のコンパイルを行う仕組みを作れるため、他に gRPC サービスを起動するために必要な情報をプラグインの形で外から与えさえすれば、Protocol Buffers を編集した瞬間に gRPC サービスが立ち上がる状態を作ることができます。 個人的にはこれを Protocol Buffers Driven Development と呼んでおり、スキーマ駆動開発を促進できると考えています。図にすると以下のようになります。 今後 メルペイ社内では、 gRPC Federation を使ったサービスがそろそろ本番環境で稼働し始めようとしています。そこで、最終的な機能の精査を行い、6月末を目標に Version 1.0 ( GA版 ) を提供する予定です。 1.0 以降は、基本的に破壊的な変更を入れず後方互換性を保ち、どうしても変更したい場合は十分な変更期間をとるなど社外のユースケースを想定してメンテナンスしていくことを考えています。 そのため、gRPC Federation の導入を考えるとてもいい機会だと考えています。導入のご相談は随時受け付けていますので、ぜひお気軽にご連絡ください。また、 OSS に関しても積極的にコントリビューションを受け付けています。こちらもあわせてよろしくお願いします。
はじめに こんにちは、mercari.go スタッフの hiroebe です。 3月21日にメルカリ主催の Go 勉強会 mercari.go #25 を YouTube でのオンライン配信にて開催しました。この記事では、当日の各発表を簡単に紹介します。動画もアップロードされていますので、こちらもぜひご覧ください。 Learning TLS1.3 with Go 1つめのセッションは @shu-yusa さんによる「Learning TLS1.3 with Go」です。 発表資料: Learning TLS1.3 with Go TLS1.3 におけるハンドシェイクのプロセスについて、Go のコードを交えて説明しました。TLS1.3 では TLS1.2 から多くの変更が入っていて、ハンドシェイクの改善もそのうちの1つです。Go において TLS に関連する暗号技術は crypto/ 以下のパッケージで提供されていて、発表ではこれらのパッケージを用いたコード例が多く紹介されています。 個人的に普段触れる機会の少ないパッケージも多く、とても興味深かったです。コードとともに理解することで、ハンドシェイクの各ステップにおける処理の流れがつかみやすくなっていると感じました。 Exploring Go Runtime Metrics 2つめのセッションは @Chin-Ming さんと @mohit さんによる「Exploring Go Runtime Metrics」です。 発表資料: Exploring Go Runtime Metrics Go の runtime/metrics パッケージについて、導入された背景や内部実装について紹介しました。Go における従来のランタイムメトリクスの取得方法にはいくつかの問題点があり、それが runtime/metrics パッケージによってどのように解決されたかについて説明されています。 将来的なランタイムの変更にも対応するためにどのような API デザインとするか、という点は非常に興味深かったです。発表の後半では、現在サポートされているメトリクスの一覧とそれらのユースケースについても紹介されていました。 Securing Code with Govulncheck 3つめのセッションは同じく @Chin-Ming さんと @mohit さんによる「Securing Code with Govulncheck」です。 発表資料: Securing Code with Govulncheck Go プログラムの脆弱性チェックを行うための Govulncheck というツールについて紹介しました。ツール自体の利用方法に加えて、チェック対象とする脆弱性が Go においてどのように収集・管理されているか、という点についても説明されています。 Go Vulnerability Database についての説明など個人的にも知らなかった点が多く、とても勉強になりました。Govulncheck は CI にも容易に組み込めるそうなので、興味のある方は試してみてはいかがでしょうか? おわりに 今回は Go の標準ライブラリやツールを題材とした3つの発表をお送りしました。Go を通して TLS の仕組みやソフトウェアの脆弱性についても知ることができて、運営としても非常に勉強になりました。 ライブで視聴いただいた方も録画を観ていただけた方も本当にありがとうございました! 次回の開催もお楽しみに! イベント開催案内を受け取りたい方は、connpassグループのメンバーになってくださいね! メルカリconnpassグループページ