TECH PLAY

タイミー

タイミー の技術ブログ

274

こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。 冷やし中華はじめました的なタイトルですね。分かります。 今回はタイミーが本番運用しているRailsアプリケーションに対してRails edgeでCIを回すようになった話を紹介します。翌週には「〜見つけたエラー編(仮)〜」と題して、実際に弊社で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。 $ ruby -v ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux] $ rails -v Rails 7.0.6 弊社ではRubyもRailsも積極的に最新バージョンにあげる活動をしています。今回の記事はRailsに関してですが、Rubyのアップグレードも同様に行っています。過去にはRuby3.2にし、YJITを有効化にした記事を公開しているので興味があれば、ご一読ください。 tech.timee.co.jp Rails edgeとは? ChatGPTに聞いて楽をしてみました。 この記事ではChatGPTと同様に rails/rails のmainブランチを指すこととします。 Rails edgeは、安定版ではなく開発版なので以下のような特徴があります。 将来のリリースで利用可能になるかもしれない新機能が含まれている 正式リリースされていない既知のバグ修正が含まれている 安定版ではなく、まだ評価されていない機能やバグが含まれている可能性があるため、本番環境で使用する際には注意が必要です。GitHubでは、毎週本番環境で使うRailsをedgeにアップデートしているそうです *1 。 なぜRails edgeでCIを回すのか タイミーでは、RubyやRailsのコミュニティの進化に継続的に追随することで、高速化や機能追加などの恩恵を受け、ユーザーに最大限の価値を提供していきたいと考えています。 Rails edgeでは、将来のリリースで利用可能になる新機能を事前に検査できるため、潜在的な影響を確認することができます。また、バグや互換性の問題を早期に発見し、解決策を見つけることも可能です。その他にも、最新の機能や修正に関して何かあれば、安定版リリース前に異議申し立てをしやすいといったメリットもあります。 これらの利点を考慮し、Rails edgeでCIを回すことにしました。 Rails edgeでCIを回す rails以外のgemはそのまま利用したいため、既存のGemfileを読み込み、railsのみを rails/rails に上書きすることでRails edge用のGemfileを用意します。 # frozen_string_literal: true # 既存のGemfileを読み込む eval_gemfile File .expand_path( ' ../Gemfile ' , __dir__ ) # 上書きしたい依存関係を削除する dependencies.delete_if { |d| d.name == ' rails ' } # rails/railsのメインブランチに上書きする gem ' rails ' , branch : ' main ' , github : ' rails/rails ' 使用するgemのバージョンが変更されたので専用のlockファイルを用意する必要があります。 BUNDLE_GEMFILE を指定する事で指定したGemfileを使用できます。 $ BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install これによりRails edgeと他のGemとの依存関係を定義したファイルの用意が完了します。次にこれらのファイルを利用してCIでテストを実行する準備をしていきます。実際の設定ファイルを簡略化して紹介しています。なお、弊社ではCIツールとしてCircleCIを利用しています。 version : 2.1 orbs : ruby : circleci/ruby@2.0.1 jobs : test : parameters : gemfile : type : string default : Gemfile environment : BUNDLE_GEMFILE : << parameters.gemfile >> BUNDLE_PATH_RELATIVE_TO_CWD : true docker : - image : cimg/ruby:3.2.2 steps : - checkout - ruby/install-deps : key : gems-<< parameters.gemfile >> gemfile : << parameters.gemfile >> - run : name : Test command : bundle exec rspec workflows : version : 2 workflow : jobs : - test : filters : branches : only : - master - /^rails_edge.*$/ matrix : parameters : gemfile : - "gemfiles/rails_edge.gemfile" 今回はmasterブランチと rails_edge.* ブランチでのみ実行するようにしました。 Orb - circleci/ruby で定義されるコマンドである install-deps ではgemfileパラメータを渡すと指定したGemfileを利用してbundle installを実行してくれるため、その機構を利用しています。 また、 BUNDLE_PATH_RELATIVE_TO_CWD を利用することで、Gemfileではなくカレントディレクトリに対して相対的にパスを指定するのでGemの競合を回避でき、キャッシュを有効活用できます。 Rails edgeで落ちるテストに印をつける 後出しですが今回の目的は「 Rails edgeでCIを回す 」ことであり、全てのテストを修正することではありませんでした。そこで、今回は落ちたテストをpendingすることにしました。 skipではなくpendingを採用したのは、mainブランチの追従によりテストが成功する可能性があるためです。skipはテストの成功・失敗に限らずテストを実行しませんが、pendingはテストの失敗時のみ保留にするため修正に気づくことができます。 便宜上、これから出るであろうRails7.1で動かないこととそれまでに直すぞという意味合いも込めて以下のようなメッセージを表示するようにしています。ここは適当に変えていただくのが良いかと思います。 # frozen_string_literal: true module PendingIfRailsEdge def pending_if_rails_edge ' PENDING: Rails 7.1で動くように直す ' if Rails :: VERSION :: STRING .start_with?( ' 7.1 ' ) end end RSpec .configure do |config| config.extend PendingIfRailsEdge end 以下のように使えます。こうすることでテストコードをgrepした時に落ちているコードをすぐに見つけることができるの便利でもあります。 context ' foo ' , pending : pending_if_rails_edge do ... end it ' bar ' , pending : pending_if_rails_edge do ... end これでRails edgeでCIを回すことができるようになりました。 Rails edge用のlockファイルを追従させる masterブランチと rails_edge.* ブランチでRails edgeでCIを回す運用を開始してから、featureブランチでGemの追加や削除を行ったが rails_edge.gemfile の更新を忘れてmasterブランチにマージしたため、masterブランチでRails edgeでのCIがfailするケースが発生しました。 本番環境に影響がないとはいえmasterブランチのCIがfailしているのはモヤモヤすることや、せっかく導入した仕組みが形骸化してしまうため、masterブランチマージ前に気づける仕組みを導入しました。 具体的には BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install を実行して差分が発生すれば失敗するステップを追加し、featureブランチでも実行するようにしました。 jobs : check_outdated_gemfile : parameters : gemfile : type : string docker : - image : cimg/ruby:3.2.2 steps : - checkout - ruby/install-deps : key : gems-<< parameters.gemfile >> gemfile : << parameters.gemfile >> - run : name : re-bundle install command : bundle install - run : name : check file changes command : | if [ `git diff --name-only << parameters.gemfile >>.lock` ] ; then echo 'Please run `BUNDLE_GEMFILE=<< parameters.gemfile >> bundle install`' exit 1 else exit 0 fi 最後に 今回はRails edgeでCIを回し始めた背景や導入する方法について紹介しました。 今回の取り組みを通して、将来のRailsアップグレードにおいて遭遇するであろうバグに早期に気づき、迅速な対応ができるようになりました。まだGitHubのように *1 、 Rails edgeを毎週取り込める状態ではないですが、引き続き頑張っていこうと思います。 次回は、Rails edgeでCI回し始めたことで見つけた問題を紹介していきます。記事公開時には公式Twitter ( @TimeeDev ) でアナウンスしていくのでフォローしていただけると嬉しいです。 また、タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう! product-recruit.timee.co.jp *1 : https://github.blog/2023-04-06-building-github-with-ruby-and-rails
アバター
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の 小栗 です。 今回は、DSグループのメンバーにおすすめの本を聞いてみたのでご紹介します! *1 おすすめ本を通して、DSグループの雰囲気や、業務で活用するスキル・知識について、みなさんに伝わればいいなと考えています。 データサイエンス(DS)グループの紹介 本題に入る前に、軽くDSグループの紹介をさせてください。 DSグループは、タイミーの事業成長をデータ・アルゴリズムで支援することを目的としています。 例えば、以下のような業務を日々行なっています。 Google Cloudを利用した機械学習パイプライン&基盤の開発・運用 ユーザーへ仕事を推薦するレコメンドエンジンの開発 営業活動を支援する予測モデルの開発 ビジネス施策の効果検証 現在、専任・兼任・業務委託のメンバーで構成されており、今後も規模を拡大する予定です。 Web企業、コンサル企業、AIベンチャーなどで、データサイエンティスト/機械学習エンジニアとして経験を積んできたメンバーが在籍しています。 特に、機械学習システム設計、機械学習モデル実装、効果検証などに強みがあるメンバーが揃っています。 DSグループおすすめ本の紹介 それでは、DSグループの各メンバーに聞いた「心からおすすめできる本」を、推薦メンバーからのコメントを載せつつ紹介します。 ジャンルとしては、機械学習、因果推論、開発、組織に関する書籍を取り上げます。 機械学習のおすすめ本 Rによる統計的学習入門 Rによる 統計的学習入門 作者: Gareth James , Daniela Witten , Trevor Hastie , Robert Tibshirani 朝倉書店 Amazon メンバーからのコメント↓ カステラ本として有名な 『統計的学習の基礎』 を手掛けた著者が書いた、機械学習の入門書。 『統計的学習の基礎』は良書ではあるものの内容・分量がヘビーなので、実務家に一番最初におすすめしたいのはコレかなと思ってます。 説明の平易さが適切であり、個人的に入門書の中で一番わかりやすかったです。 ベイズ推論による機械学習入門 機械学習スタートアップシリーズ ベイズ推論による機械学習入門 作者: 須山敦志 講談社 Amazon メンバーからのコメント↓ ベイズ推論による学習・予測を丁寧に数式レベルで追うことができる良書です。 ギブスサンプリングや変分推論を理論から理解したい方におすすめしたいです。 自然言語処理の基礎 IT Text 自然言語処理の基礎 作者: 岡﨑直観 , 荒瀬由紀 , 鈴木潤 , 鶴岡慶雅 , 宮尾祐介 オーム社 Amazon メンバーからのコメント↓ 自然言語処理の基礎から始まり、近年の深層学習ベースの手法まで丁寧に解説した本。 Transformer、BERT、GPTなど、大規模言語モデルの興隆を支える新しい技術の解説もあり、和書としては貴重です。 推薦システム実践入門 推薦システム実践入門 ―仕事で使える導入ガイド 作者: 風間 正弘 , 飯塚 洸二郎 , 松村 優也 オライリージャパン Amazon メンバーからのコメント↓ 推薦システムについて、アルゴリズムの解説に留まらず、企画段階やデザインまで幅広く扱った実践的な一冊です。 本書を教材にDSグループで勉強会を開くなど、実務で重宝しました! 因果推論のおすすめ本 効果検証入門 効果検証入門〜正しい比較のための因果推論/計量経済学の基礎 作者: 安井 翔太 技術評論社 Amazon メンバーからのコメント↓ RCTだけでなく、観察データを用いた因果推論の手法をわかりやすく解説した、実務家向けの一冊。 DSグループではビジネス施策の効果検証を行うことも多く、仕事をする上で基礎になっています。 DSグループと同じ部署に属するBIグループも勉強会で本書を使っていたので、一緒にわいわい学びました。 統計的因果推論の理論と実装 統計的因果推論の理論と実装 Wonderful R 作者: 高橋将宜 , 石田基広 , 市川太祐 , 高橋康介 , 高柳慎一 , 福島真太朗 , 松浦健太郎 共立出版 Amazon メンバーからのコメント↓ 図表を交えた解説がわかりやすい、理論と実装のバランスが良いなど、非常に洗練された書籍です。 『効果検証入門』では深く説明されなかった手法も解説されており、併せて読むのがおすすめです。 開発系のおすすめ本 ロバストPython ロバストPython ―クリーンで保守しやすいコードを書く 作者: Patrick Viafore オーム社 Amazon メンバーからのコメント↓ Pythonで保守しやすい、堅牢なコードを書くための情報がよくまとまっています。 機械学習モデリングやデータ処理を含む実験コードを本番環境に乗せていく際に、本書や 『リーダブルコード』 に立ち返って開発やレビューすることを意識しています。 単体テストの考え方/使い方 単体テストの考え方/使い方 作者: Vladimir Khorikov マイナビ出版 Amazon メンバーからのコメント↓ 単体テストの考え方を網羅的かつ深くまとめた書籍。 DSグループでは(アドホック分析のコード以外は)テストを実装することにしているため、価値のあるテストケースをつくるため手元に置いている一冊です。 データマネジメントが30分でわかる本 データマネジメントが30分でわかる本 作者: ゆずたそ , はせりょ , 株式会社風音屋 Amazon メンバーからのコメント↓ 中身も見た目もヘビーすぎる DMBOK を独自に要約してまとめた本。 DMBOKの本質が簡潔にまとめられており、(大半のケースでは)こちらを参照することで問題を解決できる気がしています。 ソフトウェア見積り 人月の暗黙知を解き明かす ソフトウェア見積り 人月の暗黙知を解き明かす 作者: スティーブ マコネル 日経BP Amazon メンバーからのコメント↓ 有名な「不確実性コーン(プロジェクトの進行に伴って不確実性が減少することを表した図)」の初出本。 ソフトウェア開発における見積もりに関して、本質的な視点を提供してくれます。 開発に関わるすべての人におすすめできる書籍です。 組織系のおすすめ本 エンジニアリング組織論への招待 エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング 作者: 広木 大地 技術評論社 Amazon メンバーからのコメント↓ エンジニアリング組織における課題の解決方法についてまとめられた本。 アジャイル的な思考や、不確実性の考え方はエンジニアだけでなくデータサイエンティストにとっても重要なので、おすすめです。 恐れのない組織 恐れのない組織――「心理的安全性」が学習・イノベーション・成長をもたらす 作者: エイミー・C・エドモンドソン , 村瀬俊朗 英治出版 Amazon メンバーからのコメント↓ 「心理的安全性」の提唱者であるエドモンドソン博士が、心理的安全性が組織にもたらす影響についてまとめた本。 関連書籍は今たくさんあるのですが、これ一冊読めば心理的安全性のコアがわかり、応用が効くと思っています。 データ統括部では 心理的安全性勉強会 を開催するなど、文化の浸透をはかっています。 We’re Hiring! タイミーのデータ統括部では、こういったおすすめ本の情報が日々Slackに飛び交っていたり、勉強会を定期的に行なっていたりします。 学習意欲や好奇心のある人にとって嬉しい環境ではないかと、(手前味噌ながら)いちメンバーとして思っています。 タイミーでは、データサイエンティストやエンジニアをはじめ、一緒に働くメンバーを募集しています! product-recruit.timee.co.jp *1 : 先日、データ統括部メンバーのおすすめ本も note で紹介しましたので、興味があればご一読ください。
アバター
はじめに 初めまして、タイミーのDREチーム(Data Reliability Engineering Team)でエンジニアをしてます、筑紫です。 今回DREチームで実施した合宿ついてご紹介させて頂こうと思います。 DREのカルチャーを少しでも知って頂けたら嬉しいです。 DREチームについて紹介 DREチームでは、社内の様々データを集約し、クレンジングを行い、社内外で利活用できる形で提供するためのデータ基盤プロダクトの開発・運用を行なっております。 データ基盤の詳細については、ProductOwner(以降POと記載)の土川の記事をご参照ください。 tech.timee.co.jp 今年の4月に私を含め2人入社したことでメンバーの入れ変えもあり、データ基盤の開発体制が新しくなりました。 メンバーが大きく変わったこともあり、開発を進める上で今までのデータ基盤の歴史的背景や方向性の理解にメンバー間で差分があることが課題になっていました。 DREチームのMissionと合宿の目的 DREではチームとしてのMissionを定めています。 ここでいうMissionとは、ビジネスシーンでよく用いられる組織やチームのMVV(Mission・Vision・Value)のMissionで、組織やチームの存在意義、果たすべき使命を指します。 Missionを定めて、チームとしての明確な目標をチームの共通言語にすることで、チームの役割や責任範囲を明確化し、成果の向上や成功につながるデータ基盤プロダクトの開発を効率的に進めることができます。 また、Missionは外部とのコミュニケーションをスムーズにし、チームのモチベーションを高める要素ともなります。 元々のMissionは以下の通りです。 『信頼性の高いデータ基盤を整備し、活用のための環境を提供する』 ただ、Missionを定義した頃から時間も経っていること、またチーム構成が大きく変わったこともあり、アップデートしたい機運が高まっていました。 今のチームでMissionを再検討することで、上述の課題も含め解消できるのではという話になり、DREチームのMissionを決めるワークショップを開催することになりました。 合宿の内容 普段はリモートワークが多いチームですが、合宿は1日だけ東京オフィスに集まり、会議室を貸し切って実施することになりました。 Missionを決めるにあたって、まずは、DREチームの存在意義についてメンバーそれぞれポストイットで意見を出し合い、それを議論しながらクラスタリングしました。 その結果を基に、Missionの方向性を導き出す形で進めました。 ただ、ポストイットの結果からMissionという形で、抽象度高いフレーズを抽出することに苦戦しました。 重要視する要素については、メンバー間で認識が概ね揃っていたものの最終的にMissionという形でどう表現するかに難儀しました。 議論中で、元々のMissionに入っていた、”信頼性”というワードを採用することになりました。 タイミーのDREチームにおける“信頼性”とは、スピード、品質、安定性、ユーザビリティを総合的に表現したものであり、ユーザがデータを利活用する上で、DREチームではこれらの要素を特に重視しています。 特にスピードについては、データが生成されてから活用されるまでの時間を短縮していきたいというPOの思いがあり、また、これからリアルタイム性を求められる要求に対応していくためにも温度感の高い指標になってきています。 また、高い”信頼性”を達成するための手段として、DataOpsという観点を導入することになりました。 DataOpsは、ガートナー社が提唱している概念で、データパイプラインの構築、自動化、監視、デプロイメントの迅速化、データ品質の向上などを重視することで、組織のデータ管理者と利用者の間の連携促進し、データの収集、処理、分析、展開のプロセスを効率化するためのプラクティスです。 このプラクティスを用いて、”信頼性”あるデータ基盤を構築していくことをMissionとすることになりました。 その後議論が進み、最終的に以下のMissionに決まりました。 『DataOpsを実現し、信頼性の高いデータ基盤プロダクトを提供する』 まとめ 日頃このようなチームの方向性などを深く話を機会が少ないので、とても貴重な時間を過ごせました。 チーム内でMissionを定めることができ、同じ方向を向いてプロダクト開発を進めていけそうで、メンバー間での満足度も高く、良かったと思います。 また、それ以上にチームで議論しながら、データ基盤プロダクトの構想や方向性をPO+開発メンバー間で共有できたという、その過程にとても価値がある会だったと思います。 今回定めたMissionを持ってデータ基盤プロダクトの成長を加速させていきたいと思ってます。 最後に、タイミーではエンジニア・データサイエンティストを初め、様々な職種のメンバーを募集してます! product-recruit.timee.co.jp
アバター
こんにちは。2023年1月に株式会社タイミーに入社したバックエンドエンジニアの id:euglena1215 です。 RubyKaigi 2023 がとうとう明日に迫ってきました。楽しみですね。 タイミーは RubyKaigi で初めてブース出展を行います。至らぬ点もあるかと思いますが、RubyKaigi を一緒に盛り上げていければと思っています!どうぞよろしくお願いします。 今回はタイミーが本番運用している Rails アプリケーションに対して Ruby 3.2.2 へのアップデートと YJIT の有効化を行い、パフォーマンスが大きく改善したことを紹介します。 RubyKaigi で「Ruby 3.2+YJIT 本番運用カンパニーです」と言いたいので粛々と進めている — Shintani Teppei (@euglena1215) 2023年4月19日 前提 タイミーを支えるバックエンドの Web API は多くのケースで Ruby の実行よりも DB がボトルネックの一般的な Rails アプリケーションです。JSON への serialize は active_model_serializers を利用しています。 今回の集計では API リクエストへのパフォーマンス影響のみを集計し、Sidekiq, Rake タスクといった非同期で実行される処理は集計の対象外としています。 今回は Ruby 3.1.2 から Ruby 3.2.2 へのアップデートと YJIT 有効化を同時に行いパフォーマンスの変化を確認しました。そのため、パフォーマンスの変化には Ruby のバージョンアップによる最適化と YJIT 有効化による最適化の両方の影響があると考えられますがご容赦ください。 結果 以下のグラフは API リクエスト全体のレスポンスタイムの 50-percentile です。アップデート前後でレスポンスタイムが 約10%高速化されている ことが確認できました。 API リクエスト全体のレスポンスタイムの 50-percentile リクエスト全体としては大きく高速化されていることが確認できました。それでは、レスポンスが遅く、時間当たりのリクエスト数が多いアプリケーションの負荷の多く占めるエンドポイントではどうでしょうか? そこで、タイミーの Web API のうち2番目に合計の処理時間 *1 が長いエンドポイントへのパフォーマンス影響を確認しました。 *2 以下のグラフは2番目に合計の処理時間が長いエンドポイントのレスポンスタイムの 50-percentile です。Ruby アップデートの数日前に GW 中の負荷対策のためのスケールアウトを実施したことで変化が少し分かりにくくなっていますが、Ruby 3.1.2 から Ruby 3.2.2+YJIT にしたことによって 10%以上高速化されている ことが確認できました。 2番目に合計の処理時間が長いエンドポイントのレスポンスタイムの 50-percentile 元々十分に高速なエンドポイントだけでなく、アプリケーション負荷の多くを占めていたエンドポイントのパフォーマンスも改善されていることが分かります。Ruby 3.2 アップデート+YJIT 有効化はパフォーマンスチューニングへの十分実用的な打ち手と言えるのではないでしょうか。 まとめ Ruby 3.1.2 から Ruby 3.2.2 へのアップデートと YJIT を有効にしたことでリクエスト全体のレスポンスタイムの 50-percentile が約10%高速化されました。また、アプリケーション負荷の多くを占めていたエンドポイントも同様に10%以上高速化されていることを確認できました。 Ruby 3.2’s YJIT is Production-Ready でも YJIT によって Shopify が 5~10% 高速化されたと記されていることから、「Ruby 3.2 の YJIT は一般的な Rails アプリケーションを 10%程度高速化させる」と考えて良いのではないかと思っています。 これほどの高速化に尽力していただいた Ruby コミッターのみなさん、本当にありがとうございました。 余談ではありますが、自社で YJIT を有効化したことによって YJIT という技術がより自分ごとになり、内部で何が行われているのかをきちんと理解したいと思うようになりました。 RubyKaigi 2023 で理解を深めようと思います。 宣伝 冒頭で説明したように、タイミーは RubyKaigi 2023 でブース出展を行います。今回の記事ではパフォーマンス改善の結果のみ紹介しましたが、タイミーでのこういった技術改善における取り組み方など話したいことは色々あるので、ぜひブースでお話しさせてください! また、RubyKaigi 後 5/16(火)にはスポンサー振り返り会を Qiita さん、Wantedly さんと実施予定です。 「自分の会社も RubyKaigi スポンサーをしてほしいと思っているエンジニア」をターゲットにしたちょっとニッチな会ですが、もしかして自分ターゲットかも…?と思う方はぜひご参加ください! wantedly.connpass.com *1 : 合計の処理時間 = 平均レスポンスタイム x hits数 *2 : 最も合計の処理時間が長いエンドポイントはGWの繁閑の影響でレスポンスタイムに変化が生じ、比較が困難であったため除外しています。
アバター
こんにちは。2022年11月に株式会社タイミーに入社した sinsoku です。 最近は GitHub Actionsの YAML を書く機会が多く、 YAML も複雑化してきました。 しかし、日常的に YAML を触っている職人以外にはパッと読めないことも多いので、社内の方々が読めるように GitHub Actionsの YAML の書き方をまとめたいと思います。 目次 三項演算子 環境変数(env) 変数(outputs) 関数(workflow_call) 関数 + 配列(dynamic matrix) GitHub CLIの活用 まとめ 三項演算子 GitHub Actions には 三項演算子 がないため、代わりに論理 演算子 を使います。 - steps : - run : echo "${{ (github.ref == 'refs/heads/main' && 'production') || 'staging' }}" 参考: Expressions 環境変数 (env) 環境変数 を使いたい場合は env で定義します。 env : TIMEE_CEO : ryo TIMEE_CTO : kameike jobs : job-env : runs-on : ubuntu-latest steps : - run : echo $TIMEE_CEO # この実装だと置換後の文字列 `echo kameike` を実行する - run : echo ${{ env.TIMEE_CTO }} ただし、 env で env の値を指定するとエラーになるケースがあるので注意してください。 env : DEPLOY_ENV : ${{ (github.ref == 'refs/heads/main' && 'production' ) || 'staging' }} # Unrecognized named-value: 'env'. Located at position 1 within expression: env.DEPLOY_ENV == 'production' IS_PROD : ${{ env.DEPLOY_ENV == 'production' }} jobs.<job_id>.env でも同様のエラーが出ます。 jobs : job-error : runs-on : ubuntu-latest # Unrecognized named-value: 'env'. Located at position 1 within expression: env.DEPLOY_ENV == 'production' IS_PROD : ${{ env.DEPLOY_ENV == 'production' }} jobs.<job_id>.steps[*].env であればエラーになりませんが、同じ階層の env の値は参照できません。 env : TIMEE_CTO : kameike jobs : job-env : runs-on : ubuntu-latest steps : # この実装だと `echo "true, foo, -bar"` を実行する - run : echo "${{ env.IS_KAMEIKE }}, ${{ env.FOO }}, ${{ env.BAR }}" env : IS_KAMEIKE : ${{ env.TIMEE_CTO == 'kameike' }} FOO : foo BAR : ${{ env.FOO }}-bar 参考: Workflow syntax for GitHub Actions 変数(outputs) 汎用的な名前の 環境変数 を定義すると、何かの CLI コマンドに影響する可能性があります。 これを避けるために、outputs で変数を定義することもできます。 steps : - id : var run : | echo "x=foo" >> "$GITHUB_OUTPUT" echo "y=bar" >> "$GITHUB_OUTPUT" # この実装だと `echo "foo, bar"` を実行する - run : echo "${{ steps.var.outputs.x }}, ${{ steps.var.outputs.y }}" outputs は env と違い、 bash の処理結果を変数に定義することができます。 steps : - uses : actions/checkout@v3 - id : var run : | echo "terraform-version=`cat .terraform-version`" >> "$GITHUB_OUTPUT" # この実装だと `echo "1.4.4"` を実行する - run : echo "${{ steps.var.outputs.terraform-version }}" 参考: Defining outputs for jobs 関数(workflow_call) ワークフローの一部を別のファイルに定義し、関数のように呼び出すことができます。 # .github/workflows/_say.yml name : say on : workflow_call : inputs : name : required : true type : string jobs : hello : runs-on : ubuntu-latest steps : - run : echo "Hello, ${{ inputs.name }}." bye : needs : hello runs-on : ubuntu-latest steps : - run : echo "Bye, ${{ inputs.name }}." 使い方は以下の通りです。 jobs : job-say : uses : ./.github/workflows/_say.yml with : name : kameike 参考: Reusing workflows 関数 + 配列(dynamic matrix) workflows_call の入力には真偽値、数字、文字列しか使えません。 The value of this parameter is a string specifying the data type of the input. This must be one of: boolean, number, or string. 引用: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callinputsinput_idtype しかし、少し工夫することで配列を扱うことができます。 # .github/workflows/_say_multi.yml name : say_multi on : workflow_call : inputs : names : required : true type : string jobs : matrix : runs-on : ubuntu-latest outputs : names : ${{ steps.set-matrix.outputs.names }} steps : - id : set-matrix run : | names=`echo '${{ inputs.names }}' | jq -csR 'split("\\n") | map(select(. != ""))' ` echo "names=$names" >> $GITHUB_OUTPUT hello : needs : matrix runs-on : ubuntu-latest strategy : matrix : name : ${{ fromJSON(needs.matrix.outputs.names) }} steps : - run : echo "Hello, ${{ matrix.name }}." 使い方は以下の通りです。 job-say-multi : uses : ./.github/workflows/_say_multi.yml with : names : | ryo kameike GitHub CLI の活用 GitHub API を使うことで、チェックアウトせずにファイル名を取得することができます。 job-gh-matrix : runs-on : ubuntu-latest outputs : files : ${{ steps.set-matrix.outputs.files }} steps : - id : set-matrix run : echo "files=`gh api '/repos/{owner}/{repo}/contents/.github/workflows?ref=${{ github.sha }}' --jq '[.[].name]'`" >> $GITHUB_OUTPUT env : GH_REPO : ${{ github.repository }} GH_TOKEN : ${{ github.token }} job-gh-echo : needs : job-gh-matrix runs-on : ubuntu-latest strategy : matrix : file : ${{ fromJSON(needs.job-gh-matrix.outputs.files) }} steps : - run : echo "${{ matrix.file }}" 画像のようにファイル名の一覧で並列にJobを実行できます。 まとめ YAML むずかしいですね。 この記事が読んだ方の参考になれば幸いです。 また、弊社の GitHub Actionsやデプロイフローについて気になることがあれば、フッターの採用ページのURLから面談の申し込みをどうぞ!
アバター
こんにちは、フロントエンドエンジニアの樫福です。 タイミーのフロントエンドの開発に関わるエンジニアの人数が増えてきました。大人数で開発しながら品質を高い状態に保つには、品質に対する共通認識を作ることが大切です。 このたび、チームでフロイントエンドの 単体テスト についての勉強会を開催しました。 testing-library というフロントエンドのテストに使うライブラリを例に挙げ、具体的な手法よりも、テストを実装する前に抑えておきたい思想についてフォーカスしました。 フロントエンドでテストしたい項目 フロントエンドの単体テストを難しくする要因 testing-library を使って壊れにくいテストを作る方法 要素を見つけるクエリ ユーザの動作をシミュレーションする user-event 要素の状態を検査する jest-dom 実際にテストを書いてみる テストが書けないケース 運用するときの注意点 単体テストを作る目的を明確にする アクセシビリティのガイドラインを定める おわりに フロントエンドでテストしたい項目 フロントエンドでテストしたい項目には次のようなものがあると思います。 ユーティリティ関数や ビジネスロジック が正しく実装されていること 意図しない見た目の変更が起こらないこと ユーザの動作に対して期待した正しい応答が返ってくること ユーティリティ関数や ビジネスロジック は関数やクラスとして切り出し、それらに対しての 単体テスト を書くことになります。基本的には入力に対する出力を確認する出力値ベーステストでテストします。クラスで実装する場合にはアクションに伴って変更した状態を検査する状態ベーステストも併せて使います。これらはフロントエンド以外でも扱うトピックです。 意図しない見た目の変更が起こらないことをテストする場合は Visual Regression Test (VRT) を使います。 reg-suit などのツールを使って、変更ごとの画像のキャプチャを撮って比較します。 ユーザがボタンをクリックする、などの動作のテストは 単体テスト で実装することができます。 testing-library というライブラリを使ってユーザの動作をシミュレーションすることで、 UI コンポーネント の挙動を検査します。画面上の表示に対しては状態ベーステストを、 API にリク エス トを送ることのテストはコミュニケーションベーステストを実装することになります。 テストトロフィーとそれぞれのテストで検査できること *1 勉強会では 単体テスト におけるフロントエンド特有の難しさについて扱いたいので、3つ目のような「ユーザの画面上に表示された要素に対する動作を起点とするようなテスト」に注目しました。 フロントエンドの 単体テスト を難しくする要因 一般的なソフトウェアと比較してフロントエンドにおいて特筆すべき点に次のようなものがあります。 ユーザが要素を取得する ユーザが(マウスやキーボードの操作などの)アクションを実行する 要素が変化したことを検査する ユーザのアクションなどに応じて API リク エス トをする フロントエンドのテストが壊れやすかったり理解するのが難しくなったりする場合、とくに、要素の取得をシミュレートすることに躓いていることが多いように感じています。 testing-library を使って壊れにくいテストを作る方法 testing-library はフロントエンドのテストに使うツール群です。 testing-library には次のような基本方針があります。 The more your tests resemble the way your software is used, the more confidence they can give you. testing-library.com そのソフトウェアが実際に使われる姿に似ているほど、テストの信頼性が高くなります。 testing-library は、ソフトウェアの使われ方に似たテストを作れるような機能を提供してくれています。ライブラリを使ってテストを実装するときには、この考え方に則ってテストを書くように心がけましょう。 以下では、 testing-library が提供している機能を3つ紹介します。 要素を見つけるクエリ 一つ目は、ページに表示されている要素を見つけるクエリです。要素に対して動作をする場合も、要素の状態を検査する場合も、まずは要素を見つけることから始まります。 testing-library.com クエリには優先度があり、なるべく優先度が高いものを使うことが推奨されています。たとえば、 getByRole は優先度が高く、 getByPlaceholderText は優先度が低いです。これらはどのように決められているのでしょうか。 たとえば、ラベルが『生年月日』で プレースホルダ ーが "2023-03-27" であるような入力要素を見つけるクエリを作ってみます。次の2種類の実装のいずれも想定通りに動きました。 // 1. ラベルが『生年月日』である入力要素を取得する screen.getByRole ( "textbox" , { name: "生年月日" } ); // 2. プレースホルダーが "2023-03-27" であるような入力要素を見つける screen.getByPlaceholderText ( "2023-03-27" ); 1, のクエリは「ラベルが『生年月日』である入力要素」を取得しています。2. のクエリは「 プレースホルダ ーが "2023-03-27" であるような入力要素」を取得しています。ユーザがアプリケーションを使用する際、おそらく 1. のクエリのような考え方で要素を認識するでしょう。 getByRole が getByPlaceholderText よりも優先度が高い理由は、このようなユーザの考え方を反映しやすい傾向にあるからです。 必ずしも優先度が高いクエリを使うことが良いわけではないですが、なるべくユーザの考え方を反映させて要素を取得するクエリを書くことはよいテストを作りに欠かせないと思います。 ユーザの動作をシミュレーションする user-event ユーザの動作をシミュレーションするために user-event というライブラリを提供しています。 testing-library.com 特定の要素をクリックしたり、キーボードで入力したりをシミュレーションする際は次のように実装します。 // ユーザオブジェクトの生成 const user = userEvent.setup (); // ボタンをクリックする await user.click ( screen.getByRole ( "button" )); // 入力要素に「こんにちは」と入力する await user. type( screen.getByRole ( "textbox" ), "こんにちは" ); user.click を実行すると、実際に要素をクリックしたときと同じようにイベントが発火します。これによって、実装者はボタンをクリックしたときに裏側でどのような挙動が取られるかを気にすることなくテストを実装することができます。 同じようにユーザの動作をシミュレーションする方法に、同じく testing-library の fire-event を使った実装があります。こちらは、ユーザの動作そのものではなく DOM のイベントを発火させる機能を持っています。 user-event と fireEvent ではどちらを使うべきでしょうか。 ユーザがアプリケーションを使う場合、「このボタン要素の click イベントを発火させよう」と考えるのではなく「このボタン要素をクリックしよう」と思って使うはずです。したがって、テストをソフトウェアが使われる姿に似せるという観点において、 user-event を使うことが推奨されています。ただし、 user-event が再現できていないブラウザの挙動もいくつか存在します。そのような挙動に対してテストを書きたいときには fireEvent を使うといいでしょう *2 。 要素の状態を検査する jest-dom 要素の状態を検査する機能として、 jest-dom というライブラリが提供されています。 testing-library.com jest のマッチャーを追加して、確認(AAA パターン *3 における Assert)をしやすくしてくれます。 提供している関数の一覧 を見るとよくわかりますが、直感的に状態を確認できるようになっています。 たとえば、要素がフォーカスされていることをテストする場合は次のように実装します。要素がどういう状態のときにフォーカスされているかという実装の詳細には立ち入らず、要素がフォーカスされていることをユーザが認識するのと同じように、テストが実装されていることがわかります。 const inputElement = screen.getByRole ( "textbox" ); expect ( inputElement ) .toHaveFocus (); 実際にテストを書いてみる 具体的なテストの例を見てみましょう。次のような UI コンポーネント に対してテストを書いてみます。 この コンポーネント は次のような仕様を満たします。 ラベル『ユーザ名』『ニックネーム』『生年月日』の入力要素がある 入力して『送信』ボタンをクリックすると、 props で渡す submit 関数が呼ばれる。入力値はその引数として与えられる 『ユーザ名』『生年月日』は必須項目である。空文字列のまま『送信』ボタンをクリックすると submit が呼ばれず、アラートメッセージが表示される 次のような二つのテストを実装します。 すべての入力要素にデータを入力し『送信』ボタンをクリックすると、データが送信されること 『ユーザ名』だけ空文字列にして『送信』ボタンをクリックすると、データが送信されずにアラートメッセージが表示されること まずは、すべての入力要素に値を入力し、 submit 関数が呼び出されていることを確認するテストを実装してみます。 test ( "入力したデータが送信される" , async () => { // 準備フェーズ const mockSubmit = jest.fn (); render (< UserForm submit = { mockSubmit } / >); const user = userEvent.setup (); // 実行フェーズ await user. type( screen.getByRole ( "textbox" , { name: "ユーザ名" } ), "太郎" ); await user. type( screen.getByRole ( "textbox" , { name: "ニックネーム" } ), "たろちゃん" ); await user. type( screen.getByRole ( "textbox" , { name: "生年月日" } ), "2023-03-27" ); await user.click ( screen.getByRole ( "button" , { name: "送信" } )); // 確認フェーズ expect ( mockSubmit ) .toBeCalledWith ( { name: "太郎" , nickname: "たろちゃん" , "birthday" : "2023-03-16" } ); } ); 実行フェーズの await user.type(screen.getByRole("textbox", { name: "ユーザ名" }), "太郎"); は、「アクセシブルな名前が『ユーザ名』であるような入力要素に "太郎" と入力する」という意味です。 確認フェーズでは、 submit が呼ばれていることと、その引数として渡されるオブジェクトを検査しています。 次に、『ユーザ名』の入力要素に値を入力せずに送信ボタンをクリックした場合のテストを実装します。このとき、 submit 関数が呼び出されず、アラートメッセージが表示されることを確認したいです。 test ( "ユーザ名の入力がないと、データが送信されない" , async () => { // 準備フェーズ const mockSubmit = jest.fn (); render (< UserForm submit = { mockSubmit } / >); const user = userEvent.setup (); // 実行フェーズ await user. type( screen.getByRole ( "textbox" , { name: "ニックネーム" } ), "たろちゃん" ); await user. type( screen.getByRole ( "textbox" , { name: "生年月日" } ), "2023-03-27" ); await user.click ( screen.getByRole ( "button" , { name: "送信" } )); // 確認フェーズ expect ( mockSubmit ) .not.toBeCalled (); const alertTextBox = await screen.queryByRole ( "alert" ); expect ( alertTextBox ) .toHaveTextContent ( "ユーザ名が入力されていません。" ); } ); 前のテストと比べて、実行フェーズにおけるはアクセシブルな名前が『ユーザ名』である入力要素への入力がなくなりました。 確認フェーズでは、 submit が呼ばれなくなったことを確認しています。また、アラートが表示され、そのテキストについても検査しています。 いずれのテストも、ユーザがアプリケーションを使う際の使用方法や状態を観測する方法がそのままテストに反映されているのがわかると思います *4 。 このようなテストは、要素の順番が入れ替わったり一部の属性が変わったりしても影響を受けにくく、人にとって理解もしやすいです。なるべくシンプルなテストを実装できるように、ぜひ testing-library の提供している API を眺めて使い方を考えてみてください。 テストが書けないケース もし、テストを書いた UI が次のような マークアップ で実装されているとどうでしょうか。 < p > ユーザ名 </ p > < input id = "name" /> < p > ニックネーム </ p > < input id = "nickname" /> < p > 生年月日 </ p > < input id = "birthday" /> < div > 送信 </ div > この状態だと先ほどのテストを実行することはできなくなります。たとえば、『ユーザ名』という文字列は id="name" である入力要素のアクセシブルな名前として認識されませんし、『送信』と書かれている要素はボタンとして認識されません。 この実装は、テスト容易性が低いという以前にマシンリーダビリティが低い状態にあります。マシンリーダビリティとは、機械にとってのコンテンツの読み取りやすさの度合いです。フロントエンドのテストは、機械がコンテンツを読み取ったり操作をしたりしてソフトウェアの動作をシミュレーションするという性質上、マシンリーダビリティが高いほうがテスト容易性が高くなる傾向にある。 フロントエンドのテストの導入の前に、マシンリーダビリティ(ひいては アクセシビリティ )の向上を目指すとよいと思います。先ほどの実装は、たとえば、次のように修正することでマシンリーダビリティもテスト容易性も高まります。 < label for = "name" > ユーザ名 </ label > < input id = "name" /> < label for = "nickname" > ニックネーム </ label > < input id = "nickname" /> < label for = "birthday" > 生年月日 </ label > < input id = "birthday" /> < button > 送信 </ button > 運用するときの注意点 テストはソフトウェアの品質の向上になくてはならないですが、それ自体がソフトウェアの品質を向上させる魔法ではありません。 ソフトウェアの品質を上げられるようなテストを実装するために、次のようなことを事前に決めておくとよいです。 単体テスト を作る目的を明確にする バリデーションを含む入力フォームの コンポーネント 、特定の API を叩く コンポーネント 、他のページへの導線がある コンポーネント などなど、多種多様な コンポーネント ごとに必要になるテストは異なります。 単体テスト を作る目的は、ソフトウェアの退行に対する保護や リファクタリング への耐性を与えて、ソフトウェア開発プロジェクトの成長を持続可能にすることです *5 。しかし、何をもってして「持続可能である」と主張するかは人によって変わります。 どれだけのシナリオをカバーするテストを実装するか ソフトウェアがあるシナリオで仕様通りに動作することを検査するにはどのようなテストがよいか どういう状態のとき、"よいテストである"といえるか これらは、チームの思想や対象となるソフトウェアによって回答は様々でしょう。 カバレッジ を上げることを目的としてテストが大量に実装しても、これらを軽視してしまうと技術的負債になってもったいなんです。テストに取り組む前に、テストを実装する目的をしっかり考えられるとよいテストの実装ができると思います。もちろん、これらのことはフロントエンド以外のテストでも同じです。 アクセシビリティ の ガイドライン を定める 前述のとおり、マシンリーダビリティが高いほど testing-library を使ったテストが実装しやすくなります。 testing-library を使ったテストの品質を高めるためには、 アクセシビリティ の向上が必須と言ってよいでしょう *6 。 ただし、テストの品質の向上のためだけに アクセシビリティ の向上を目指すと、本当にユーザによってアクセシブルなソフトウェアになるとは限りません。 アクセシビリティ を向上させる目的がわからなくなってしまっては本末転倒です。 たとえば、 img タグに alt 属性を付与すると、 getByAltText を使って要素を取得することができます。一方、 aria-label を付与すると、 getByRole を使ったクエリで要素を取得することができるようになります。テストだけを考えると getByRole のほうがクエリの優先度が高いので aria-label 属性を付与するほうがよいように感じます。しかし、 alt と aria-label では挙動がことなり、一概に aria-label を使うことがよいとは言えません *7 。テストはあくまで内部品質の向上のために実装されるもので、内部品質の向上ために外部品質を棄損するのは避けたほうがよいです。 alt 属性を使った実装のほうがユーザ体験がよくなると判断したなら、 testing-library のクエリの優先度は無視して実装をするべきです。 まずはテストを気にせずに、 アクセシビリティ の ガイドライン を定めるのがよいでしょう。そして、制定された ガイドライン をもとにした実装に対するテストの実装方法について検討します。 もし、 ガイドライン に沿った実装ではテストを実装しづらいと感じるならば、ユーザへの影響がない範囲で ガイドライン を改定するのがよいと思います。 おわりに テストはユーザ体験に影響を与えませんが、開発者体験の向上に大きく寄与します。せっかくテストを作るのだから、より効果的なテストを書いて開発者体験を向上させたいです。 今回はフロントエンドの 単体テスト という観点でよいテストの書き方について考えましたが、 VRT や E2E テストでは、また違った観点が必要になります。様々なテストを使いこなすまでの道のりは長いですが、少しづつ改善していけるように努力していきたいです。 *1 : 画像引用: https://testingjavascript.com *2 : https://ph-fritsche.github.io/blog/post/why-userevent *3 : 単体テスト の考え方/使い方 p57-58 *4 : コミュニケーションベーステストはその性質上、ユーザが知覚するままのテストにはならないです。submit が呼ばれることの検査はコミュニケーションベーステストです。 *5 : 単体テスト の考え方/使い方 p6-10 *6 : https://logmi.jp/tech/articles/328087#s3 *7 : https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html#images-that-convey-information
アバター
こんにちは、タイミーのデータ統括部でデータサイエンティストをしている小関です。 タイミーのデータサイエンスチームでは、データ分析、機械学習モデル構築に加えて、Google Cloudを主軸としたMLOps基盤の構築などの業務に日々取り組んでいます。 その中でもGoogle Cloudを主軸としたMLOps基盤の構築に関連して、 Google Cloud Professional Machine Learning Engineer認定資格 を社内制度も活用しながら取得したので、実際にした勉強の内容などを紹介したいと思います。 これから受験される方の参考になれば大変嬉しいです! 受験の動機 筆者の勉強開始時の状況 勉強方法 1. 機械学習をビジネス活用する際のベストプラクティス 1.1. Googleが考える機械学習プロジェクトのベスプラ*1を理解 2. Google CloudのML関連サービス 2.1. Google CloudのサービスをWhizlabsのコース*2に付属している練習問題を解きながら整理 2.2. Googleが考える設計パターンをアーキテクチャセンター*3から理解 2.3. TensorFlow関連サービス*4の概要を理解 3. 機械学習・統計学に関する知識 3.1. Googleが提供している無料の機械学習コース*5を一通り閲欄 合格後貰えるCertificateとノベルティ Certificate ノベルティ We’re Hiring! 受験の動機 Google CloudのML関連サービス・ML周りの思想を資格勉強を通して整理したかったため MLOps基盤におけるアーキテクチャ設計を行うためのインプットをしたかったため 弊社で始まった 資格取得支援制度 をすぐ活用したかったため 当時、ドル円レートが¥140を超えており、受験料$200を会社で負担頂けるのは大変ありがたい... 筆者の勉強開始時の状況 勉強開始時の状況によって、勉強時間や内容が異なると思うので、 本試験に必要だと思われる知識・能力における、筆者の勉強開始時の状況を3つの観点で共有しようと思います。 下記の通り、筆者の勉強開始時の状況は、機械学習の前提知識、英語能力はある程度あるが、Google Cloudに関する知識はまだ浅めといった状況でした。 Google Cloud 触り始めて4ヶ月ぐらい Vertex AI上でのMLパイプライン構築をする過程で、その他の関連サービスも一通り経験している NLP、動画像系のサービスは一度も触った事がない TensorFlowとその関連サービスの深い知識はない 機械学習 新卒から3年間データサイエンティストとして働いており、主要なML関連の理論とその実装は身についている 英語 今回の試験で要求されるReadingレベルには、余裕を持って達している 勉強方法 試験ガイド に記載されている内容と受験した所感から、 出題される問題は以下の3観点で分類出来ると感じました。 機械学習をビジネス活用する際のベストプラクティス Google CloudのML関連サービス 機械学習に関する知識 ここからは、上で定義した3観点ごとに勉強した内容を紹介させて頂きます。 前章で触れた通りGoogle Cloudのサービス周りの知識がまだ浅かった事もあり、その点を重点的に学習しました。 1. 機械学習をビジネス活用する際のベストプラクティス 1.1. Googleが考える機械学習プロジェクトのベスプラ *1 を理解 DS、MLエンジニアとして働いている方は、一度は読んでおいた方が良さそうです 出題ポイントとして、特に抑えておいた方が良い主張は以下の通りです 解くべき課題に対してMLを用いるか否かは慎重に検討する MLを用いた解決策とMLを用いない解決策を比較する際は、改善幅、コストや保守性の観点から比較する MLの活用には、十分な量かつ品質が担保されたデータがある事が重要 実際の問題では、MLを用いた解決策と非MLな解決策が選択肢にあり、問題文にある具体的な課題設定に対して適切な解決策を選択するといった形式で出題されます 2. Google CloudのML関連サービス 2.1. Google CloudのサービスをWhizlabsのコース *2 に付属している練習問題を解きながら整理 各サービスの特性やアーキテクチャの設計を理解する上で重要な観点(出題ポイント) ノーコードで実現したいのか否か ノーコードと問題に書いてある場合には、GUI系 or AutoML系のサービスを選択する モデリングの工数を掛けるのか否か モデリングの必要が無く、事前学習済みのモデルで事足りる場合は、事前学習済みのモデルを使用できるサービスを選択する 少ない工数でカスタムモデルを構築する必要がある場合は、AutoML系サービスを選択する 自由度が高くある程度工数の掛かるモデルを構築する必要がある場合には、Vertex AI Workbenchなどの開発環境系サービスを選択する Googleのベスプラに則ったアーキテクチャで構成できているか 各サービスの特性や、後述するアーキテクチャセンターからGoogleが推奨するアーキテクチャを理解する 下記のGoogle Cloud上のML関連サービスとその機能を抑えておくと良さそうです データベース系 BigQuery フルマネージド、サーバーレスデータウェアハウス BigTable 大量のデータをリアルタイムで処理することに優れているNoSQLデータベース CloudSQL My SQL, PostgreSQL等をクラウド上で動かすためのマネージドサービス 分析基盤系 Dataflow リソースのオートスケーリングなどを特徴に持つETLツール Data Fusion GUI操作でETL・ELTパイプラインを構築出来るサービス Dataproc 分散処理ツールであるHadoop/Sparkの実行環境をクラウド上で提供してくれるサービス Pub/Sub リアルタイムデータやイベントデータの取り込みを行うためのメッセージングサービス 運用系 Cloud Build CI/CDの構築、実行を提供するサービス Cloud Source Repositories Google Cloud でホストされているプライベートGitリポジトリ 前処理系 Dataprep データを効率的にクレンジング処理出来るサービス Cloud Data Loss Prevention 機密性の高いデータを検出、分類、保護する機能を提供するサービス ML系 Vertex AI Google CloudのML関連のサービスが統合されたプラットフォーム AutoML 系のサービスもこちらに全て統合された Vertex AI Workbench, Vertex AI Data Labeling, Vertex AI Feature Store等の詳細な機能の概要も抑えておくと良さそうです gcloud CLI 経由でVertex AIのjobを実行するためのコードも少し出題されます BigQuery ML SQLを使用して、BigQuery上でMLモデルを構築し、実行できるサービス BQML上でサポートされているモデル は抑えておいた方が良さそうです Recommendations AI EC向けのレコメンドシステムを提供するフルマネージドサービス 最適化したい指標によって推薦の仕方を選択出来る Kubeflow Googleが公開している機械学習ワークフローをKubernetes上で実現するためのOSS Pythonでは、 Kubeflow Pipelines SDK を用いてMLパイプラインを実装する事が出来る 自然言語系 Speech to Text API 音声データをテキストに変換するAPI Text to Speech API テキストを自然な音声に変換するAPI Natural Language AI Natural Language API 事前学習済みモデルによって、感情分析、エンティティ分析、エンティティ感情分析、コンテンツ分類、構文分析などを提供するAPI 以下のサービスは、Vertex AI AutoMLへの統合に伴い公式に 非推奨 となったが、受験当時には出題されていた AutoML Natural Language API カスタムモデルによって、Natural Language APIの機能を提供するAPI Document AI API 非構造化データを対象に、データを簡単に理解、分析、利用できるようするAPI Dialogflow チャットボットサービスを作成できるサービス 動画像系 Vision AI Vision API 事前学習済みモデルによって、画像分類、オブジェクト検出などを提供するAPI Vertex AI Vision 動画像データの取り込みからモデル構築、デプロイまで一貫して行えるサービス 以下のサービスは、Vertex AI Visionへの統合に伴い公式に 非推奨 となったが、受験当時には出題されていた AutoML Vision カスタムモデルを構築して、Vision APIの機能を提供するサービス AutoML Vision Edge エッジデバイス向けに最適化されたモデルによって、Vision AIの機能を提供するサービス Video AI Video Intelligence API 事前学習済モデルによって、動画からの物体検出などを提供するAPI 以下のサービスは、Vertex AI Visionへの統合に伴い公式に 非推奨 となったが、受験当時には出題されていた AutoML Video Intelligence カスタムモデルを構築して、Video Intelligence APIの機能を提供するサービス 2.2. Googleが考える設計パターンをアーキテクチャセンター *3 から理解 ここに載っているアーキテクチャと類似するものが実際の問題でも出題されていました 出題される形式は、アーキテクチャを構成する一部サービスが空白になっており、そこに当てはまるサービスを選択するといった感じでした 2.3. TensorFlow関連サービス *4 の概要を理解 出題数は多くなかったですが、ここに載っているTensorFLow関連サービスの名前とその機能は一応抑えておくと良さそうです 3. 機械学習・統計学に関する知識 3.1. Googleが提供している無料の機械学習コース *5 を一通り閲欄 出題ポイントとして、特に理解しておいた方が良さそうな内容は以下の通りです 機械学習モデル 問題設定に対する適切な機械学習モデルの選び方 画像分類にはCNN、時系列問題にはRNNなど TensorFlowで書かれたDNNの構造を読み取る 評価指標 Precision, Recall, F1-scoreに代表される分類問題の評価指標の定義 目的に対する適切な評価指標の選び方 不均衡データに対する評価指標の選び方 前処理 カテゴリカルデータへの適切な前処理手法の選択 One-hot encoding, Label encodingなど 数値データへの適切な前処理手法の選択 Min-Max normalization, Z-score normalization, Log scalingなど 欠損データへの適切な対応 ハイパーパラメータチューニング DNNにおけるハイパーパラメータチューニング 学習率、バッチサイズ、エポック数、ドロップアウトなど 問題設定に対する適切なデータ分割手法の選び方 k-fold, group k-fold, leave-one-out, time series splitなど 過学習への対応 適切な正則化手法の選び方 Leakageが発生しうる条件の理解とその対応 合格後貰えるCertificateとノベルティ Certificate www.credential.net ノベルティ このMachine Learning Engineer Vestを着て出社すれば、社内で一目を置かれること間違いなしです。 2年前に受験した同僚は、タンブラーを貰えたらしい。羨ましい。 Machine Learning Engineer Vest We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています! product-recruit.timee.co.jp *1 : https://developers.google.com/machine-learning/problem-framing *2 : https://www.whizlabs.com/professional-machine-learning-engineer/ *3 : https://cloud.google.com/architecture/ai-ml?hl=ja *4 : https://www.tensorflow.org/resources/libraries-extensions?hl=ja *5 : https://developers.google.com/machine-learning
アバター
こんにちは、タイミーのデータ統括部でデータサイエンティストを担当している 小栗 です。 データ統括部は、組織内におけるデータ利活用を促進するため、データ分析、予測モデル構築、データ基盤構築などの業務に日々取り組んでいます。 今回、 部署内で「心理的安全性」に関する勉強会を開催しました 。 この記事では、 勉強会の内容をもとにして「心理的安全性」について解説 したいと思います。 心理的安全性とはなにか 「心理的安全性」とは 「アイデア・質問・懸念・間違いを話すことで、罰せられたり、辱められたりしないという信条のこと」 を指します。 もう少し噛み砕くと「アイデアや意見を言っても受け入れられ、評価される環境」と表現できます。 この概念を提唱したのはハーバード大学の組織行動学者であるエイミー・エドモンドソン博士です。 彼女が行なった病院の医療ミスに関する研究が、心理的安全性の概念のベースになりました [ 1 ]。 研究の結果、優秀な医療チームのスタッフは日頃から小さなミスや懸念点を率直に話し合っており、医療ミスが少ないことが分かりました。 その一方で、優秀でない医療チームでは、スタッフは日頃のミスや懸念点を報告せず、医療ミスが発生していることに気づきました。 この優秀な医療チームが持つ風土に、彼女は「心理的安全性」と名前をつけました。 その後、Googleの研究チームが「心理的安全性が高いチームはパフォーマンスが高い」という研究結果を発表すると、心理的安全性の知名度は格段に向上しました。 現在では、組織にもっとも必要な要素であると広く認知されていると思います。 心理的安全性が低い組織に存在する4つのリスク 心理的安全性について、もう少し解像度を上げて解説してみます。 エドモンドソン博士は、 心理的安全性が低い組織には対人関係における4つのリスクが存在する としています。 ◯◯だと思われたくない なので… 無知 必要なことでも質問・相談ができない 無能 自分の考えが言えない、ミスを隠す 邪魔 必要でも助けを求めず、不十分な仕事で妥協する 否定的 素直に意見を言えず、議論ができない 心理的安全性が低い組織では、これら4つのリスクの存在により、メンバーが恐怖を抱いてしまい表の右側の行動につながります。 その結果、 組織内のコミュニケーションが不足したり、アイデアが出なかったりと、組織の生産性が下がってしまう と考えられます。 心理的安全性を高めるメリット 次に、心理的安全性を高めるとどんな効果があるのか、エドモンドソン博士の著書で紹介されていた研究をもとに見ていきます [ 1 ]。 組織学習が促される メリットの一つに、組織学習が促進される点が挙げられます。 病院の集中治療室を対象に行われた研究では、心理的安全性が高いチームでは「知識の共有」などのチームベースの学習が活発に行われていました。そして、チームベースの学習は手術の成功率と関連があることが示されています [ 2 ]。 Googleの研究チームが行った研究では、Google社内においてチームの効果性にもっとも影響を与える因子は心理的安全性だと結論づけられています [ 3 ]。 そして、心理的安全性の高いチームのメンバーには以下のような特徴があることを報告しています。 離職率が低い マネージャーから評価される機会が2倍多い 他のメンバーが発案したアイデアをうまく活用できる 心理的安全性が高い組織では組織学習が促進され、組織と個人が高いパフォーマンスを発揮することができる可能性があります。 多様なメンバーのポテンシャルが発揮される 別のメリットとして、多様なメンバーのポテンシャルを引き出すことができる点が挙げられます。 多様性が尊重され、かつ心理的安全性が高い組織では、メンバーのパフォーマンスが高くなる傾向があることが研究で明らかにされています [ 4 ]。 この傾向は、組織内のマジョリティに属するメンバーより、マイノリティに属するメンバーにより強くみられました。 これは、心理的安全性はマイノリティに属するメンバーにとって特に重要なものであることを示唆しています。 別の研究では、メンバーが持つ専門知識の多様性が高いチームと、画一的なメンバーを集めたチームの比較を行なっています [ 5 ]。 研究の結果、専門知識の多様性が高いチームは、心理的安全性が高い場合は同条件の画一的なチームよりパフォーマンスが高くなる傾向があり、逆に心理的安全性が低いと画一的なチームよりパフォーマンスが低くなる傾向があることがわかりました。 多様性を尊重する組織においては、心理的安全性も併せて高めていくことで、各メンバーが高いパフォーマンスを発揮する土壌をつくることができそうです。 心理的安全性の落とし穴 一方で、心理的安全性をただ高めればいいというわけではなさそうです。 よくある落とし穴は、心理的安全性を高める努力をした結果、いわゆる「ぬるい職場」になってしまうことだと思います。 エドモンドソン博士は 「心理的安全性は、ただ親切にすることでも、パフォーマンス目標を下げることでもなく、その逆である」 と述べています [ 6 ]。 そして、 「心理的安全性」と「目標達成に対する責任感」どちらも高い状態である”Learning zone”を目指すべき としています。 心理的安全性を高めるだけに注力するのではなく、他の要素にも着目して組織の生産性を高めていくことが重要そうです。 引用: Amy C. Edmondson, “The Competitive Imperative of Learning”, Harvard Business Review 心理的安全性を高める方法 次に、心理的安全性を高める方法について考えてみます。 心理的安全性を高める4つの因子 日本国内で心理的安全性を広める活動をされている石井遼介さんは、 心理的安全性の高いチームをつくるためには、「話しやすさ」「助け合い」「挑戦」「新奇歓迎」という4つの因子が重要 だとしています [ 7 ]。 そして、それぞれの因子を満たすために必要なアクションを提唱しています。 心理的安全性を高める因子 因子を満たすアクション例 話しやすさ 意見をもらったら真っ先に「ありがとう」と伝える 助け合い 積極的に相談する、相談に乗る 挑戦 間違うことは悪くない、そこから学ぶことが重要だと伝える 新奇歓迎 違いを良い悪いではなく、ただ違いとして認める 上の表ではそれぞれの因子とアクション例をまとめていますが、勉強会ではもう少し多くのアクションを紹介しました。 気になる方は、 石井さんの著書 を読んでみてください。 タイミーのデータ統括部で実際に取り組んだこと タイミーのデータ統括部では、心理的安全性を高める第一歩として今回の勉強会を開催しました。 (とは言っても、心理的安全性について理解が深いメンバーがほとんどなので、「知識の再確認」といった感じでしたが…笑) 勉強会の後半では、心理的安全性をテーマとしたグループワークに取り組みました。 グループワークでは「心理的安全性を下げる対応をしてしまいがちな状況において、心理的安全性を高めつつ効果的に問題解決に近づくにはどういった行動をすればいいか」というテーマで議論を行いました。 例えば、以下のような状況における対応を参加者で議論する、といったものです。 とある問題に対する解決策についてチームで議論しているとき、メンバーの一人がある解決策を提案しました。 しかし、あなたのこれまでの経験や視点から考えると、その解決策は筋が悪いように思えます。 こんなとき、どう対応するのが良いでしょうか? 参加者からは、以下のような意見が挙がりました。 筋が悪かったとしても、「意見してくれたこと」に対して感謝を伝える メンバーが提案した解決策の軌道修正を一緒にやる 筋が悪いとはそもそも捉えず、メンバーがその解決策に至った思考プロセスを掘り下げて、認識や思考の違いを理解する 唯一絶対の正解がない問題をテーマに議論することで、自分一人では思いつかない対応や着眼点などが具体化され、有意義な時間になったと感じています。 勉強会やグループワークは地道ではありますが、大きな手間がかからないので取り組みやすいですし、心理的安全性について考えるキッカケになります。 組織の心理的安全性を高める手段のひとつとしておすすめです。 さいごに 近年、心理的安全性は組織にとって重要だと広く認められるようになりました。 しかし、心理的安全性の担保された組織をつくることは実際には想像以上に大変です。 タイミーのデータ統括部は心理的安全性が高い環境だと私は感じていますが、メンバーがどんどん増えていくフェーズを迎えていることもあり、心理的安全性をより一層意識し、高いレベルで担保できるよう努力したいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています! product-recruit.timee.co.jp
アバター
こんにちは、タイミー開発プラットフォームチームで業務委託として働いている 宮城 です。 タイミーはリリースから4年が経過したプロダクトで、2022年の前半から一部領域でGraphQLを利用し始め現在導入を進めています。 本記事では、GraphQLをプロダクトに導入する上で判断に迷った箇所や課題に対して、タイミーでの意思決定とその理由を紹介します。参考にしていただければ幸いです。 GraphQLの選定理由についてはこの記事では触れませんが、CTOの @kameike が以下のイベントで詳しく紹介する予定です。まだ参加申し込みは可能ですので、興味がある方はぜひ合わせてご覧ください。 timeedev.connpass.com なお、本記事のタイトルはソウゾウさんの以下の記事にインスパイアされています。 engineering.mercari.com GraphQLの「Getting Startedの次にぶつかる壁」について多く言及しており、プロダクトに導入する上で非常に有用な記事でした。合わせて一読することをオススメします。 前提となる技術スタック 技術選定において採用したライブラリ graphql-ruby Apollo Client @graphql-codegen/cli graphql-batch rubocop-graphql, graphql-eslint バックエンドで考えたこと graphql-rubyのデフォルトのディレクトリ構造を変更 バックエンドのテスト方針 Application-level Interface-level Transport-level nullability ページネーション query/mutationによって接続するDBのreader/writerインスタンスを切り替える Datadogによるリクエストの監視 フロントエンドで考えたこと GraphiQL Fragment Colocation Testing, Storybook オンボーディング 終わりに 前提となる技術スタック 「タイミー」の技術スタックを紹介します。バックエンドはモノリシックなRuby on Railsで構築されており、働き手となるワーカー向けモバイルアプリ(Swift/Kotlin)・雇用主となる企業向けWebアプリケーション(Rails SSR/Next.js)・社内メンバー向けWebアプリケーション(Active Admin/Next.js)の3つのアプリケーションを提供しています。 このうちWebアプリケーションはそれぞれNext.jsとRails APIを利用した構成への移行を進めており、その領域で利用する技術としてGraphQLを導入することにしました。 技術選定において採用したライブラリ GraphQLを導入する上で、最終的に採用したライブラリは以下です。 バックエンド: graphql-ruby クライアント: Apollo Client クライアントコード生成: @graphql-codegen/cli dataloader: graphql-batch linter: rubocop-graphql, graphql-eslint 一つひとつ選定理由を紹介していきます。 graphql-ruby github.com RubyでGraphQLサーバーを構築する上でデファクトスタンダードとなっているライブラリです。ShopifyやGitHubで長期的に利用されておりメンテナンスの継続可能性は高いと判断しています。タイミーではメンテナであるRobert Mosolgoさん *1 のスポンサーもしています。 GraphQLの仕様やベストプラクティスに従った実装がしやすいことが特徴的で、例えばページネーションのベストプラクティスであるCursor Connections *2 を追加実装なしで利用可能です。その他にも、ドキュメントの手厚さやテストの書きやすさなどから非常に使いやすいライブラリだと思います。 Apollo Client github.com フロントエンドのGraphQLクライアントにはApollo Clientを選定しました。ReactにおけるGraphQLクライアントの他の選択肢はRelay *3 , urql *4 , graphql-request *5 などがありますが、以下の観点からApolloを選択しました。 コミュニティが活発であり、利用者・インターネットの情報・関連ライブラリの種類等それぞれ大きいため、これから数年は利用が可能と想定できる graphql-rubyでフルサポートされているためApollo/graphql-rubyそれぞれで特に設定なしに利用可能であり、統合時に詰まるポイントが少なそう とはいえ、Apolloを選択する上で気になるポイントはいくつかあります。 正規化されたキャッシュがプロダクトのユースケースに即しているのか React Suspenseに対応していない *6 代替として比較したのがurqlで、Suspenseが使える・シンプルなドキュメントキャッシュ・軽量なバンドルサイズなど利点はかなり多いものの、以下の理由から選択せずにいます。 コミュニティの小ささと利用者の少なさの点でApolloに劣り、新しくGraphQLのキャッチアップを始めるタイミーにおいてはマッチしないと考えた。運営母体が小さいのも気になる。 Apollo, Relayは思想が違うためそれぞれ残り続けるだろうが、urqlが残り続けるかどうかについては不確実性がまだ高いと判断した あなたのプロダクトにApollo Clientは必要ないかもしれない *7 という一休さんの記事でApolloの向き不向きに関する言及があるが、(Suspense以外で)urqlで実現可能でApolloで実現不可能なことはないため、問題が出てきてから乗り換える形で問題ないと考えた 後述するgraphql-codegenはApolloとurqlの両方に対応しているため、Apollo Link *8 やキャッシュストラテジーの複雑なカスタマイズなどのようなApollo特有の機能を多用しなければ乗り換えは難しくない しかしGraphQLに精通したメンバーが多ければおそらくurqlを選択していたかなとも思います。クライアントライブラリについては運用を続けながら検討したいと考えています。 @graphql-codegen/cli www.the-guild.dev GraphQLサーバーから提供されるスキーマを基にReactのカスタムフックやTypeScriptの型を生成してくれるライブラリです。個人的にGraphQLを利用する大きな理由の1つであり、これのあるなしで開発体験が大きく変わるほどだと思っています。 各種設定などは後述のフロントエンド周りの実装方針の項で詳しく述べます。 graphql-batch github.com DataLoaderをRubyで実装するためのライブラリとしてgraphql-batchを選択しました。 GraphQLのクエリでは取得したいデータのノードを辿って必要なデータを一度に取得できるため、しばしばN+1が起きてしまいます。しかしどのフィールドの組み合わせが要求されるかはクエリによって異なるため、ActiveRecordモデルのassociationsの取得に対して単純にpreloadやeager_loadを行うのは無駄な読み込みが増えてしまい良い解決策とは言えません。 そのための解決策としてDataLoaderを利用した遅延読み込みを実装します。Rubyでの実装方法としては、今回選択したgraphql-batchかgraphql-ruby同梱のdataloaderの2つが選択肢として上がりそうです。 どちらも動かしてみた上で、今回はgraphql-batchを選択することにしました。 graphql-batch Shopifyがメンテナンスしており、ある程度枯れているといえる。ShopifyがGraphQLを利用し続ける限りはメンテナンスが続くと想定できる 大元のnode実装のdataloaderのAPIに近く、他言語での実装経験がある人は理解しやすい とはいえgraphql-rubyのfield extensionなどの複雑なことをしようとする場合、Promise.rbのキャッチアップが必要なのはデメリットか graphql-ruby同梱のdataloader 後発のためAPIが直感的で使いやすい印象 graphql-batchではPromiseオブジェクトをresolveしなければオブジェクトが手に入らない場面があったが、直接ActiveRecordオブジェクトが返ってくるため理解しやすい 2021年リリースで比較的新しい。内部的にはRubyのFiberを利用しているが、Fiberはデバッグが難しく問題が出てきた際の解決は難しい可能性が高い 今回は安全を取ってgraphql-batchを選択しましたが、何かあった際のgraphql-ruby同梱のdataloaderへの移行(またはその逆)は難しくないというのもあり暫定で意思決定しています。運用しながら判断をする予定です。 rubocop-graphql, graphql-eslint github.com github.com 新しくGraphQLを学ぶメンバーが多い環境のため、GraphQLの思想やベストプラクティスを学ぶためにも初期段階でLinterを用意しておくのは良いと判断し、上記2つを導入しました。 graphql-eslintについてはparserとしての役割も担い、VSCode上でgraphqlファイルを書く際の補完も有効になるのが便利です。 バックエンドで考えたこと ここからは実装を進める上でぶつかった課題についてそれぞれ述べていきます。まずはバックエンドから。 graphql-rubyのデフォルトのディレクトリ構造を変更 graphql-rubyではgeneratorが付属しており、 rails generate graphql:install で必要なファイル群が生成され以下のようなディレクトリ構成になります。 ❯ tree app/graphql app/graphql ├── mutations │   └── base_mutation.rb ├── timee_schema.rb # Railsアプリケーション名から自動で命名される └── types ├── base_argument.rb ├── base_connection.rb ├── base_edge.rb ├── base_enum.rb ├── base_field.rb ├── base_input_object.rb ├── base_interface.rb ├── base_object.rb ├── base_scalar.rb ├── base_union.rb ├── mutation_type.rb ├── node_type.rb └── query_type.rb このコード群のうち気になる点がいくつかありました。 TimeeSchemaや追加された全てのクラスにおいて、名前空間がグローバルに設定されている typesディレクトリのクラスは Types::BaseArgument となり、TypesモジュールはGraphQL以外でもよく使われるはずで名前空間の範囲が広すぎる たくさんのbaseクラスがtypesディレクトリにヒラ出しになっている。それぞれのbaseクラスを継承した具象クラスをこのディレクトリに追加していく場合すぐ見通しが悪くなることが見込まれる そのため以下のようにディレクトリ構成を変更しました。 ❯ tree app/graphql app/graphql └── graphql_schema ├── arguments │   └── base.rb ├── connections │   └── base.rb ├── edges │   └── base.rb ├── enums │   └── base.rb ├── fields │   └── base.rb ├── input_objects │   └── base.rb ├── interfaces │   └── base.rb ├── mutations │   └── base.rb ├── objects │   ├── user_type.rb ... 具象クラスの例 │   ├── base.rb │   ├── mutation_type.rb │   └── query_type.rb ├── scalars │   └── base.rb ├── timee_schema.rb └── unions └── base.rb graphql_schemaディレクトリにラップし、それぞれのtypeごとにディレクトリを用意しています。 GraphqlSchema::TimeeSchema などのクラス名になり、GraphQL関連のクラスは全てGraphqlSchemaネームスペース下に含まれることになります。 これにより外からこれらのクラスを参照することがもしあったとしたら何かおかしいと気づけるはずです。 private_constant を利用して機械的に可視性の制限もできます。 このディレクトリ構成で困ったことはほぼなかったのですが、 rails generate graphql:object などのgraphql-rubyが提供するScaffoldジェネレーターがそのまま利用できない問題がありました。ActiveRecordモデルに対応するオブジェクト型クラスを作る場合、モデルのattributesからフィールドの型を類推しコード生成してくれるため非常に便利で、どうにか活用したいです。 ジェネレーターのテンプレートで Types モジュール下にクラスがある想定なのが原因で *9 、ジェネレータークラスを継承したカスタムジェネレーターを作ることで解決できました。 # lib/generators/timee/graphql/object/object_generator.rb require ' generators/graphql/object_generator ' module Timee module Graphql class ObjectGenerator < :: Graphql :: Generators :: ObjectGenerator source_root File .expand_path( ' templates ' , __dir__ ) def create_type_file template ' object.erb ' , "#{ options[ :directory ] } /graphql_schema/objects/ #{ subdirectory } / #{ type_file_name } .rb " end # idフィールドはGraphQL::Types::Relay::Nodeで実装するため除外する def normalized_fields super @normalized_fields .reject! { _1.instance_variable_get( :@name ) == ' id ' } end end end end # lib/generators/timee/graphql/object/templates/object.erb <% module_namespacing_when_supported do -%> module GraphqlSchema module Objects class < %= ruby_class_name %> < Base implements GraphQL::Types::Relay::Node <% normalized_fields.each do |f| %> <% = f.to_object_field %> <% end %> end end end <% end -%> これにより rails generate timee:graphql:object MyNamespace::ModelName でオブジェクト型クラスをScaffoldingできます。 バックエンドのテスト方針 チーム開発でアーキテクチャを安全にスケールさせるためには、定義したレイヤーに対応したテスト方針を用意しておくことは開発メンバー間での認識を揃えるために有用と考えています。graphql-rubyのドキュメントには以下の3つのレイヤーとテスト方針が紹介されており *10 、それを基にタイミーでのテスト方針を定めました。 Application-level 認可やビジネスロジックのレイヤーです。 ActiveRecordモデルやPOROなどのGraphQLとは別のレイヤーなどにロジックを書き、単体テストを書くのが良いでしょう。 Interface-level GraphQLの各query/mutationのレイヤーです。 各query/mutation単位で期待するフィールドが返るかを検証するテストを書くのが良いでしょう。以下はgraphql-rubyのドキュメントに記載されている参考例です。 it " loads posts by ID " do query_string = <<- GRAPHQL query($id: ID!){ node(id: $id) { ... on Post { title id isDraft comments(first: 5) { nodes { body } } } } } GRAPHQL post = create( :post_with_comments , title : " My Cool Thoughts " ) post_id = MySchema .id_from_object(post, Types :: Post , {}) result = MySchema .execute(query_string, variables : { id : post_id }) post_result = result[ " data " ][ " node " ] # Make sure the query worked assert_equal post_id, post_result[ " id " ] assert_equal " My Cool Thoughts " , post_result[ " title " ] end MySchema.execute に対してクエリ文字列やvariablesを渡し、期待するレスポンスが返るかどうかを検証しています。 API実装時には基本的にこのテストを書くことが多くなるはずです。 Transport-level GraphQLサーバーはHTTP(Railsのrouting)で提供しています。 疎通確認のために1件だけrequest specを用意しました。個別のquery/mutationについてテストする必要はありません。 nullability graphql-rubyで生成したschema.graphqlを見ると、connection_typeのnodesフィールドなどほとんどのフィールドがnullableで定義されていることに気づきます。自身でフィールドを定義する場合もデフォルトではnullableであり、non-nullにしたければ明示的に null: false を付与する必要があります。 この仕様を知った時、なぜnullableなのか?と違和感がありました。nullableではフロントエンドの多くの箇所で存在チェックをしなければならなくなり、無駄にコードを複雑にしてしまいます。 GraphQL公式ドキュメントのベストプラクティス *11 によると「データベースやその他のサービスに支えられたネットワークサービスでは、うまくいかないことが度々あるからだ」と述べられています。またWhen To Use GraphQL Non-Null Fields *12 というブログ記事では「GraphQLはバージョンレスAPIの思想を持ち、多くのチームが一つのAPIに依存するため破壊的変更が難しくなりやすく、スキーマの進化を困難にさせる」と述べられています。 チームでの議論の上、そうした背景も踏まえつつもREST APIでのリソース設計と同様で「理由がない限りnon-nullのフィールドとする」方針で進めることにしました。理由は以下です。 non-nullのフィールドに対してDBから返却されたデータがnullであった場合、graphql-rubyがそのクエリ自体をエラーとしてハンドリングしレスポンスを組み立ててくれること そもそもそのような状態はビジネスロジックの要求を満たせていないことが多いはずで、nullableにしてしまうことで問題に気づけない状況を防ぎたい 一般的にnon-nullからnullableに変更する方が簡単であり、その逆は困難を伴うか不可能な場合が多い GitHubのAPIでは、connection typeのほとんどがnon-nullとなっている e.g.) https://docs.github.com/en/graphql/reference/objects#organization プロダクトの特性上、破壊的変更に対処することが難しいわけではない GraphQL APIを外部に公開する予定がなく、ステークホルダーの調整も社内で完結する クライアントは現状Webのみのため、オンデマンドなリリースが可能 graphql-rubyの場合、non-nullにオプトインする設定が用意されているためそれを利用すれば良いです。 https://graphql-ruby.org/api-doc/2.0.14/GraphQL/Types/Relay/ConnectionBehaviors/ClassMethods#node_nullable-instance_method ページネーション graphql-rubyでは標準のページネーションとして cursor-based pagination の仕組みが導入されています。 cursor-based paginationでは前後のページのカーソルのみ手に入るため、Googleの検索結果にあるような ページ番号を選択して直接ページに飛ぶ ことができません。 タイミーでGraphQLを導入するのは主に管理画面であり、管理画面のようなプロダクトでページ番号による操作が行えないことは困る場合が多いため、もう1つのページングアルゴリズムであるoffset-based paginantionを自前で実装しています。 主にこれらの記事を参考にしています。 Generic page number / per-page pagination with GraphQL-Ruby · GitHub ichikawa-dev.hatenablog.jp query/mutationによって接続するDBのreader/writerインスタンスを切り替える タイミーではRailsのマルチDB機能を利用しているため、GraphQLでも活用するためにリクエストに含まれているquery/mutationを識別してDBインスタンスを切り替える仕組みをgraphql-rubyのtracerを使って実装しました。詳しくはこちらに記載しています。 zenn.dev Datadogによるリクエストの監視 GraphQLは単一のPOSTエンドポイントに全てのリクエストが送信されるため、通常のAPMを利用したエンドポイントの監視では適切な監視を行えません。そのためgraphql-rubyでは、リクエストをトレースしAPMとの統合を行える仕組みが用意されています。タイミーではDataDogを利用しており、graphql-rubyがデフォルトでDataDogとの統合が提供されていました。 これにより、GraphQLのoperation name単位で分類しつつ監視できます。 フロントエンドで考えたこと GraphiQL GraphiQLは、GUIでGraphQLを操作するための統合開発環境です。コード補完・シンタックスハイライト・APIドキュメントの閲覧と、実際にquery/mutationも実行可能です。 graphql-rubyでは https://github.com/rmosolgo/graphiql-rails が初回セットアップ時に追加されるためそれを使うのが一般的かもしれませんが、graphiql-railsを使うとなるとタイミーの場合APIリクエストをする際の認可プロセスをNext.js用に用意しているものとは別で用意しなければならず、理想とはいえませんでした。 npmで公開されている https://www.npmjs.com/package/graphiql が提供しているReactコンポーネントが利用できることに気づき、そちらをNext.js上で利用することで解決しました。どんなクエリでも実行できる自由さは危険なため、Production環境では使用できないようにしています。 Fragment Colocation 今回はプロジェクト開始時点でFragment Colocationの方針で進めることにしました。Colocationは「一緒に配置する」という意味とのことで、GraphQLのFragment定義とコンポーネントを近い場所に置き、コンポーネントに必要なデータを宣言的に定義する方針を取っています。 より具体的な概念や実現方法はこのスクラップで詳細にまとめられています。 zenn.dev RelayではFragment Colocationが “most important principle” とされていて厳格な運用を強制されますが、Apolloではそこまで堅い設計になっていません。実際の運用としてはgraphql-codegenのnear-operation-file *13 プラグインを利用することで運用が可能です。graphqlファイルの同階層にコード・型を生成したファイルを配置してくれます。以下がcodegen.ymlの設定例です。 overwrite : true schema : 'src/apis/graphql/schema.graphql' documents : - "src/**/*.graphql" generates : src/ : preset : near-operation-file presetConfig : extension : .generated.ts baseTypesPath : ~~/apis/graphql/types.generated plugins : - 'typescript-operations' - 'typescript-react-apollo' config : immutableTypes : true nonOptionalTypename : true avoidOptionals : true ~~~ 省略 ~~~ Fragment Colocationを運用することで以下のメリットがあると判断しています。 クエリの実行はPageコンポーネント・各コンポーネントで必要なデータはfragmentとして定義するようにルール化すると、初回ページ表示に必要なネットワークリクエストを1回で終えられる 必要なデータがコンポーネントの横に定義されているので見通しが良い・不要な定義の削除漏れに気付きやすい・underfetching/overfetchingに気付きやすい 生成コードを1つのファイルにまとめるデフォルトの方法と比べてファイルチャンクの最適化が可能、ページに必要なコードのみを含めることができる ref: https://blog.hiroppy.me/entry/2021/08/12/092839 課題としてfragmentでvariablesを定義できないことの不都合や、mutationのrefetchQueriesとの相性が悪いなどは考えられますが、それを差し置いてもメリットは大きいと思っています。 Testing, Storybook コンポーネントのテストはGraphQLがなくても変わらず、Storybookを起点に行なう方針としています。 Fragment Colocationを利用しているため大半のコンポーネントは必要なデータに相当するpropsを渡すだけで問題ないので考えることは少ないですが、ネットワークリクエストが発生するPageコンポーネントではリクエストのモックを検討する必要があります。 モックを実現するためのライブラリとしてはstorybook-addon-apollo-client *14 とMock Service Worker *15 の2つが選択肢として考えられますが、チームの学習負荷が大きい状況だったため一旦は学習コストの少ないstorybook-addon-apollo-clientを選択しました。しかしGraphQLの導入とは別の課題感からmswを導入する動きがチームで始まっており、ゆくゆくはmswに載せ替えていきたいと考えています。 またモックデータを簡単に生成するための仕組みとしてgraphql-codegen-typescript-mock-data *16 を導入しています。Railsでよく使うFactoryBotのように簡単にオブジェクトが生成できてかなり便利です。概要はこちらの記事が理解しやすいかと思います。 zenn.dev オンボーディング 最後に組織へのGraphQLの浸透のためのオンボーディングについて紹介させてください。 タイミーでは組織の拡大に伴って多くの課題が生まれては乗り越えてきました。現在は書籍チームトポロジーを組織内の共通言語として扱う取り組みを進めつつ、チームトポロジーをベースにチームの役割の再定義・分割を進めています。その過程で事業価値を最大化を目指す複数のチーム(チームトポロジーで紹介されているストリームアラインドチーム)の他に、ストリームアラインドチームを支援する開発プラットフォームチームを立ち上げました。 *17 開発プラットフォームチームは、ストリームアラインドチームが自身の責任領域に注力しやすくするためにバリューストリームから遠い技術的な領域に関する認知負荷の削減をミッションとしています。 筆者も開発プラットフォームチームに所属しGraphQLの導入に取り組んできましたが、ストリームアラインドチームが解決したい課題を解決するためにGraphQLを利用できて、すぐにバックエンドのquery/mutationを、フロントエンドのGraphiQLでクエリをそれぞれ書き始められる状態を目的としていました。 合わせて弊チームはチームトポロジーでいうイネイブリングチームとしての側面も持ち、GraphQLのスキルをストリームアラインドチームが習得するための短期的な支援も担っています。そのための支援の例としてモブプログラミング会を開催したり、社内記事の充実化を図ったりしています。本記事は社内ドキュメントに書き連ねていた内容からプロダクト固有の内容を省略しまとめたものだったりもします。 社内記事の例 またGraphQLを利用した実装で判断に迷う時の議論を減らすための土台としてコーディングスタイルガイドの制定に取り組んでいます。まだ進行中ではありますが、目次だけ紹介します。 目次 ストリームアラインドチームが実際に機能実装をする過程で出てくる課題によって加筆修正を繰り返していく予定です。本文が読みたい方はぜひ入社を、カジュアル面談もお待ちしています。 終わりに 本記事では、RailsとNext.jsを利用しているプロダクトでGraphQLを導入する際に考えたことを紹介しました。概念等の説明は少なめに、より実践的な内容の紹介を目的としていました。組織でGraphQLを導入を始める際に議論の参考にしていただけると幸いです。 ここまで読んでいただきありがとうございました。 *1 : https://github.com/rmosolgo *2 : https://relay.dev/graphql/connections.htm *3 : https://relay.dev/ *4 : https://formidable.com/open-source/urql/ *5 : https://github.com/prisma-labs/graphql-request *6 : ApolloでのSuspenseはサポート予定ではあるようです。 https://github.com/apollographql/apollo-client/issues/9627 *7 : https://user-first.ikyu.co.jp/entry/2022/07/01/121325 *8 : https://www.apollographql.com/docs/react/api/link/introduction/ *9 : https://github.com/rmosolgo/graphql-ruby/blob/db26a55a639a47f1206f5f7a3bf70ebbb61aaed0/lib/generators/graphql/templates/object.erb *10 : https://graphql-ruby.org/testing/integration_tests *11 : https://graphql.org/learn/best-practices/#nullability *12 : https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8 *13 : https://www.the-guild.dev/graphql/codegen/docs/presets/near-operation-file *14 : https://storybook.js.org/addons/storybook-addon-apollo-client *15 : https://mswjs.io/ *16 : https://github.com/ardeois/graphql-codegen-typescript-mock-data *17 : タイミーのプロダクト組織の変遷やなぜチームトポロジーか、に興味がある方はこちらの記事もご覧ください。 チームトポロジー Vol. 2 「組織をチームトポロジーで振り返るメリット」タイミー 亀田 彗 | Forkwell Press | フォークウェルプレス
アバター
こんにちは、タイミーでバックエンドエンジニアをしている難波 @kyo_nanba と申します。 今回は9月8, 9, 10日に開催され、タイミーもプラチナスポンサーとして協賛したRubyKaigi 2022の参加報告になります。 こういった大規模カンファレンスは昨今の情勢もありオフラインでの開催がなかなか難しい状況でしたが、今年は 三重県 津市で現地開催されるということになりぜひ参加したいという有志が集まって参加させて頂くことになりました。 なおタイミーでは自分達がお世話になっている技術や OSS に対してコントリビュートやスポンサーなど様々な面から貢献することを推奨しており、今回のRubyKaigi参加もその一環として社内参加者には移動費や宿泊費などが補助されています。感謝 🙏 また余談ですがRubyKaigiとほぼ同時期に開催された iOSDC Japan 2022 についてもスポンサーをしております。タイミーの使用している技術などに興味を持って頂いた方はぜひ こちら をご覧ください。 当日のタイミーチーム 今回タイミーとして参加させて頂いたのはエンジニアが2名(プラス現在お手伝い頂いている業務委託の方が1名)、技術に興味があり参加したいと手を挙げてくれたカスタマーサポートの方が1名、プロダクトHRの方が2名という少し変則的なチームでした。 エンジニアの参加はもちろんなのですが、タイミーでは開発組織をよりスケールしていくために開発組織の人事企画・採用を担当する「プロダクトHR」という部署があり、その活動をより加速させていくためにもまずは知ることから始めようと、様々な知見を得る機会として一緒に参加頂きました。カスタマーサポートの方も含めエンジニア以外の職種でもプロダクトを支える技術に興味を持っている方が多くいるのがタイミーの良いところの1つかなと思います。 なお、そのプロダクトHRからも別途 RubyKaigi 2022参加記事 が公開されているので、よろしければそちらもご覧ください。 発表について 3日間様々な発表に参加させて頂きましたが、どれもとても濃い内容の発表ばかりでした。 発表で得た気づきをお互いに共有したり改めて考えてみるために後日社内の参加した人たち、参加できなかった人たちで集まって振り返り会を行ったので、ここでは振り返り会の中で話題に挙がった、特に印象に残った発表についていくつか簡単に紹介したいと思います。 Ruby meets WebAssembly Ruby meets WebAssembly - Speaker Deck まずは一日目のキーノートである " Ruby meets WebAssembly" になります。社内振り返り会でも「 Ruby で書かれたものがJSに コンパイル されWebで表示されて、今のフロントのトレンドが変わっていくのかなぁなど個人的にWebAssemblyに興味を持った」や「初学者のプログラミング学習において最初の壁となるのはやはり環境構築であり、ブラウザで動かせることは裾野を広げるためにも非常に大事」など、この辺りの技術がより深まっていくことに対する期待の声が多く挙がりました。 TRICK 2022 (Returns) GitHub - tric/trick2022 こちらについてはやはり見た目の インパク トが強く「ターミナルで実行される Ruby のプログラムの文字列自体がアニメーションされるのはすごすぎた。最初の単純な作品を見た時はこういうのを作りながら Ruby を学ぼうかなと思ったが、上位の作品が発表される度に『すごいなぁ』と思うだけになっていました。」など社内振り返り会でも驚きの声が挙がっていました。 ruby /debug - The best investment for your productivity こちらは Ruby 3.1から同梱されるようになった debug.gem についての解説でした。今まで弊社でよく使われていたのはbyebugとpryだったのですが、こちらの発表を機にモチベーションが高まり現在では社内で最も大きい Rails プロジェクトの リポジトリ もdebug.gemと irb に代わっております。 グルメについて RubyKaigiといえば開催地の名産など様々なものを楽しむのも醍醐味です。 三重県 といえば鰻や松坂牛は言わずもがな、地元のお酒や海産物など様々なグルメがあります。RubyKaigi開催中は生憎の天候になることもままありましたが、夜は比較的天候が落ち着いたこともあり、いろいろなお店で地元のグルメを楽しむことができました。 写真を見ていると三重の食事が恋しくなってしまうのでグルメの話はこの辺りにしておきましょう。 参加してのひとこと 最後に少し私事ですが、私が前回RubyKaigiに参加したのは2018年の仙台開催だったので実に4年ぶりのRubyKaigi参加でした。あの頃とは情勢の変化もあり盛り上がり方に多少の変化はありましたが、やっぱりRubyKaigiは年に一度 Rubyist が集まるお祭りといった雰囲気で、参加者のみなさんから感じるモチベーションの高さや楽しんでいる様子から自分自身ももっとアウトプットしていこう!英語を勉強しよう!という気持ちにさせられます。 次回は長野県 松本市 での開催ですが、弊社としてもブースを出したり他のスポンサードを考えたりと、もっと積極的に関わっていきたいですね。 最後にこの難しい状況の中で入念に準備を行い無事に会を成功させた主催者の方々に心より感謝を申し上げます。
アバター
はじめに こんにちは、フロントエンドエンジニアの樫福 @cashfooooou です。 先日、 和田卓人氏(以下、 t_wada さん)に「質とスピード」というテーマで講演をしていただきました。 この講演にはエンジニア以外の方々も参加してくれました。 僕は学生時代に t_wada さんの テスト駆動開発 についての講演を聞いたことがあり、それ以来 テスト駆動開発 を取り入れるようになりました。 今回の講演でも、なにか気づきが得られるとうれしいなあとワクワクしながら参加しました。 はじめに こんな講演でした 冒頭で投げられた問い 犠牲にされがちな「品質」とはなにか 内部品質を犠牲にしたときのスピードの損益分岐点はどこか 講演会の振り返り エンジニアの振り返り エンジニア以外の参加者の感想 おわりに こんな講演でした 講演の内容を簡単にまとめてみました。 t_wada さんが公開されている こちらの資料 もぜひ参考にしてください。 冒頭で投げられた問い 講演の冒頭で二つの問いが投げられました。 「スピードを得るために品質を犠牲にします」と言うときの「品質」とは? 品質を犠牲にしてスピードが得られる「短期」と逆にスピードが得られなくなる「中期」の境目は? 犠牲にされがちな「品質」とはなにか SQuaRE という品質モデルでは、品質を大きく「利用時の品質」と「製品品質」に分けます。 利用時の品質は実際にソフトウェアを利用するユーザにとっての品質を指し、製品品質はソフトウェアの開発者にとっての品質です。 さらに、「製品品質」は「外部品質」と「内部品質」とに分類できます。 外部品質はソフトウェアの正しさや使いやすさなど利用時の品質に直接影響を与えるもので、内部品質はコードの読みやすさや理解のしやすさなど利用時の品質に直接的な影響を与えないものです。 「スピードを得るために品質を犠牲にします」といって犠牲にするのは「内部品質」で、もっと踏み込むと「保守性」です。 保守性とは「テスト容易性」「変更容易性」「理解容易性」などから構成されます。 では保守性とスピードは トレードオフ の関係なのでしょうか?答えは NO です。 保守性が高まると開発しやすくなりスピードが上がります。スピードが上がることで学習する機会が増えて保守性が高まるのです。 質とスピードは相互に高め合う関係にあることがわかりました。 内部品質を犠牲にしたときのスピードの 損益分岐点 はどこか 内部品質を犠牲にしたときスピードが得られるのは1ヶ月以内だと言われています。 内部品質を犠牲にしてもたったそれだけしかスピードが得られないならば、常に内部品質の高い実装を目指したほうがよいですね。 講演会の振り返り エンジニアの振り返り 講演会の終了後に、各チームで「品質の高い理想的な状態とはなにか?」について話し合ってもらいました。 あるチームは Miro を使ってアイディアを出し合い、「テスト容易性」「変更容易性」「理解容易性」の観点で話しました。講演会と話し合いを通じて次のような感想をいただきました。 バックエンドエンジニアの方 もともと明文化されてなかった「品質の高い状態」の共通認識が取れたと思います。 思い返すと、これまで議論をするときに「品質の高い状態」の認識のズレのせいでうまく進まなかったことがあったかもしれないなあと感じました。 講演会を聞いて品質についてチームで振り返ったことで、この人はこんな観点を気にしてたんだ!という気付きに繋がりました。 今後、チームで議論をしていくときには「どういう観点をもって話しているか」をより意識した良いコミュニケーションがとれて品質を高めることができそうです。 エンジニア以外の参加者の感想 営業部の方 「スピードと質が相互に高め合う関係にある」という話は営業にも通ずると感じました。 私達にとっての「スピードが速い」状態とは「営業活動の回数が多い」こと、「質が高い」状態とは「営業活動に再現性がある」ことではないかと考えています。 営業職はお客さんと話して初めて気づくことがたくさんあるので、いい営業活動をするために場数を踏むことは必須です。 場数を踏んで得られる知見は、データに起こし効果検証を経た上で社内に共有するようにしています。こうすることで自身の営業活動の再現性を高めるともに 、初めて営業活動をする人でも既知の問題をクリアできる角度の向上や 工数 の短縮に繋がります。結果として、未知の問題に取り組む機会も増やすことができます。 この「場数をこなすから再現性が上がる、再現性が上がるから場数がこなせる」という好循環を意識していきたいですね。 広報の方 エンジニアの方がどんなことに興味を持ってるのか、気になって参加してみました。 「品質とスピードのどっちが大切か…」という悩みが解決してよかったです。 さらに、エンジニアの方々も同じような悩みを抱えていると知ってより親近感が湧きました。 私は仕事で原稿を書くことが多いのですが、原稿の品質は人に読まれることで判断されます。 講演内で「スピードを速くすることで学習する機会が増え、高品質に繋がる」という話がありましたが、 自身の仕事では、「まず原稿を書き上げてしまって、レビューや推敲が何度もされている状態」に置き換えられると思いました。 時間をかけてもいい記事が書けるとは限らないですからね。 学習機会を増やして品質を高くするという考えは大切に普段 からし ていきたいです。 エンジニア以外の皆さんにも、とても勉強になったと言っていただきました! 皆さんの感想を伺いながら、部署やチームにとっての質やスピードを定義すること、メンバーがその定義を理解していて共感できていることが大切だと感じました。 また、部署を横断した取り組みについては、お互いの質とスピードの定義について理解し合い、双方の質とスピードが高くなるような取り組み方が模索できると理想的だなあと思いました。 ゆくゆくは会社全体で「質とスピードの高いサービスの提供」ができるようになることを目指していきたいです。 おわりに t_wada さんをお招きし「質とスピード」というテーマで講演をしていただきました。 講演会に加えてチームでの振り返りを行ったことで、今後のいい開発体験に繋がりそうです。 エンジニアだけでなく会社全体に浸透する文化にしていきたいなあとも思っています。 今回の講演会をきっかけに、みんなの質とスピードについての考え方が変わりました。 次は「どのよう質とスピードを向上させるか」という技術的な課題にも取り組んでいきたいです。
アバター
不定期な割り込みタスクは見落としやすく、振り返りづらい Slack + Notionで、割り込みタスクを管理する CSメンバーはNotionに起票後、Slackで報告 エンジニアメンバーは、Daily Standupで優先度をつけ着手 職種をまたいだ依頼フローをもっと整えたい ※このブログは Cocoda さんに寄稿したものです。 タイミーでバックエンドエンジニアをしている edy です。 スキマ時間にバイトができるアプリTimeeを運営しています。 timee.co.jp エンジニアとしてサービス開発に関わる中で、日々の スクラム などで「計画的に行っているタスク」とは別に、「CSなど別チームから急に依頼されたタスク」に対して、どんな優先度で、どのように向き合っていくとよいのか頭を悩ませていました。 試行錯誤した結果、タイミーではNotionを活用してそのような「割り込みタスク」に対処する運用フローをつくり、うまく回り始めています。 お客様のサポートをするCSメンバーは、Notionに起票後、Slackのワークフローを使ってエンジニ アメンバー にお知らせ 起票を受けたエンジニアは、「対応可能なチケット数」に応じて、対応するかどうかや、優先度を決定し、解消に向かう 今回は、CSメンバーとエンジニアを滑らかにつなぐ、NotionとSlackを使った割り込みタスクの運用方法について、その背景やプロセスをまとめていきます。 プロダクトの改善フローや、体制づくりに悩まれているPMやエンジニアの方々の力になれると嬉しいです。 不定 期な割り込みタスクは見落としやすく、振り返りづらい これまで、「サイトの情報が古いから変更をお願いします」といった割り込みタスクはSlack上にのみ流れていて、依頼する人によって依頼の仕方もマチマチ、依頼の方法も不明、エンジニアも都度対応するのは大変、といった状況でした。 過去のSlackのみの運用(2019年頃) スレッドの件数がかさむことがしばしば発生していました。 そこで、ストック情報を扱うツールとしてNotionを活用することにしました。 Notionは普段プロジェクトやタスクの管理、ドキュメントツールとしても使っているので、割り込みタスクもNotionで管理することに。 Slack + Notionで、割り込みタスクを管理する タイミーの割り込みタスク管理は、NotionとSlackを使い「割り込み可能なチケット数」を決めて、チケット数の範囲であれば、割り込みタスクに対応していくような流れになっています。 CSメンバーはNotionに起票後、Slackで報告 まずは、サポートメンバーが、プロダクトの不具合や要望をNotionに起票。 割り込み可能なチケット数は、エンジニアの負担や割り込みタスクの数から、現在は2週間で14と決めています。 そのため、CSメンバーは残りチケット数を見ながら起票を進めていきます(厳密には、CSのリーダーが「チケットを使ってでも解消したい」というふうに優先度を決めてくれています。)。 割り込み可能なチケット数は、Notionの計算機能を使って「14 - 現在のチケット数」で算出しています。 割り込みタスクの種類には、3種類を用意しています。 データ変更依頼 … 古い情報の差し替えなど 仕様確認 … ボタンの文言がどんなロジックで変わるか、など 調査依頼 … 原因がわからない不具合の調査依頼など そして、Slackワークフローを使って、割り込みタスクをエンジニアに伝達。 Slackワークフローでは、手順書を読んだかどうかとNotionのURL、依頼内容を簡潔に伝えてもらうように。 また、CSメンバーはアルバイト等タイミーでの業務にまだ精通していない方々もいるため、 依頼方法について共通の認識を持ち、依頼を一定の水準にするための手順書も用意 しています。 依頼の際は、必ずこれを見てもらい、依頼をしてもらうようにしています。 データ変更依頼、調査依頼、仕様確認の3パターンそれぞれに対応するNotionテンプレートを用意しており、エンジニアが対応する際に必要なプロパティの入力 チェックボックス や背景や期限の理由などの記入欄が自動生成されるようになっています。 これにより、情報の抜け漏れ防止やコミュニケーション往復回数の削減、また情報の標準化に繋がっています。 報告した依頼内容は、要望を報告する #product_エンジニア依頼 チャンネルに流れてきて、必要な場合は担当するエンジニアがスレッドでコミュニケーションをするようになっています。 エンジニ アメンバー は、Daily Standupで優先度をつけ着手 こうして、Notionには割り込みタスクがチケットごとに溜まっていきます。 割り込みチケットの一覧。各ページ内に、割り込みタスクが入れられています。 エンジニアチームで、毎朝やっているDaily Standupで対応する割り込みタスクを決定し、優先度も併せてつけて対応を進めていきます。 外部の顧客に価値提供するタスクや納期が差し迫っているタスクは優先度を高くしており、社内運用向けであったり、期日の交渉が可能なものは優先度を低くして、過度に開発のスケジュールを圧迫しないように努めています。 「priority」のプロパティで、1~5の5段階で数字を割り振り、5が最も優先度の高いタスクとして扱えるようにしています。 依頼内容について、もっと細かく把握したい場合は、Slackのスレッドでやりとりをしていきます。 実際のやりとりの例 まとめると、 割り込み可能なタスクの数を「チケット数」として決める 依頼の際は割り込み可能な数の中で、CSメンバーがNotionに起票し、Slackで報告 エンジニ アメンバー が優先度を決めて、適宜コミュニケーション というフローになっています。 こうして、CSメンバーはより割り込みタスクの依頼もしやすく、エンジニ アメンバー も突発的なタスクに対応することがなくなりました (エンジニアの方々には多いかもしれませんが、突発的なタスクはストレスになることも多いのでなるべく避けたい...)。 職種をまたいだ依頼フローをもっと整えたい 普段行っている業務や、所属しているチームが異なると、どうしてもチームや職種をまたいで依頼することが億劫になったり、そもそも依頼方法が分からなかったりします。 ここまでの事例紹介でも扱った通り、依頼プロセスの入り口を日常業務で多用するSlackのワークフローに設置したことで、スイッチングコストを抑えながらシームレスに依頼手続きを開始したり、依頼方法が分からなくてもワークフローを起動すれば文言に従って情報を入力するだけで手続きが済むようになりました。 現在では、さらにNotionのプロパティを CSV でエクスポートし、過去数ヶ月でどういった種類の依頼がなされているかの 定量 的な分析も行っています。 頻度や削減できる 工数 などを総合的に判断し、必要あればエンジニアがリソースを投下して機能追加をしています。 ストック情報を活用して根本的に割り込み依頼を発生させないようなアクションを取るための意思決定材料にもなっています。 今のフローも手作業な部分もあり、改善の余地があるのですが、社内の利用者の皆さんにより素早く価値が届けられるようなフローに改良していこうと思います。
アバター
こんにちは、タイミーでプロダクトマネージャを務めている高石 ( @tktktks10 ) です。 戦略やロードマップの策定から、プロダクトの成果を最大化するための課題発見や優先順位付けを日々の業務としていますが、今回はその中でも顧客と直接顔を合わせる「ユーザーインタビュー」を起点とした取り組みの話をしようと思います。 ユーザーインタビューの積み重ねから組織のアライメントを生み出す タイミーでは最近新たに入社頂いたPMMの影響もあり、ユーザーインタビューの頻度を大幅に増やしています。きっかけは単に顧客解像度を上げようという至ってシンプルなものでしたが、横断的に継続する中で次第に部署や役職を超えた共通の顧客像 *1 (セグメント)が出来上がり、最近では全社戦略やプロダクトロードマップ、個別の施策にも引用されるまでになってきました。 一言で言えば、「 横断的なユーザーインタビューの積み重ねから組織として顧客像を定義し、戦略や施策に活用している話 」となります。 役割にかかわらず、同じ顧客像を向くことで価値提供の前提が揃います 組織全体で共通理解のある顧客像は、全社戦略からチームレベルの施策全てにおける主語となり、自ずと各所の取り組みが同じ方向を向く、所謂アライメントが取れている状態を生み出します。アライメントが取れている状態ではあらゆる場面でコミュニケーションコストが削減される他、 マーケティング 、プロダクト開発、カスタマーサクセスまでなど、 一気通貫 した課題解決に繋がります。 ユーザーインタビューやペルソナ、個別具体に関する記事は多いものの、実際に構築から運用までを紹介した実例を見かける機会は少ないように思い、今回はタイミーにおける実際のプ ラク ティスをまとめました。 *1 厳密には顧客とユーザーの双方が存在しますが、ここではまとめ上げられたもの呼称として"顧客像"(セグメント)に統一することとします 『組織として』顧客を理解し続ける意味 組織がスケールして部署やチームの数も増えると、それぞれのミッションや取り組みも新たに生まれるでしょう。この過程において全員の向き先を揃える顧客像が不足していれば、皆が少しずつ異なる顧客像を持ち始め、異なる方向に向かい出してしまいます。顧客像や注力対象がズレた状態では、連携する際のコミュニケーションコストが増大する他、部署やチーム間で時には対立を生んだりなどの問題が発生します。優れた組織設計の元ではこれらの摩擦が起きづらいものですが、それでも少なからず分業や連携というものは発生するのが現実です。 このようなサイロ化が進む状況で、いくらサイロの中にいるチームや個人から顧客像を打ち立てても、中々根本の問題は解消しません。 組織がスケールする中で個人やチーム同士を繋ぎ止めて速度を落とさないためには、役割や役職を跨ぐ形で組織として顧客を理解し、戦略や各部のアクションに還元し続ける必要があります。 以下、実際に横断的なインタビューの積み重ねから全社共通の顧客セグメントを構築しているサイクルと、得られた顧客セグメントを戦略や施策に還元したことで得られた成果の順で紹介したいと思います。なお、タイミーはBtoCのサービスですが、今回の取り組みは toC 側を対象にしたものです。 実際に行っているインタビューのサイクル 継続的な取り組みですのでサイクルという表現になり、 下記1~3のプロセスを日常的に回しています 。全体を通して、具体から抽象を見出すことを意識した設計になっています。 1. 共通理解を築きたい人とインタビューを行う 主にインタビュアー2人のデプスインタビューを継続的に行っています。1人が主な進行を担当し、もう1人が視点をフォローするようなサポートをします。現在ミニマムで週5回ですので、毎日1回はどこかしらのペアがインタビューを行なっているくらいの頻度になります。 この時、 共通理解を築きたい人(裏を返せば、理解に溝がある人)を同席相手に選定します 。プロダクトマネージャである自分は、戦略の議論が必要な場合経営陣や関連部署のメンバー、 バックログ レベルで解像度を上げたい場合開発チームと同席することが多いですし、 マーケティング 部では既存メンバーが新しく入社したばかりのメンバーとオンボーディングの一環として同席しています。実際の顧客を目の前にして時間を共有することは、内輪の会議からは得がたい強力な共通理解を築く方法です。 また、インタビュー自体にも様々なテクニックはありますが、基本的には 事実を聞くように徹します 。後述する共通理解を築く上で、主観ではなく事実を把握しておくことは非常に重要です。個人的には下記の アンチパターン 集 *2*3 などは参考になりました。 *2 良いユーザーインタビュー、悪いユーザーインタビュー|Mizuho Kushida|note *3 失敗から学んだ、ユーザーインタビュー23の心得|Goodpatch Blog グッドパッチブログ 2. インタビューで得られた事実について考察をし、ユーザーの特徴を簡潔にまとめる Tips: 「利用を始めたきっかけ」「現在の使い方」「今後の利用見込み」の3点を具体性を持って書くことが多いです インタビューが終わったらその直後、遅くとも記憶が新しい翌日までに考察の振り返りを行います。インタビューで得られた事実を基に、ユーザーの利用開始文脈や現状解決している課題、解決しきれていない課題などに対して協力して解釈(厳密には仮説)を当てます。 当然異なる2人が解釈を当てるので、双方自分には無かった観点が飛び交います。この過程ではお互いの観点を共有して見方を増やし解釈の精度を上げることにフォーカスしますが、ユーザーを起点にしてお互いの考え方を知る時間でもあります。 考察が一通り終わった後は、ユーザーの特徴を3~4行で簡潔にまとめあげます。このまとめが思いの外重要であり、2人で協力して特徴をサマライズすることで「この人はこういう目的でタイミーを使っていたよね」といった、ユーザーに対する共通理解が生まれると同時に、後からこの文書をみたときにユーザーの顔やインタビュー内の会話をスッと思い出せる入り口にもなります。 3. インタビュー結果を集合させ、顧客像を定義・アップデートする 個々のインタビューとその考察で得られた情報を、 集合知 にするプロセスです。 最初こそまとめ方は模索しつつ皆でインタビュー結果をホワイトボードに貼り出して考察を重ねたりしていましたが、総インタビュー数が30~40回を超え始めたところで利用目的、即ち解決している課題が大きく異なる顧客像が2つ浮かび上がり、皆が腑に落ちる形でセグメントとして 言語化 ・定義されました *4 。結果こそシンプルですが、過程は決して事務的な作業ではなく、殆どが会話によるワークショップ的な時間を通して出来上がったものになります。 ユーザーストーリー マッピング や結果の共有会を通じてジョブ別に分類 利用目的が異なるというのがポイントで、これは解決しているジョブが異なることを意味します。一般的にセグメントといえば職業や年齢などから成るデ モグラ ベースの物が多いかもしれませんが、今回定義されたのは解決している課題別、即ちジョブベースのセグメントであり、プロダクトの4階層を借りるならばWhyに相当するもの *5 です。 なお、一度セグメントが定義された後はここに立ち返る形で新たなインタビュー結果を照合し、継続的に内容や理解をアップデートし続けています(現在では開始から4ヶ月で計100~150人程になりました)。この2つのセグメントが組織内の共通言語になっており、後述する戦略や施策などの前提として利用されることになります。 *4 本記事では取り上げていませんが、実際はある程度 定量 データで裏付けをとりながら定義を進めています *5 何のためにペルソナをつくるのか - 4つの使い分け|小城久美子 / ozyozyo|note 得られた顧客像の活用例とその成果 ここまでの過程を プロダクトに関わる全ての役割・役職が協力することで、個人でもなくチームでもなく『組織として』顧客像を作り上げている のがポイントです。自分たちで作っているので都度背景を説明する必要もなく、以降の活用フェーズにおいて既知の共通言語として利用することができます。 全社戦略やロードマップにおける注力セグメントとして利用されることで、部署やチームを 一気通貫 した課題解決に繋がる 以前からも全社戦略やロードマップは運用されていたものの、顧客像の認識は今より曖昧であり、部署やチームの施策レベルで目的の分散が起きてしまう課題が起こりつつありました。今思い返すと、データを元にした会話は多かったものの、インタビューなど顧客のWhyに触れる定性的な時間を共にする機会は少なかったように感じます。 既に共通言語になっている顧客像を戦略の骨子に利用する 一方、最近戦略やロードマップがアップデートされたタイミングで今回作り上げた顧客像を引用したことに加え、事業の方向性やプロダクトビジョンと照らし合わせた上で注力するセグメントと目指すべき方向性も自ずと決定されました。これは、中長期的・組織的な目線において「 誰をどんな状態にしたいのか 」の方針が立ったことに相当します。重ねての強調になりますが、そもそも組織的に作り上げた顧客像であるため、この時点で既に関係者間で共通理解がある状態です。したがって、後からドキュメントによる追加の補足や翻訳作業などもほとんど必要ありません。 結果的に、獲得施策からプロダクト開発、マーケットインまで共通の顧客像を持つことで、以前よりも正しい顧客に正しいプロダクトを届けるための連携ができるようになってきました。部署やチームの前に顧客像とフォーカスが決まっていることで、組織全体を活かした課題解決に繋がっています。 部署間の前提が揃うことで、プロダクト開発の優先順位付けが効率化した こちらはもう少し具体に近い、日々の バックログ に対する優先順位付けの話です。 共通の顧客像とフォーカスができる前は、そもそも向いている顧客像が若干ズレていて視点が広がりすぎているために、ステークホルダからの要望に対して都度目線の調整したり断ることが多かったように思います。もちろん個別の施策に対してコストの軽いものから随時実行するという判断もできなくはないですが、フォーカスの定まらない表層の改善だけが連続してしまうことになります。本質的な課題を解決し中長期的な成長を目指すには、同一のゴールに対して継続的に改善を試みる 選択と集中 が必要であるため、そういった状況になることは好ましくありません。 一方、共通の顧客像とフォーカスが決まってからは自ずと施策レベルの方向性も揃い始めたため、目線の調整や断るケースがかなり減りました。また、プロダクト開発における直近の注力テーマをステークホルダに理解してもらうことで、その間は開発を必要としないプロダクト外で並走できる改善に取り組んでもらうなど、むしろ協力する機会も増えてきたように思います。根っこの目線が揃うことでコミュニケーションコストが削減されるのはもちろん、以前より効率的な分業もできるようになりました。 開発時に引用され、ものづくりにおける意思決定の促進に繋がる 前述した優先順位付けにもつながりますが、顧客像とそのフォーカスが決まっていることで、 バックログ 自体も自然と流れを持った構成になります。また、これはまだ一部ですが、 バックログ リファインメントで開発チームが顧客セグメント引用することで、自律的に機能開発の方向性を決定できるケースも出てきました。ただし、UI/UXレベルの意思決定を精度良く高く行うには、もう一段階具体に寄ったWhatレベルの顧客像が必要であるとも感じます。この辺りは最近取り組み始めたUXリサーチやプロトタイピングを通じて、組織としての仮説検証能力を上げていきたいポイントでもあります。 今回の取り組みを始める際にやってよかったこと 最初はとにかく量を重ねること 正直なところ、インタビュー開始当初は目的設計も曖昧でした。しかし今思い返すと、 顧客解像度が低く目的設計が曖昧になるためインタビューが起こりづらいが、インタビューが起こらないため顧客解像度が上がらない負のスパイラル に陥っていたように思います。 一方、ひとたび顧客の解像度が上がりはじめると自ずと新たな疑問(仮説)が生まれます。その疑問を解消するために次のインタビューを設計して…、というポジティブフィードバックサイクルが生まれれば、後は勝手に自走するでしょう。まずは目的が曖昧でも良いので、量を重ねてみることをおすすめします。 インタビューの参加コストを抑える仕組みを同時に作ったこと こちらは以前出した記事の内容になるのですが、タイミーでは思い立てば誰でもインタビューに参加できる社内の仕組みが構築されています。実際インタビューを行うにはインタビュイーの選定から実際の募集、謝礼の受け渡し、当日までの連絡など、様々な附帯業務が発生する訳ですが、これらをインタビューを支えるチームがサポートすることで、参加者はインタビューのコンテンツだけに集中できるようになっています。 前述したポジティブフィードバックサイクルに乗るためにも、インタビューを行う人とインタビューの設定をサポートする人を分離したことは、今回の取り組みに一役買ったと感じています。 おわりに 最初は何気なく始めたユーザーインタビューでしたが、今では拡大し続ける組織において顧客の共通理解を保ち続ける重要なプロセスとなりました。 プロダクトマネジメント においても、共通の顧客像が主語になることで、組織でものづくりをする力がついてきているように感じます。 とはいえ、タイミーは急成長中のプロダクトで課題もまだまだ転がっています。もっと具体的に話を聞いてみたい方、ユーザーインタビューに限らずプロダクト開発や プロダクトマネジメント について興味がある方、もちろんタイミー自体に興味のある方、職種問わずお気軽にお話しましょう!( Twitter からDMを頂けると助かります)
アバター
前編(トランザクション範囲の最小化)へ はじめに こんにちは。タイミーのバックエンドエンジニア中野です。 前編では締めの バッチ処理 における トランザクション の範囲を最小化した技術的改善をご紹介しました。 トランザクション の範囲を バッチ処理 全体から最小限の範囲に変更したことにより、 バッチ処理 が失敗した場合に請求レコードの処理が途中まで完了している状態が発生するようになりました。後編では、処理対象の請求レコードに対し状態を持たせることで バッチ処理 全体での冪等性を担保し、 バッチ処理 が途中で失敗した場合でも安全に処理を再開できるようにした取り組みをご紹介します。 はじめに 締めのバッチ処理とは 現状の課題認識 実施した施策 冪等性とは 冪等性を実現する方法 バッチ処理への適用 達成できたこと 今後の課題 スループット向上とリソース最適化 まとめ 締めの バッチ処理 とは まずは前編のおさらいになりますが、弊社の締めの バッチ処理 に関して説明します。締めの バッチ処理 とは、月初に定期的に実行されるオンラインバッチであり企業への先月の請求を確定させる処理になります。 具体的な締め処理までのプロセスを記述すると以下の通りになります。 企業への請求が発生した段階で「請求テーブル」にレコードが作成されます 毎月月初の「締め」の バッチ処理 により、「請求テーブル」のレコードを「確定請求テーブル」に複製することで企業への請求を確定させていきます つまり、締めの バッチ処理 とは月初時点の請求テーブルのスナップショットを撮る行為であるとみなすことができます。 現状の課題認識 前編の技術的改善により トランザクション の範囲を バッチ処理 全体から最小限の範囲(レコード単位)に変更しました。これまでは バッチ処理 の途中で処理が失敗した場合には トランザクション の ロールバック により次の2つの状態が担保されてきました。 確定請求レコードが全て作成される状態 トランザクション が成功した場合に遷移する状態。「請求テーブル」のレコードがすべて処理され「確定請求テーブル」に過不足なくレコードが作成されている。この状態に遷移した時に確定請求テーブルに依存している利用明細画面の表示が一斉に更新される。 確定請求レコードが1つも作成されていない状態 バッチ処理 開始前または処理が失敗したに遷移する状態。 バッチ処理 が途中で失敗した場合には トランザクション の ロールバック により処理開始前の状態のままとなる。確定請求テーブルに依存している利用明細画面の表示は バッチ処理 実行前の表示のままとなる。 トランザクション をレコード単位に変更することで、 バッチ処理 全体での原子性が担保されなくなる状況が発生しました。下図右側の失敗時の通り、途中までレコードが作成されている状態が存在するようになりました。 トランザクション を最小化したことによる影響 途中まで「確定請求テーブル」のレコードが作成される状態が出現したことにより生じうる課題は下記の通りです。 処理再開時に重複して確定請求レコードが作成される問題 これまでは バッチ処理 が失敗した際に、 トランザクション の ロールバック 機構により バッチ処理 開始前の状態に戻るため失敗の原因を排除後に再度最初から バッチ処理 を再開することできました。しかし、 バッチ処理 全体での トランザクション を廃止し、途中まで「確定請求レコード」が作成された状態が発生したことで、再度処理を実行すると重複した確定請求レコードが作成される可能性がでてきました。 利用明細への影響 企業管理画面では、翌月の請求予定額及び月ごとの確定した請求額を閲覧する機能を提供しています。この翌月の請求予定額が「請求テーブル」に依存しており、締め処理がされていない請求テーブルのレコード合計を表示するようになっています。一方で、月ごとの確定した請求額は「確定請求テーブル」に依存しています。 バッチ処理 全体において トランザクション が適用されていた場合は、 トランザクション がコミットされたタイミングで処理結果がデータベースに反映されるため、 バッチ処理 完了時に利用明細の表示が一斉に更新されてきました。しかし、 バッチ処理 全体の トランザクション を廃止し逐次的に処理結果をデータベースに反映するようになったため、企業が利用明細を開いたタイミングによって翌月の請求予定額が変動する問題が発生しました。 締め処理と利用明細画面のテーブルへの依存関係 このように処理が途中で失敗するケースは バッチ処理 だけに限った話ではなく、アプリケーションの開発・運用していく中で様々な原因に起因し発生します。例えば、プログラムのバグ、データ欠損や異常値、メモリ不足などが挙げられます。そのため処理が失敗した場合に備えてアプリケーション設計を考えていく必要があります。 実施した施策 上記の課題を解決すべく、締めの バッチ処理 を再設計するにあたり冪等性を意識しました。 冪等性とは 冪等性とは「ある操作を1度実行しても、複数回実行しても同じ結果になる」性質のことを言います。冪等性が担保されていると、仮に処理が途中で失敗したときに処理をリトライしても最終的な結果が1回で処理が成功した場合と変わらないことになります。今回扱う締めの バッチ処理 では、リトライ時に処理済みの請求データに基づく確定請求レコードが重複して作成される可能性があります。そのため、 バッチ処理 に冪等性を持たせることが非常に重要となってきます。 バッチ処理 において冪等性を実現する方法を次に2点紹介します。 冪等性を実現する方法 すでに処理が完了した対象をリトライ時に処理しない方法 バッチ処理 の対象に未処理もしくは処理済みの状態をもたさせることで、リトライ時に処理済みの対象をスキップさせる方法があります。リトライ時に未処理のみを対象に処理を開始できるため、 バッチ処理 全体の完了時間を早められる反面、状態を管理する必要があるため条件分岐のロジックが入るなどコードの複雑さが増す傾向にあります。 すでに処理が完了した対象も含めてリトライする方法 1度目の バッチ処理 によって処理された対象も含めて処理する方法です。例えば、処理した結果をデータベースに永続化している場合はすでに存在しているレコードを削除し新規でレコードを作成し直す処理を1つの トランザクション 内で実行します。そうすることで重複したレコードは作成されず冪等性が担保されます。留意すべき点としては1つひとつの処理対象レコード自体が冪等性を有しており外部サービスに依存していないことです。1と比較し分岐処理によるコードの複雑性は回避できる反面、リトライ時に最初から処理の実行を再開する必要があるため完了時間が遅延する傾向にあります。 バッチ処理 への適用 今回は1の方法で冪等性を実現しました。具体的には、請求レコードに「締め前」「締め中」「締め後」の状態を持たせることで締め処理済みかどうかの判定を行うことができ、たとえ途中で バッチ処理 が失敗した場合でもリトライ する際に締め処理済みのレコードをスキップすれば確定請求レコードの重複作成を回避できます。また、請求テーブルに状態を持つことで「締め後」の状態に遷移した際に依存先の利用明細画面の表示を切り替えるだけで、締め処理が途中で失敗した場合の締め途中の中途半端な状態の金額が閲覧される問題を回避できるようになりました。 達成できたこと 締めの バッチ処理 に冪等性を考慮した設計改善を行うことで次の成果が得られました。 処理が失敗した場合にリトライしても重複した請求が発生することを防止できる 請求レコードに状態を持たせることで バッチ処理 の処理状態に依存している利用明細画面の表示を制御できるようになった 副次的な効果として バッチ処理 全体の完了時間の短期化を実現できました。 バッチ処理 が失敗した際に、処理が失敗した原因を排除後に単にリトライすれば処理を再開できるようになり、結果的に バッチ処理 完了までの時間の短縮化につながりました。 今後の課題 最後に、これから改善を考えている課題を提示した上で バッチ処理 の改善に関する記事の「締め」とさせていただきたいと思います。 スループット 向上とリソース最適化 バッチ処理 は通常処理負荷が高いためサーバーのCPUやメモリなどのリソース消費が高くなる傾向になります。無限にリソースが利用できれば良いですが、通常はコスト制約が存在するため利用できるリソースにも制約が生まれます。そこで処理時間とリソースの制約条件に基づき バッチ処理 のパフォーマンスをチューニングする必要があります。よくある バッチ処理 の事例として、以下の処理を考えます。 データベース からデータを読み取る アプリケーションのインメモリでデータを加工処理する データベースへ加工処理したデータを挿入する 上記の場合に、データベースのレコード1件ごとにRead/Writeクエリを走らせると、大量のデータ量を扱う場合に処理時間の致命的な遅延を招きます。そのため通常はバッチ数を設定し一定件数ごとに処理を行います。バッチ数を大きく設定した場合は一度にインメモリで処理できる件数が増加するためデータベースとのIOを削減でき処理の スループット が向上しますが、メモリのリソース消費が大きくなります。 バッチ処理 の処理時間とメモリのリソース制約を鑑み最適なバッチ数を設定していく必要があります。 まとめ バッチ処理 の一連の技術的な改善において以下の学びを得ました。 一般的な システム開発 において トランザクション の範囲は最小限に限定する必要がある。今回の事例のようにレコードに広範囲のロックがとられる可能性があり、オンライン処理へ影響を与える。 バッチ処理 全体における トランザクション を廃止した結果、 バッチ処理 全体の原子性が担保されなくなった。原子性が担保されなくなったことにより、依存先の利用明細画面の表示に一貫性のない状態が発生した。そこで、処理対象レコードに処理済みの状態を持たせることで バッチ処理 全体の冪等性を担保できるようになった。結果的に、処理失敗時のリトライが容易になり異常状態からの回復が迅速に行えるようになった。 今回は バッチ処理 における トランザクション の改善及び冪等性の設計に関してご紹介しましたが、 バッチ処理 にはここで紹介しきれなかった様々な論点があります。 僕たちの バッチ処理 改善の戦いはこれからだ(完)連載打ち切り tech.timee.co.jp
アバター
後編(冪等性の設計導入)へ はじめに こんにちは。タイミーのバックエンドエンジニアの中野です。 よく Gopher くんに似てると言われます。 本記事では月次で実行している「締め」の バッチ処理 に関する一連の技術的改善について掲載します。弊社のプロダクト「タイミー」は著しい事業成長に伴いデータ量が急増してきています。そこで今回はデータ量の急増を背景とした中長期的な バッチ処理 の設計改善にどのように取り組んできたのかをご紹介したいと思います。 バッチ処理 に関する技術的改善の記事は前編・後編の2部構成をとっています。前編は バッチ処理 における トランザクション の改善をテーマに、後編では バッチ処理 に冪等性の設計を導入したことをご紹介したいと思います。 今回は前編の トランザクション の改善をテーマにご紹介します。すでに本番稼働しているアプリケーションにおいて トランザクション の範囲が大きい場合にどのような問題が発生したのか、そしてどう解決していったのかを中心に取り上げます。 読み手としては、今後データ量の急激な増加が見込まれるプロダクト開発の中長期的な設計・運用を模索している方を想定しています。また読み手に該当しない場合でも、現在タイミーがどのような事業を推進しているのか興味を持ってもらえるように記載しましたので是非最後までお付き合いください。 はじめに バッチ処理について バッチ処理改善のきっかけ 締めのバッチ処理とは 弊社と私のチームが扱っている領域 「締め」の概念 現状の課題認識 実施した施策 トランザクションA トランザクションB トランザクションC 達成できたこと まとめ バッチ処理 について ある程度の量のデータを一括で処理することを「 バッチ処理 」と呼び、通常は日時、週次、月次で定期的に実行されます。オンライン処理 *1 を稼働もしくは停止させたまま バッチ処理 を実行するかによって、 バッチ処理 は次の2種類に分けられます。 オンラインバッチ オンライン処理を継続して稼働させた状態で実行する バッチ処理 です。オンライン処理と同じリソース(例えばデータベース)へアクセスが発生する場合はオンライン処理へ影響を与える可能性があります。 オフラインバッチ オンライン処理を停止させた状態で実行する バッチ処理 です。例えば、夜間にメンテナンス期間を設けることでシステムのダウンタイムを許容し実行する バッチ処理 が考えられます。 今回説明する締めの バッチ処理 ではオンラインバッチを前提としています。 バッチ処理 改善のきっかけ 弊社では毎月初に締めの バッチ処理 を実行しています。具体的な処理内容の説明は以降のパートに譲りますが、月次で定期的に実行されるオンラインバッチを想定してください。約2年前の設計当初は問題なく稼働していましたが、プロダクトが成長するにつれてバッチの処理時間が長期化してきました。処理時間が長期化するにつれて、これまで見えてこなかった問題が起きるようになりました。その問題とはオンライン処理におけるLockWaitTimeoutエラーの発生です。実は改善前の バッチ処理 では処理全体で トランザクション を貼り実行していたため、 バッチ処理 で作成されるレコードまたは外部キー制約において値が登録されている親テーブルのレコードにおいて占有・共有ロックが行われていました。レコードにロックがとられている場合、 トランザクション がコミットされるまでの間に、他の処理からロックされているレコードに対しての読み書きに処理待ちが発生する場合があります。 バッチ処理 のロックがとられる時間が長期化した結果、オンライン処理でLockWaitTimeoutエラーが発生しユーザーに500エラーが返る事態が発生しました。 この問題をどのようなプロセスを経て改善していったのか、まず弊社の締めの バッチ処理 に関する説明をした上でご紹介します。 締めの バッチ処理 とは 弊社と私のチームが扱っている領域 締めの バッチ処理 を弊社でどう改善してきたのか説明する前に、事前知識として弊社及び「締め」の概念に関して簡単にご紹介します。 弊社で開発しているプロダクト「タイミー」は「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービスです。私のチームでは図1グレー部分の企業・クライアントに対し求人を円滑に掲載するための機能を主に開発しています。 用語説明 クライアント :タイミー上に求人を掲載する主体。クライアントの管理画面ではワーカー管理や出退勤管理などの機能を提供している。 企業 :クライアントがタイミーを利用した際に発生する請求を管理する主体。法人単位であることが多く、企業は複数のクライアントを持つ。企業管理画面では請求書や利用明細閲覧の機能を提供している。 各チームが扱っている領域の説明 「締め」の概念 タイミーで働いたワーカーの給料はタイミーが立替払いを行なっています。そのため毎月月初の特定の時点で先月分の給料およびタイミーのサービス利用料を確定させ企業に請求を行っています。この毎月月初で請求を確定させる行為を「締め」と呼びます。 ここではより詳細に「締め」でどのような処理を行なっているのかを説明します。ワーカーの稼働により報酬が確定した段階で「請求テーブル」にレコードを作成し管理しています。毎月月初の「締め」の バッチ処理 により、「請求テーブル」のレコードを「確定請求テーブル」に複製することで企業への請求を確定させていきます。簡単に説明すると、締めの バッチ処理 は月初時点の請求テーブルのスナップショットを撮る行為であると捉えてください。 また企業の利用明細画面では翌月の請求予定額及び月ごとに確定した請求額を閲覧する機能を提供しています。翌月の請求予定額は締め処理が行われていない「請求テーブル」のレコードを参照しており、確定した月の請求額は「確定請求テーブル」を参照しています。 請求関連テーブルと利用明細画面との依存関係 現状の課題認識 締めの処理対象である請求テーブルの特徴として、タイミーの事業成長に伴い急激にデータ量が増えてきたことが挙げられます。締めの バッチ処理 が実装された約2年前から比較すると月次で処理すべきデータ量が20倍近くになっていることが分かります。 *2 請求テーブルのデータ量推移 2年前の バッチ処理 設計当初はデータ量が少なかったこともあり、 バッチ処理 全体に トランザクション を適用していました。利用明細画面の表示が締め処理により作成される確定請求テーブルに依存していたため(図「請求関連テーブルと利用明細画面との依存関係」を参照)、 トランザクション の性質を利用することにより次の2つの状態を担保していました。 確定請求レコードが全て作成される状態 トランザクション が成功した場合に遷移する状態。 この状態に遷移した時に翌月の請求予定額及び締めにより確定した月の請求金額の利用明細画面の表示が一斉に更新される 確定請求レコードが1つも作成されていない状態 処理開始前または バッチ処理 が失敗した場合に遷移する状態。 バッチ処理 が途中で失敗した場合に トランザクション の ロールバック により処理開始前の状態に戻ります。翌月の請求予定額及び締めにより確定した月の請求額の利用明細画面の表示は バッチ処理 実行前のままとなる。 このように トランザクション における原子性の特性を利用することで、 バッチ処理 が失敗した場合でも処理途中までの確定請求レコードが中途半端に作成される状態は存在しません。そのため、確定請求テーブルに依存している利用明細画面の表示上の金額も上の2つの状態しか取りえないことになります。 これまで バッチ処理 全体における トランザクション 貼ることで上記のような恩恵を受けてきましたが、次のような課題も出てきました。 バッチ処理 の影響によるオンライン処理のエラー発生 タイミーは現在モノリシックなサービスのため、オンラインバッチで処理を実行するとオンライン処理と同じデータベースに対し トランザクション 処理を行うことになります。 トランザクション は、他の トランザクション 処理の影響を受けないようにするためレコードまたはテーブルに対してロックをとることがあります。レコードに対してロックが取られた場合に他の トランザクション からのレコードに対する更新系の処理は停止し待たされる状況が発生します。一定時間ロック状態が継続しロックが解放されない場合にLockWaitTimeoutエラーが発生し処理が失敗します。このLockWaitTimeoutエラーが発生すると、ユーザーが意図した処理が実行されないばかりか、エラー発生までユーザーは処理が待たされていることを認知できないため意図しない誤操作を行いかねません。 バッチ処理 失敗時の処理完了時間の遅延 バッチ処理 を運用していると様々な要因により処理が異常終了します。例えば、データ量の増加に伴いコンテナのメモリ量が不足し処理が失敗する問題や請求テーブルのデータに異常なデータが混在しているなどです。処理が失敗した場合に トランザクション の ロールバック により締め開始前の状態に戻るため、処理が失敗した原因を特定後に再度締めの開始から処理を再開する必要がありました。結果的に締めが完了までの時間が長期化し企業への請求確定メールが遅延することも課題としてありました。 実施した施策 トランザクション の範囲を バッチ処理 全体から最小限の範囲に限定しました。 では、最小限の範囲とはどう評価すべきでしょうか。 トランザクション はアプリケーションから見て一貫した状態から次の一貫した状態へ遷移させる作用とみなすことができます。ここで言及した最小限の範囲とは、一貫した状態から次の一貫した状態へと遷移させる トランザクション のうち最小の処理を持つ トランザクション の適用範囲を指します。 言葉で定義するとわかりにくいため、次の事例を考えます。 請求レコードに対応する確定請求レコードの作成 確定請求レコードを作成が完了した場合に処理済みの請求レコードを管理するための処理済みフラグを更新 上記1, 2の処理を締め対象月の請求レコードに対し逐次的に処理していきます。 トランザクション の最小限の範囲を考えるにあたり次の3つの トランザクション (A, B, C)を例に考えます。 トランザクション A まず初めに状態aから状態bへの トランザクション による遷移を考えます。これは今まで締めの バッチ処理 で扱ってきたパターンです。a, bの状態はアプリケーション上許容される状態のため一貫した状態であると言えそうです。ただし、締め処理全体の処理であるため最小の処理を持つ トランザクション とは必ずしも言えなそうです。 状態a: 締め対象月の確定請求テーブルのレコードが1つも作成されていない状態 状態b: 締め対象月の確定請求テーブルのレコードが全て作成され請求レコードも全て処理済みに更新されている状態 トランザクション Aによる状態遷移 トランザクション B 次に、状態cから状態dへの トランザクション による遷移を考えます。c, dの状態は確定請求レコードの作成と請求レコードの処理済みフラグの更新が両方とも実行されており、アプリケーション上許容される状態です。そのためc, dは共に一貫した状態であると言えそうです。さらに最小の処理を持つ トランザクション は存在するのでしょうか。 状態c: ある請求レコードに対応する確定請求レコードが作成されていない、かつ請求レコードが処理済みに更新されていない状態 状態d: ある請求レコードに対応した確定請求レコードが作成され、かつ請求レコードが処理済みに更新された状態 トランザクション Bによる状態遷移 トランザクション C 最後に状態eから状態fへの トランザクション による遷移を考えます。これが トランザクション による最小の処理と言えそうです。しかし、前提として確定請求レコードの作成と処理済みのフラグ更新は両方とも処理が成功するか、もしくは両方とも処理が失敗すべきかの状態が担保される必要があり、確定請求レコードの作成のみが処理されている状態fはアプリケーション上許容されません。これは一貫した状態への遷移とは言えなそうです。 状態e: 請求レコードに対応する確定請求テーブルのレコードが作成されていない状態 状態f: 請求レコードに対応する確定請求テーブルのレコードが作成されるも請求レコードの処理済みのフラグが更新されていない状態 トランザクション Cによる状態遷移 上の3つのケースから一貫した状態から次の一貫した状態への最小限の処理を持つ トランザクション 処理は、 トランザクション Bとなります。このようにして、締めの バッチ処理 に対して最小限の トランザクション 範囲を限定し変更していきました。 達成できたこと これまで1度の締めの バッチ処理 に対してLockWaitTimeoutエラーを20 ~ 30件近く観測していましたが、 トランザクション の範囲を最小限に限定することで0件にまで抑制することができました。しかし、 バッチ処理 全体における トランザクション を廃止したため バッチ処理 全体における原子性が担保されなくなりました。原子性が担保されなくなったことにより、請求テーブル及び確定請求テーブルに依存している利用明細画面の表示に問題が発生しました。どのような問題が発生し、どう解決していったのか後編で詳細をまとめていますので、是非後編もご覧ください。 まとめ データ量の増加が見込まれるシステムでは、それに限らず一般的な システム開発 において トランザクション の範囲は最小限に限定する必要があります。今回の事例のようにレコードに広範囲のロックがとられる可能性があり処理の遅延を招くことがあります。 後編(冪等性の設計導入)へ *1 : バッチ処理 とは対照的にユーザーからのリク エス トに応じて逐次処理を実行し即時的にレスポンスを返す処理をオンライン処理と呼びます。 *2 : 縦軸は バッチ処理 が導入された当初(2020/4)の月次データ量を1としたときの倍率を表しています
アバター
この記事はタイミーのPMM(Product Marketing Manager)のishinabeが担当します。 PMM??と思った方もいるかもしれないので軽くどんなミッションを持っているのかを説明しておくと、デプス調査や定量分析などなどを絡めて顧客課題やジョブの発見から、その深さ・ボリュームの推定、リリースする機能のマーケットイン(機能に価値を感じてもらえる顧客に機能を認知してもらい使ってもらうこと)あたりを主な任務としてIssue度が高いものから解決しようと動いています。 また、今回の記事のテーマのように、デプス含めた顧客理解を組織にインストールすることも重要なミッションとして捉えています。 ※前提、世の中的にもまだ役割がかっちりと定まっている訳ではないので、私も関連チームと会話しつつ模索しながら動いている段階です(この記事の話はどちらかというと世に言うUXリサーチャーっぽい動きかも知れない)。 さて、本題の「ユーザーインタビュー参加コストを極小化する仕組み」お話しです。 なお、以降「ユーザー」→「ワーカー」と表記します。タイミーのサービスのユーザーは、お仕事内容を掲載して働く人を募集する「クライアント」と、お仕事に申し込んで働く「ワーカー」に大別されるためです。 何をやったか やりたいという意思表示をしたらあとは当日参加するだけ 裏側で処理されていること インタビュー参加のサポート なぜやったか 取り組みへのフィードバックなど 今後の展望 最後に自己紹介とおさそい 何をやったか やりたいという意思表示をしたらあとは当日参加するだけ 今の状態から話すと、ワーカーインタビューが 毎日、各1時間ずつ枠が確保されている ので、「やりたいという意思表示をGoogleカレンダーで示す」だけでインタビューに参加できるようにしました。具体的には以下のキャプチャの通りです。 「 IaaS(Interview as a Service) 」ですね、はい。 裏側で処理されていること ワーカーインタビュー用のカレンダーを作る ワーカーインタビューを定期的に開催する アドホックだと文化にならない インタビュー実施の数営業日前に、私が参加者の有無をカレンダーにて確認 希望のインタビュイー像があるかを参加者に確認(なければおまかせで募集) 基本的にはタイミーのサービス上でお仕事としてインタビュイーを募集 マッチングしたワーカーさんとインタビューURLなどのやり取りを実施 インタビュー用のNotionを作成し、マッチングしたワーカーさんの基本情報・お仕事のログを記載した上で参加者へ共有 謝礼に関する社内申請・精算の実施 インタビュー参加のサポート 参加者にインタビュアーとしての希望の参加度合いを確認し、それに叶うようにPMM/PdMが進行をサポート(つまりファシリテートはPMM/PdMがやるので、前準備や知識は必要なし) インタビューTipsをNotionにまとめ、初心者でもざっと心得を把握できるようにしている なぜやったか 状況としては2つあったと認識してます。 プロダクトチーム(PdM&エンジニア)として、以下の課題を感じていた 企画〜実施までの工程が重く、現実的にそれを実現するのは大変(に感じていた) インタビュー目的を設定して項目を作って インタビュイーを募集してやりとりして 社内の承認や費申請などを通して 当日の準備をやって 終わったら報酬のお支払いなどのやり取りをして などなど・・・ 目的を明確に設定していないとインタビューが起こりづらく、ちょっと聞きたいだけだったり、プロダクトゴール外の普遍的な顧客理解が進みにくい 私が、ジョインして以降(勝手に)インタビューをしまくっていた 昨年12月にPMMとしてジョインして以降、加入当初はまず全体感を把握するために幅広にインタビューしながら、明確にミッションを持ってからはそのIssueを深掘りするために特定セグメントのインタビューを重ねていました(数えたら営業日以上の回数インタビューしていた) フリーダムなムーブをしていた自分を、嗅覚のするどいPdMの  たかし (@tktktks10) | Twitter  がめざとく見つけて「折角だからうまく合流して仕組み化できれば良くない?」という話になりました(すごく簡略化しましたが、PdM&PMMとして、組織がユーザーの課題やジョブに深く向き合いながらサービス改善していく文化を作りたいという信念があり、その足がかりとしたい意図も合致して特に引っかかりもなくやろうとなりました) 取り組みへのフィードバックなど PdMの @tktktks10 からのお言葉 「 インタビューの企画設定コストを極限までas a serviceとして削ぎ落としたことで、開発チームが本質的なコンテンツに集中しながら顧客と話せるようになったのが尊い 」 他チームからも問い合わせが いわゆるセールスや渉外の役割を持ったチームからもワーカーインタビューをしたいという問い合わせが来ました。浸透させていきたい...。 CEOの小川も参加しました。大事。 記事を書いている途中に最近入社したBIチーム・マーケチームのメンバーも参加してくれました 参加人数(のべ) 正確にいつから始めたか調べるのが面倒なのでざっくりですが、2月下旬くらいからプロダクトチームに声をかけて、のべ20回超(5月11日時点)はプロダクトチームの人に参加してもらいました(※UUではありません。参加回数にまだまだばらつきはあります) 今後の展望 今後として、「仕組み化」という文脈ではプロセス上にまだ自分が挟まってしまっているので各工程で自動化できる余地を探りつつ、プロダクトチームが顧客の声を直接聞くという営みの価値の高さは実感できたので、より組織に浸透させる後押しをしていきたいなと思っています。 別途、インタビューをどう開発プロセスや組織の意思決定にフィードバックしたかは別記事で触れられればと思います。 また、実際のサービスやプロトタイプのユーザビリティを「定期的に」観察できる場も用意できると良いかなと考えています(Issue/Jobの探索はこの記事の取り組みで一定できるようになっているので、Solutionへの解像度を高める取り組みも浸透させたいの意)。 というわけで、顧客と向き合うの最高ですよ、まずは騙されたと思って5回やってみてください!踏み出したとして1回や2回そこらでやめない方がいいです。 そしてできればタイミーで一緒にやりましょう。 最後に自己紹介とおさそい 前職ではPdM(Product Manager)をやっていましたが、タイミーでPMMロールにチャレンジすることになりました。役割は冒頭に書いた部分を中心に据えて動いていますが、まだまだ模索中、かつ、流動的な状況です。 部署的な話でいうと組織図上はマーケティング部の中にいますが、気持ち的にはプロダクト半分、マーケ半分で、スクラムの各イベントに参加したり、こうしてTech Blogに寄稿したりしています。 昨年12月にジョインして約半年になりましたが、事業としての成長スピードもえげつなく、やれることが溢れるようにあると感じています。が、とはいえPMMは今のところ私一人なので拾いきれてないものも多く、ご一緒する方を探し求めています。 まだPMMというロールの浸透度が薄く、選考フローに乗ってくる絶対数がほぼないという現状なので、 少しでも興味があればぜひお話しできればと思ってます (意見交換目的でもぜひ...)。 product-recruit.timee.co.jp また、少なくともこの4月までプロモーションマーケの方にも関わっており、そちらの雰囲気を知りたいマーケ志望の方。もしくは、PMM視点でプロダクトチームの雰囲気を知りたいエンジニアの方など、幅広にお声がけお待ちしております。 それでは。
アバター
はじめに こんにちは、フロントエンドエンジニアの樫福 @cashfooooou です。 タイミーでは toB 向け管理画面を作成しています。 半年ほど前、タイミーでは顧客からのサービスへの要望を集め、管理するシステム(以下、要望回収システム)を作りました。 顧客の課題から新しい機能について考え、顧客により価値のあるものを届けるための施策です。 システムの実装には Notion という SaaS を活用しました。 最小限の実装で良い機能・良い運用が作れたと思っています。 この記事では、要望回収システムの実装に取り組んだ経緯から、実際の運用の例まで紹介します。 同じように顧客の要望回収を行いたい方はもちろん、 SaaS を使ったミニマルな機能開発の参考になれば幸いです。 はじめに Notion 従来の回収システム 課題 解決したい課題 Notion API を用いた課題解決 制約 データベースの構成 回収システムの Notion の構築 運用 まとめ Notion Notion は、タスク管理や Wiki の作成に適した Web アプリケーションです。 ユーザの手によってカスタマイズしやすく UI も使いやすいので非常に便利です。 www.notion.so 弊社ではエンジニアだけでなく、社全体のドキュメント管理の大部分で Notion を用いています。 Notion 好きの社員も多く、 最近は Notion の活用術について CTO の kameike が インタビュー を受けたりしました。 従来の回収システム 従来の回収システムは Google フォーム と Google スプレッドシート を組み合わせたものを利用していました。 課題 従来の回収システムは顧客から寄せられた要望を閲覧できますが、次のような複雑な操作が難しいです。 似た要望同士を紐付けて管理する 対応の優先順位を決める プロジェクトやタスクと要望を紐付ける 結果として、 要望が集まれば集まるほど確認作業の手間が増える というジレンマに陥り、運用が十分に回らなくなってしまいました。 解決したい課題 従来のシステムの課題をもとに、次の方針を立てました。 要望に対して操作をしやすく 似た要望の紐付け 進捗の管理、優先度をつけられる 要望からタスクに繋げやすく なるべく実装が少なく 我々のチームは普段 Notion を用いてタスク管理しているので、それとうまく連携が取れると運用が楽になって嬉しいです。 加えて、要望回収システムは弊社のサービスのコア ドメイン との直接的な関係が薄いのでなるべく手間をかけずに実装したいと考えていました。 Notion API を用いた課題解決 この議論をしていたのは2021年7月ごろでしたが、その2ヶ月ほど前に Notion API がパブリックベータとして公開されました。 developers.notion.com パブリックベータとはいえ、レコードの閲覧や作成ができるのでいろんなことができます。 寄せられた要望を Notion 上でを管理できると、Notion を用いた既存のタスク管理との連携も容易になりました。 また、似た要望をまとめるような操作も Notion のリレーションなどを使って実現できます。 そして何より、これらのロジックもUIも、そのほとんどを Notion の機能を用いて実現できるので実装の手間がとても小さくて済みます。 今回実装した要望回収システムは、 Notion API を用いて次のような構成になりました。 要望システムの構成 顧客は弊社のサービス画面の「プロダクトへの要望フォーム」から要望を投稿します。 投稿された要望は API サーバから Notion API を経由して Notion のデータベース に蓄積されます。 要望が投稿されると Slack に通知が届くので、エンジニアは Notion の Web ページから顧客の要望を確認することができます。 このシステムを導入する上で実装すべきことは 顧客の投稿フォームの実装 Notion API を用いた Notion の DataBase への投稿機能 Slack への投稿 のみです。 Notion の機能のおかげで下のような恩恵が受けられました。 DB のカラムの変更などが、エンジニアの 工数 を割かなくても手軽にできる データの操作のための UI や API の設計・実装が不要 Notion API の登場によって、抱えてた問題の全てが解決しました。 制約 こちらのページ でNotion API には 平均3リク エス ト/秒のレートリミットについて触れられています。 将来的に料金プランに応じてレートリミットが変わる予定もあるようです。 Notion API を利用する際にはこのあたりに注意することをおすすめします。 弊社のサービスに寄せられる要望は多くても一日10件程度のなので、レートリミットの問題は無いと判断しました。 データベースの構成 システムを実装するにあたって、顧客から寄せられる要望の分析をしました。 寄せられた要望を見ると、「〇〇の条件下で△△してほしい」や「✗✗機能を追加してほしい」のような具体的な解決案が送られてくることが多いです。 これらの具体的な解決策に直接対応していっても、プロダクトが抱えている課題を本質的に解決するのは難しいことが多いです。 そこで、似た要望をまとめて抽象化し、プロダクトの課題として扱うのがよさそうだと考えました。 以上のことを踏まえて、要望の管理のために次のようなテーブルを作成することにしました。 *1 図中の矢印は Notion のテーブルのリレーションを表しています。 データベースの構成 「要望」は顧客から寄せられたメッセージそのものです。 「課題」は要望の根底にあると考えられるプロダクトの課題です。 要望と課題はそれぞれ進捗度合いを表すステータスを持っていて、進行度合いを確認できます。 タグは類似の要望や課題を整理するのに役立ります。 また、要望・課題・backlog はそれぞれリレーションを持っていて、情報の管理に役に立ちます。 回収システムの Notion の構築 こちらのページ に、上の図のテーブルを再現してみました。 寄せられた要望、そこから深ぼった課題、実際にエンジニアが着手するタスクの例を埋めています。 (簡単のために、 twitter のような架空の SNS に対して寄せられた要望という設定です。) 要望データベース 「要望」テーブルの一番右の列は、要望と紐付いている「課題」テーブルのアイテムが表示されています。 同様に、「課題」テーブルには紐付いている「backlog」テーブルのアイテムが表示されています。 更に、Notion でテーブルの各レコードが Notion Page になっているのも大きな利点です。 要望・課題について議論をしたときに直接 議事録を書き込むことができるので、後で確認するときに情報が漏れる心配がないです。 議事録の作成には Database templates が便利です。 ページ内の課題のレコードを開くと議事録のサンプルも確認できるようになっています。 Database Template を使った議事録の作成 実際に Notion を触っていただくとよくわかると思いますが、データの更新、ソートやフィルタ、他のテーブルとのリレーションなど、実際に実装するとかなりの 工数 のかかりそうな機能が手軽に使えるのは本当に嬉しいです。 運用 エンジニアや PM が定期的に要望テーブルを確認してタグを割り当てたり課題・タスクの作成・紐付けを行っています。 課題の作成・紐付けも Notion のデータベースのフィルタやソート機能を使って、過去に作成した同様の課題と比較しながら作業ができるので無駄が少ないです。 テーブルのレコードがそれぞれが Notion Page となっていることの恩恵を強く感じます。 Database templates を用いた議事録やテーブルの管理がかなり楽で助かります。 まとめ プロダクトの要望回収システムを Notion API を用いて実装しました。 Notion の使い勝手がよいので実装の手間がかなり小さく、よい選択ができたと思っています。 今回のような社内向けの機能の開発において Notion API は、最小限の実装のコストで十分な機能が実現できる Notion のポテンシャルを見せつけられました。 加えて、すでに社内の情報管理のために Notion を導入しているのであれば、連携することでさらなるうまみが得られるチャンスです。 コア ドメイン ではない機能の開発や新機能の仮説検証など、あまり 工数 をかけずにミニマルな実装をしたいときには Notion をはじめとした SaaS を活用してみてはいかがでしょうか。 Notion 最高!一番好きな SaaS です! *1 : 実際に運用しているテーブルからいくつかのプロパティを落としています。
アバター
はじめに はじめまして、 Android エンジニアのmurata( @orerus )です。 アイラ系 ウイスキー を愛していますが、肝臓が弱まってきた為最近は専ら0.5% ハイボール を愛飲しています。 本記事では、タイミーのモバイル アプリ開発 におけるSLO(サービスレベル目標)を設けているメトリクスのちょっとした改善事例について紹介します。 SLOとは何かといった話やタイミーで運用しているSLOについては こちらの記事 にて詳しく紹介していますので是非ご覧ください! 本記事の概略 タイミーのワーカーチームでは、モバイル アプリ開発 における指標の一つであるクラッシュフリーレートをSLI(サービスレベル指標)としてSLOの運用を行っています。 しかし、長く運用する中で、SLO運用に期待されている「当たり前品質と攻めたリリースのバランスを取る」「当たり前品質の低下をいち早く検知する」「適切なタイミングでプロダクト品質への改善圧をかける」といった役割が果たされていないと感じられるケースが度々発生していました。 そこで、モバイル アプリ開発 メンバーが普段観測しているクラッシュフリーレートのメトリクスを改善することにより問題を解消した事例について紹介します。 目次 はじめに 本記事の概略 目次 現状と課題 行った改善策 結果変わったこと 最後に APPENDIX 現状と課題 タイミーではSLOを運用しているメトリクスが日に1度slackに投稿されるようになっています。 実際にタイミーで運用している、とある日の改善前のクラッシュフリーレートのメトリクスがこちらです。 表1. 過去30日間、7日間におけるクラッシュフリーレート ① : 過去30日間、7日間におけるクラッシュフリーレート ② : プラットフォーム/アプリバージョン単位でのメトリクス詳細 ※今回は触れません 表の左から プラットフォーム ( iOS or Android ) アプリバージョン クラッシュに遭遇しなかったユーザー数 クラッシュに遭遇したユーザー数 クラッシュフリーレート こちらのメトリクスにおいて、ワーカーチームでは以下のSLOを策定していました。 iOS : クラッシュフリーレート 99.95% 以上 Android OS: クラッシュフリーレート 99.60% 以上 ここで先程のメトリクスを見てみると、 Android 側はすでに過去30日間におけるクラッシュフリーレートにおいてSLO違反を起こしてしまっており、プロダクト品質への改善圧をかけるべき状態となっていることが分かります。 しかしこの時、 Android 開発メンバーの間では「直近で品質改善の施策を行ったので既に改善されているはずだ」という認識を持っていました。 このような事例の他、幾つかの事例からこのメトリクスによるSLO運用には以下の課題があることが分かりました。 メトリクス単体で指標値の変化の傾向が掴めず、問題発生の検知が遅れ早期の対応が行えない SLO違反を引き起こしている要因をメトリクスから把握することが難しい 品質改善などの施策の結果がメトリクスに反映されるまで時間がかかり、かつ反映されるまでの時間の予測も立ちづらく、施策実施後もSLO違反による改善圧がしばらくかかり続けてしまう これらの課題を眺めていると、いずれも過去30日間/7日間のクラッシュフリーレートという施策の実施から結果が反映されるまでに時間のかかる指標のみに頼ってSLOを運用している為に発生しているものである、という予測が立ちました。 行った改善策 先述の課題解決の為、以下を目的としたメトリクス改善を行うことにしました。 指標値の変化の傾向をメトリクスから掴めるようになる 指標値が大きく変化したタイミングを的確に把握できる その結果、追加したメトリクスがこちらです。(こちらのメトリクスは表1と同日に計測したもの) 表2-1. クラッシュフリーレートSMA 表2-2. クラッシュフリーレートSMAグラフのみ ① : プラットフォーム単位での過去60日間におけるメトリクス クラッシュフリー率 : 1日毎のプラットフォーム単位でのクラッシュフリーレート 7日間SMA : 各日付を含む過去7日間の 移動平均線 (Simple Moving Average) 30日間SMA : 各日付を含む過去30日間の 移動平均線 ② : プラットフォーム/日付単位でのメトリクス詳細 ※今回は触れません 表の左から 日付 クラッシュに遭遇しなかったユーザー数 クラッシュに遭遇したユーザー数 クラッシュフリーレート 7日間SMA 30日間SMA ※上記メトリクスの算出方法については文末のAPPENDIXにて紹介します この表2のメトリクス追加により、表1のメトリクスと組み合わせて以下のことが分かります。 Android 表1の「30Daysクラッシュフリーレート」においてSLO違反を起こしているが、表2の「7日間SMA」から過去1〜2週の間に改善施策が行われていて改善傾向にある よって今後のメトリクスに置いても「30Daysクラッシュフリーレート」が改善されることが予測される iOS 「30Daysクラッシュフリーレート」がSLO違反スレスレの値ではあるが、こちらも「7日間SMA」「30日間SMA」を見ると上昇傾向にある為、直ちに対策を取る必要性は無さそう 上記の例はポジティブな結果のみのサンプルではありますが、ネガティブなケースでも同様に傾向を把握することができそうです。 結果変わったこと 今回のメトリクス改善を行い、実体験として変化があったと思うものを以下に挙げます。 SLO違反の予兆や改善の傾向が把握できるようになった 施策の結果を客観的な指標値で速やかに把握することができるようになった 過去のメトリクスを見返す必要がなくなった メトリクスに対する 心理的 変化が発生した 1 〜 3 の結果、当初のSLO運用の目的である「当たり前品質と攻めたリリースのバランス」の判断をより精度高く判断することができるようになった為、この改善はやってよかったと感じています。 また、1 〜 3 の変化については当初の目的通りですが、4については思わぬ変化でした。以前のメトリクスでは点でしか指標値を見ることができず、結果をSLO違反しているかしていないかのゼロイチでしか受け取ることができていませんでした。それが今では線で指標値を見ることができるようになり、プロダクトの当たり前品質の機微な変化を捉えやすく、またメトリクスを施策の結果を速やかにフィードバックしてくれるパートナーとして認識できるようになったため、自然と前向きにメトリクスを確認しに行くことができるようになりました。 最後に 前項にも触れた通り、SLOおよびメトリクスは改善圧をかけてくる敵ではなく、プロダクトの当たり前品質という健康状態を維持する手助けをしてくれるパートナーです。是非味方につけて、プロダクトの体験改善の為の攻めたリリースと当たり前品質の維持を効率よく両立していきましょう! また、現在はモバイル アプリ開発 においては今回取り上げたクラッシュフリーレートというアプリのクラッシュにまつわる指標でしかSLOを運用できていませんが、クラッシュには至らずとも機能が正しく動いていないケース *1 、機能は動いているがユーザーが満足に使えていないケース *2 など、プロダクトの体験改善の為に注視していくべき観点はたくさんあります。今後も体験改善に繋げることのできる指標を測定可能にし、攻めたリリースを行うためのSLO運用を行っていく所存です。 その為に脳みそを貸してくれる仲間、また美味しい ウイスキー 銘柄を教えてくれる仲間を絶賛募集中です 😆 APPENDIX 表2の①のグラフを作成する為に使用している SQL を記載します。 こちらで抽出した結果を Google Data Studioを用いてグラフ化しています。 なお、こちらの SQL はFirebase CrashlyticsのデータをBigQueryに転送している環境( 参考URL )であればこのまま利用可能です。 ※ スキーマ 名は各自の環境に合わせて修正ください -- TemporaryTable1. BigQueryからクラッシュ情報を抽出 WITH userCrashes AS ( SELECT event_date, user_pseudo_id, platform, MAX ( event_name = ' app_exception ' ) hasCrash, MAX ( event_name = ' app_exception ' AND ( select value.int_value = 1 from unnest(event_params) where key = ' fatal ' ) ) hasFatalCrash FROM `analytics_17541xxxx.events_*` -- 各環境のスキーマ名に修正してください WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE( " %Y%m%d " , DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 60 DAY)) -- 過去60日間 AND FORMAT_DATE( " %Y%m%d " , DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 1 DAY)) AND platform = " ANDROID " -- 測定したいプラットフォーム IOS or ANDROID GROUP BY event_date, user_pseudo_id, platform ), -- TemporaryTable2. TemporaryTable1から日付単位でクラッシュ数やクラッシュフリーレートを集計 userCrashesCount AS ( SELECT event_date, platform, IF (hasCrash, ' crashed ' , ' crash-free ' ) crashState, IF (hasFatalCrash, ' crashed fatal ' , ' crash-free ' ) fatalCrashState, COUNT ( DISTINCT user_pseudo_id) AS crashFreeUsers, ( SELECT COUNT ( DISTINCT user_pseudo_id) FROM userCrashes AS uc2 WHERE uc2.event_date = userCrashes.event_date AND uc2.platform = userCrashes.platform ) - COUNT ( DISTINCT user_pseudo_id) AS crashUsers, ROUND ( COUNT ( DISTINCT user_pseudo_id) / ( SELECT COUNT ( DISTINCT user_pseudo_id) FROM userCrashes AS uc2 WHERE uc2.event_date = userCrashes.event_date AND uc2.platform = userCrashes.platform ), 4 ) AS crashFreeUserShare, ( SELECT COUNT ( DISTINCT user_pseudo_id) FROM userCrashes AS uc2 WHERE uc2.event_date = userCrashes.event_date AND uc2.platform = userCrashes.platform ) AS users FROM userCrashes -- TemporaryTable1 WHERE hasCrash = false GROUP BY event_date, platform, crashState, fatalCrashState ORDER BY event_date ) -- 抽出結果. Table2から30日間SMA、7日間SMAを算出 SELECT platform, -- プラットフォーム event_date, -- 日付 crashFreeUsers, -- クラッシュに遭遇しなかったユーザー数 crashUsers, -- クラッシュに遭遇したユーザー数 crashFreeUserShare, -- クラッシュフリーレート CASE WHEN 7 = COUNT (crashFreeUserShare) OVER ( ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) THEN AVG (crashFreeUserShare) OVER ( ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) END sevenDaysSMA, -- 7日間SMA、過去7日分の数値が取れない日付の分は空にする CASE WHEN 30 = COUNT (crashFreeUserShare) OVER ( ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) THEN AVG (crashFreeUserShare) OVER ( ROWS BETWEEN 29 PRECEDING AND CURRENT ROW ) END thirtyDaysSMA -- 30日間SMA、過去30日分の数値が取れない日付の分は空にする FROM userCrashesCount -- TemporaryTable2 ORDER BY event_date なお、上記 SQL を構築するにあたり、 こちらのStackOverFlowの回答 を参考にさせていただきました。 *1 : API の4xxエラー等の監視は別途行っています! *2 : API の422エラー(バリデーションエラー)の発生件数を監視することで、分かりやすいUI/UXを実現できているかを確認しています
アバター
はじめに はじめまして、 Android エンジニアのsyam( @arus4869 )です。 普段は愛知県からフルリモートで勤務していますが、最近は褪せ人として荒野を駆けています。 本記事の概略 本記事では、タイミーの Android プロジェクトで挑戦しているモジュール分割の取り組みについて紹介します。 また内容の理解を促すため、マルチモジュールについても軽くおさらいします。 本記事では、以下の方を読者として想定しています。 他社のモジュール分割の手法に興味がある方 マルチモジュールなプロジェクトに挑戦してみたい方 はじめに 本記事の概略 現状のタイミーとアプリについて タイミーのアプリ開発におけるチーム構成 課題 モジュール分割とは 「マルチモジュール」なプロジェクトの例 分割のステップ モジュール分割のメリット モジュール分割のデメリット 現在のアーキテクチャ analytics pubsub component style core repository repository-impl Feature Moduleによる機能分割について FeatureModuleにおける画面遷移について考えていること おわりに 現状のタイミーとアプリについて タイミーの Android アプリでは、レイヤー単位でのモジュール分割はある程度行っていますが、機能単位でのモジュール(以降、Feature Moduleと呼びます)分割が行われていないのが現状です。 現在タイミーの開発組織は「マッチング領域」と「スポットワークシステム領域」という2つの領域でわかれています。今回は私が所属している「マッチング領域」の話です。 マッチング領域ではLeSSを採用しており、POと2つの開発チームが存在します。また、そのチームの枠を超えて技術的な課題を横断的に解決するため技術領域ごとにCommunityが存在します。現状マッチング領域の Android エンジニアは全員 Android Communityに所属しており、Community内で話し合った方針に基づきアプリケーションの アーキテクチャ の改修や リファクタリング を行っています。その中の一つが Android アプリのモジュール分割です。これを行うことで将来発生する開発人数・コード規模の増加などに伴って発生する様々な問題に対処しやすくなると考えています。 LeSSについて詳しく知りたい方はこちらの The LeSS Company B.V が提供している イントロダクション が参考になると思います。 タイミーの アプリ開発 におけるチーム構成 課題 現状抱えている課題は大きく2つあります。 開発組織の拡大に伴い、複数人での並列的な開発により生じる課題 広範囲で強制化されていないコードが存在しているため、コードを触る開発者の人数が増えることによって、 アーキテクチャ の強制が効かなくなる可能性がある コンフリクトの増加 別開発チームのレビューにかかる時間が増加 サービスの拡大に伴いコード量が増加することで生じる課題 ビルド速度の低下 リファクタリング における影響範囲が読みづらくなる コードが読みづらくなる 他にも様々な課題が考えられそうですが、モジュール分割を適切に行うことによって、上記課題の改善がされることを期待しています。 モジュール分割とは タイミーの Android プロジェクトで採用しているモジュール分割の取り組みについてお話しする前にモジュール分割について簡単におさらいします。 Android Studio などの IDE で新規プロジェクトを作成すると、最初にApplication Module(app ディレクト リを持つモジュール)が作成されます。この状態はシングルモジュールなプロジェクトと呼ばれます。 このApplication Moduleを複数のモジュールに分割することが可能であり、モジュール分割されたプロジェクトを「マルチモジュール」なプロジェクトと呼びます。 本記事における「マルチモジュール」なプロジェクトは、1つの「Application Module」と「Application Module」から分割された複数の「Library Module」から構成されます。 また「Library Module」からも複数の「Library Module」を分割することが可能です。 他にも「Dynamic Feature Module」などのモジュールがありますが、本記事では取り扱わないこととします。 モジュール分割の具体的な方法については、Doroid Kaigiの codelabの既存のアプリをマルチモジュール化 するを参照してみると良いと思います。 「マルチモジュール」なプロジェクトの例 モジュール分割をする際は、既存の「Application Module」にあるレイヤーや機能を分割することで、思考の関心事を分離することができます。そのためモジュール分割を実施する前にどう分割するかを計画する必要があります。 またモジュール分割する際の注意点として、モジュール間の依存関係を単一方向に保つ必要があるため、抽象化の範囲を適切に考えておく必要があります。 モジュール分割を行なうために例えば以下のようなステップが考えられます。 分割のステップ コードの共通部品をモジュールに分割する 関心を切り離したいレイヤーを決めて分割の優先順位をつける 優先順位ごとにモジュールを分割する 古いUIのコードを別のパッケージに移す UIのコードを「Feature Module」として機能単位に分割 優先順位の付け方は諸説ありますが、ステップを踏んで適切にモジュール分割をすることによって、様々な恩恵を受けることができます。 モジュール分割のメリット モジュール分割を行なうことで受けられる恩恵のひとつとしてビルド時間の短縮化が挙げられます。 既にビルドが行われ編集されなかったモジュールは、キャッシュ化されます。 そのため、ビルド時には編集されたモジュールだけを コンパイル すれば済むので、ビルド時間の短縮につながることいわれています。(Gradleによるビルドシステムの恩恵が大きいと考えられます) モジュール分割では、ビルド時間の短縮だけでなく、以下のように様々なメリットが考えられます。 コードを触る範囲を狭め、ある程度強制することができる 関心ごとを分離できるため、コードを触る範囲が明確になる 関心ごとを分離できるため、機能の実装に集中できる 関心ごとを分離できるため、新規開発者が参画しやすくなる モジュール分割でコードの抽象化が行われることによる恩恵が大きいと思います。また、先述で挙げた課題についても改善に繋げることができると思います。 他にも様々なメリットがありますが、計画を立てずにモジュール分割を目的にするとモジュール分割に失敗し、逆にデメリットになる場合もあります。 モジュール分割のデメリット モジュール間の依存が増えると、依存間の関係性が複雑になることによりビルド時間が伸びる可能性があります。 そのため、結局ビルド時間の短縮に繋がらなくなるので過度な抽象化には注意が必要です。 モジュール分割による抽象化は正解がないので、規模が大きくなるにつれ、計画を練る難易度が高くなります。 そのため小さくステップを踏んで、少しずつモジュール分割を進めていくのをお勧めします。 現在の アーキテクチャ タイミーの Android アプリでは、1つの「Application Module」(以下、appモジュール)と20の「Library Module」で構成されています。 ここでは全体のモジュールの構成と代表的なモジュールの役割について簡単に紹介します。 全体のモジュールの構成としては、下図の通りです。 基本的にはappモジュール(Application Module)がほとんどのモジュールと依存しています。appモジュールにはレガシーなコードがまだ混在しているので、新旧それぞれのコードを別パッケージに振り分け、新パッケージ内に内包されているUIのコードが分割したモジュールと依存するようになっています。 レガシーなUIのコードはappモジュール内のviewパッケージ 新規に実装するUIのコードはappモジュール内のuiパッケージ analytics 分析基盤のモジュールです。Firebaseの他にAdjustやReproなどのアナリティクスに特化したモジュールです。 pubsub Publishersからメッセージを送り他の画面でSubscribeするモジュールです。 募集画面などでお気に入りしたイベントを他の画面に通知させたりしています。 Rxで仕組みを実現させています。 component 部品として独立したモジュールを内包しています。 imageviewer license pickimage style ActivityやUI コンポーネント 等のstyleがAppThemeとして定義されているモジュールです。 core Andoroid コンポーネント の開発で使用する共通的なプログラム等が内包されているモジュールです。 repository ドメイン モデルをリソースとして返すインターフェースが内包されているモジュールです repository-impl repositoryモジュールのインタフェースを実装した ドメイン モデルの具象を取得するプログラムが内包されているモジュールです。 各モジュールについて簡単に触れさせていただきましたが、また別の機会にて具体的に紹介できたらと思います。 タイミーのモジュール分割の現状ですが、先述の「分割のステップ4」にあたる「古いUIのコードを別のパッケージに移す」に位置しており、「分割のステップ5」の「UIのコードをFeature Moduleとして機能単位に分割する」ことを目指している最中です。 Feature Moduleによる機能分割について 本記事で取り扱う「Feature Module」についてですが、「機能としての画面」をモジュール単位で分割することを指しています。 例えば下図の「検索」や「募集一覧」が「機能としての画面」にあたります。 このような「機能としての画面」を「Feature Module」で機能分割して利用する場合、下図のような構成になります。 このように機能単位で分割することによって、自チームが検索機能を実装する場合、募集機能を実装している別チームに影響を与えずに同時に開発可能な領域を増やすことができます。 機能単位で独立して実装できるので、並列のチームを抱える開発組織において、モジュール分割でのメリットと同時に強力な恩恵を受けることができると考えています。 このような恩恵を受けるため「Feature Module」による機能分割を目指しています。 FeatureModuleにおける画面遷移について考えていること 「Feature Module」による機能分割を成功させるためには、画面導線を秩序立って配置する必要があります。そのため、画面の中に複数機能への遷移があった場合にモジュールを組み合わせしやすい形にしておきたいと考えています。 下図のような画面遷移が例として挙げられます。 しかし、相互依存しないような画面遷移について考えなくてはならないため、どのように「Feature Module」を分割すれば良いか悪戦苦闘しています。 下図は相互依存するような画面遷移の例になります。(実際に下図のような画面遷移はしないのであくまで例です。) Gradle上でモジュールの参照を定義した場合、循環参照になってしまい相互依存してしまうため、単純に「Feature Module」同士での依存は、上図の画面遷移を考慮した場合に実現不可能です。 相互依存しない画面遷移を実現するためには、「画面遷移の抽象化」を考えなければなりません。 現状においては、中間にモジュールを挟むことによって「画面遷移の抽象化」を図りたいと考えており、下図のような構造をイメージして具体の実装を考えています。 「画面遷移の抽象化」についてはまだ道半ばなため、次回以降の記事で取り上げたいと思います。 おわりに 本記事では、タイミーの Android プロジェクトで採用しているモジュール分割の取り組みについて紹介させていただきました。 タイミーは挑戦できる土壌がある会社で、脳みそをたくさん使うことができます! モチベーションの高い仲間たちと様々なことにチャレンジしてみませんか? 是非興味をお持ちの方は気軽にタイミーへ応募してみてください! iOS Communityの @sky_83325 もマルチモジュールについての記事を書いてますので、こちらも興味があれば読んでいただけらと思います! tech.timee.co.jp
アバター
はじめに はじめまして、バックエンドエンジニアのぽこひで ( @pokohide ) です。 最近の日課はゲーム実況者「 兄者弟者 」の「DYING LIGHT 2 STAY HUMAN」と「エルデンリング」を見る事です。 本記事ではタイミーで長年使われていた、マイクロサービスとして切り出されたチャットサーバ(以降、旧チャットサーバと呼びます)をタイミーの中核を担うモノリシックなRuby on Railsサービス(以降、タイミー本体と呼びます)に移行した話です。 今回は移行した経緯、気にした点などを紹介します。 対象にしている読者は以下の方々です。 レガシーなシステムと向き合っている人 無人化システム ※1 に疲弊している人 ※1 : 無人化システムとは この記事 に出てくる造語で「 誰も詳細は知らないが、なぜか動いているシステム 」を意味する はじめに チャット機能とは 旧チャットサーバとは なぜやるのか やること・やらないこと やること やらないこと 移行計画の検討 移行計画まとめ 結果 最後に チャット機能とは タイミーは「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービスです。マッチングすると事業者(クライアント)と働き手(ワーカー)はチャットを通じてやりとりができます。 タイミーリリース当初から提供されており日常的に使われている機能ですが、マッチング後に働くまでの流れといったテンプレートメッセージを事業者が手動で送信していたり、画像等のファイルを送受信不可能だったりと色々な課題も抱えてもいました。 この機能の改善はマッチングから働くまでの無駄な時間の削減に繋がり価値が高いため、弊社ではコアドメインと考えています。 旧チャットサーバとは 旧チャットサーバはGo言語で書かれておりアカウントのAuth、プロフィール情報の保存、メッセージの送受信、既読の有無といった機能を有するサービスです。 以下の図は、アプリ上でメッセージタブを開いた時の流れを簡略化したものです。 旧チャットサーバの持つアカウントはタイミー本体のアカウント情報をハッシュ化したものを利用しているため、タイミー本体は旧チャットサーバのアカウントやルームを取得できるが逆は行えないといった欠点があります。 そのため、タイミー本体からマッチング中の募集を取得してそれに対応するルームを取得するといった流れになっています。この際、GETなのにDBにWriteが走る可能性があったり悲しい事にもなっていました。 以下の図は、ルームを選択してメッセージ投稿までを簡略化したものです。 旧チャットサーバでは受信者へのプッシュ通知やメール送信は行なっていないため、メッセージ登録時にタイミー本体にリクエストを行なっています。 他にもプロフィール更新はタイミー本体を介して旧チャットサーバにリクエストを送るため、同一アカウントに対して複数の経路からAuthが実行される事で認証の奪い合いになるといった事象も発生していました。 なぜやるのか 端的に言うと コアドメインにも関わらず継続的改善ができない状況 だったからです。 上述したものも含めツラミをまとめたのがこちら クライアント、バックエンドどちらからも認証を行うため認証の奪い合いが起こる アカウント情報がハッシュ値のため特定が難しく分析が困難 チャット → マッチングが疎結合で体験的改善が走らない 障害やエラーが発生しても対応が困難 単一機能を提供することを前提とした設計のため追加の開発が困難 色々なツラミを抱えていましたが、今回は次のような目的と制約を決めました。 目的 - チャットを**継続的改善**と**タイミーとのシームレスな連携**が可能な状態にすること - チャット情報を**分析可能な状態**にすること 制約 - 旧チャットサーバに**一切手を加えない** やること・やらないこと 移行の流れを決める前に今回の目的と制約の中でやる事・やらない事を決めました。 やること 将来を見据えた設計で旧チャットサーバの機能を踏襲したチャットの実装 今までと変わらないメッセージ体験の提供 ルームやメッセージなどは同じ機能を持つ 古いバージョンのアプリをサポート やらないこと メッセージの既読情報の同期 旧チャットサーバ側のRDBに保存される過去データの移行 厳密な同期処理の実現 旧チャットサーバはメッセージ作成時にタイミー本体にリクエストを送っていますが、中身はメッセージ本文とアカウント情報のみで既読などの情報は含んでいません。旧チャットサーバに手を加えない制約のため、無理な同期はせずこれはやらないこととしました。 他にも、タイミーでは報酬確定後24時間でルームが閉じます。このため、見えないデータである過去データの移行は行わない事に決めました。 タイミーにとってのチャットは重要な機能ではありますが、SNSやメッセージアプリに比べて高精度な同期性を求められるほど頻繁に連絡が行われているわけではありません。早く届くことよりも確実に届けられることが重要です。そこで、高精度な同期はベストエフォートとしつつ結果整合性を重要視しました。 移行計画の検討 「マイクロサービスとして新規開発する」「メッセージ関連の外部SDKを導入する」といった方法も考えられましたが、タイミー本体とのシームレスな連携をするために チャットをタイミー本体に移行する ことを選択しました。 また、チャットはWebブラウザからもアプリからもアクセスされるサービスです。アプリにはバージョンの概念があるため移行に向けて必ずアップデートの浸透待ちを必要とします。 そこで今回は、タイミー本体で新しくチャットAPI(以降、新チャットサーバと呼ぶ)を実装して、Webブラウザとアプリの特定バージョン以降はタイミー本体を利用させつつ、アプリの推奨・強制アップデートを活用して旧チャットサーバを利用するバージョンを徐々に減らしていく手法を考えました。 その間、新旧チャットサーバを行き来することが考えられるのでデータ同期が必要となります。ここで既存のチャットサーバからのWebhookや解放されているAPIを利用します。 実際には旧チャットサーバのAuthは不安定なところがあるため、新→旧の同期処理は非同期で行われています。正しくデータが同期される事を保証したいのですがリトライ上限に達して非同期ジョブが終了してしまう可能性もあります。それを考慮して上限に達した場合はログを残し、それを検知可能にする事で見つけ次第、泥臭く対応する事にしました。 お気づきの方もいるかもしれませんがこのデータ同期は完璧ではありません。なぜなら、旧チャットサーバはメッセージの登録を契機にWebhookリクエストを行います。そのため、悲しい事に新 → 旧で同期を行うと旧 → 新と全く同じメッセージが返ってきてしまいます。 さらに上述の通り、このWebhookリクエストの中身はメッセージ本文とアカウント情報のみです。同じアカウントが全く同じ内容のメッセージを送信した場合、それらが違う事を識別できません。 そこで新チャットサーバ側で、 5分以内に全く同じ内容のメッセージがある場合は何もしない といった処理を加えました。これにより 新 → 旧 → 新のメッセージ投稿のループに対応できる 同じアカウントから同じ内容のメッセージは識別できないのであえて識別せず、5分以内であれば1つのメッセージとして扱う ようにしました。これは今回の移行に際して諦めたことです。 最終的にはアップデート浸透待ち期間を経て、アプリから旧チャットサーバへのアクセスがなくなったことを確認し、データ同期処理をやめて完全に新チャットサーバのみで稼働させることで移行を完了させるといった計画を立てました。 移行計画まとめ タイミー本体で新しくチャットAPI(新チャットサーバ)を実装する アプリで新チャットサーバを利用する 推奨・強制アップデートを利用して旧チャットサーバから新チャットサーバに徐々に移行する 旧チャットサーバへのリクエストがなくなったことを確認したのち、データ同期処理を止める 以下は余談です。 今思えば、アプリのバイナリ単位で向き先を変えるのではなく、ストラングラーパターン ※2 を用いて段階的に向き先を変えることで制御可能でロールバックも容易になり、より安全な移行ができたなと思いました。 ※2 : 既存のアプリケーションと新しいアプリケーションを振り分ける「ストラングラーファサード」というレイヤーを設けて機能の特定部分を新しいアプリケーションに徐々に置き換えながら移行する手法 https://docs.microsoft.com/ja-jp/azure/architecture/patterns/strangler-fig 結果 以下のような時系列で特に大きな障害なく旧チャットサーバと完全にお別れができました。 2020年12月 2021年3月 2021年4月 ~ 2021年11月 2022年1月 推奨・強制アップデートのフロー構築も同時に準備していたためアップデート浸透待ちに半年以上を要した結果となりました。 タイミーとのシームレスな連携も可能となり「チャットを通じたお問合せ」「複数人のチャット」「システム的なメッセージ」「メディアのやりとり」といった将来を見据えた拡張性の余地も持たせることができました。 また、副次的な効果として全エラーの約3割を占めていた旧チャットサーバとのAuth時の401エラーも0件になりSentryのノイズも減りました。 最後に 今回はコアドメインなのに継続的改善が行えない状況をタイミー本体に移行する事で解決しました。リスクの伴う決断でしたが、継続的な体験改善が行えるようになったのでより使いやすいアプリ開発を頑張っていきます。 技術的負債の解消や継続的な体験改善に興味がある方は是非、声をかけてください! product-recruit.timee.co.jp
アバター