メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

成功循環モデルから学ぶ、チーム力を向上させた取り組み

こんにちは。エンジニアの保立(@purunkaoru) です。

先日、弊社のMVPに、開発チームのリーダーをしている四方さん(@shikatadesu)が選ばれました。

style.medpeer.co.jp

近くで見て、僕が勉強になった点を「成功循環モデル」をもとに紹介いたします。 チームビルディングで悩まれている方の参考や気付きになれば嬉しいです。

成功循環モデルは、「関係の質」、「思考の質」、「行動の質」、「結果の質」という順序で質が向上していく考え方です。各段階の質が高まることで次の段階の質も向上し、良い結果が循環するとされています。詳しくは以下のリンクをご参照ください。

「成功循環モデル」とは、組織として成果や結果を上げ続けるために必要な要素とサイクルを示した組織開発フレームです。組織の成果や結果を継続的に上げるためには、組織に所属する従業員同士の関係性が重要です。 Schoo for Business: 成功循環モデルとは?活用するメリットや方法を解説

実は、僕自身が最近までこの考え方にピンときてなくて、深く考えたり、実践することがありませんでした。特に、メンバー間の関係が良好ならば思考や行動、そして結果の質が自動的に向上するかというと、そうとは限りません。例えば、社内の飲み会やチーム旅行が直接的な成果向上につながるとは感じにくいですよね。また、組織の一体感が思考や行動の質を高めるという考えには納得できるものの、その一体感が具体的にどのような形で結果につながるのか、長らく疑問に思っていました。

四方さんが表彰された際に、チームの成功要因を分析したところ、この成功循環モデルで説明がつきそうだなと思ったので、その成果を開発者ブログに書きました。

以下は、チームの関係の質を向上させて、成功循環を生むために工夫された点です。

1. リーダーシップ領域の明確化

このチームでは、必要な時に必要な人がリーダーシップを発揮できる、シェアド・リーダーシップができるチームビルディングを目指していました。そのために、まずは全員がどこでリーダーシップを発揮するのかを明確にして、そのためにどのような知識を増やし、どのような行動を取るのかを考えたうえで、チーム内の共有を行いました。これにより、特定のリーダーのみにタスクや責任が集中することを防ぐとともに、メンバー同士が相互に信頼できる環境ができました。

2. タスクの質と期限の厳守

各メンバーの得意領域が明確になっても、タスクの質が低かったり、決められた期限が破られると、チーム内の信頼関係が落ちる要因となります。PdMもエンジニアも、自身のタスクへの質の向上や期限を守ることへの意識がとても高く、それが信頼関係を構築できる要因となりました。

3. タスクの背景共有

毎日の朝会では、「こぼれ話」というコーナーを設けて、PdMから業界についての共有やユーザーヒアリングの結果など、PdMがインプットしている情報の共有をしていました。この透明性がエンジニアにもタスクの背景を理解させ、より具体的な要件や分析手法の提案を促しました。

これらの取り組みによって「関係の質」が向上したことで、「行動の質」にも好影響を与えました。 以下に具体的な事例を示します。

  • エンジニアの業界理解の深化:エンジニアが業界に関する知識を深めたことで、要件定義やユーザー分析に関してもPdMと共に積極的に提案できるようになりました。
  • テスト工数と手戻りの削減:チーム全体でテスト工数や手戻りをどう減らすかについて議論しました。その結果、要件定義のドキュメントフォーマットやレビュー観点を見直し、テスト工数を半減することができました。
  • タスクの負担分散:PdMが急なタスクで忙しくなると、エンジニアが積極的に業務を引き受けて進捗遅れを防ぎました。これにより、PdMの負担が軽減され、プロジェクト全体の滞りない進行が保たれました。

今回の事例は、チームが各個人の特性やリーダーシップを活かし、チーム内の「関係の質」を高めた結果、「行動の質」や「結果の質」が上がった良い例でした。朝会で趣味やランチの話をするのもいいですが、それだけの関係にとどめず、行動や結果の質を上げるための関係作りをしているのが印象的でした。

僕はまだ読んでいませんが、このチームで成果を出しているメンバーからは、「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」でも成功循環モデルについて触れられていると伺いました。

リーダーがメンバーのパフォーマンスを十分に発揮させるためには、マネージャーは価値観や性格の異なるメンバーやパフォーマンスが出ていないメンバーに対しても積極的に関係性を構築する必要があることがわかります。
(中略)
高いパフォーマンスを発揮するメンバーに対しては質の高い関係性を構築しやすいですが、パフォーマンスが低いメンバーに対しては冷淡になり、関係の質が低下しやすくなります。その他にも、質の良い関係性が構築されていたとしても、メンバー自身が自分の能力を過小評価していると高いOBSE(Organization-Based Self-Esteem)を獲得することは難しく、挑戦的な行動につながりづらいこともあります。

この本では、成功循環サイクルとメンバーの自己効力感を上げることが必要と書かれているようですね。自己効力感については、社内で行なっている施策があるので、別の機会に紹介したいと思います。

メドピアでは、チームビルディングや開発生産性を、個人の感性や勘に委ねるアートから、誰もが再現できるサイエンスにできるように、成功・失敗事例の振り返りに力を注いでいます。もし興味を持っていただけたら、開発者ブログをブックマークしていただけると嬉しいです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

HTTP API Clientライブラリの自作を手助けするGemを公開しました

こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。 育休明け早々猛暑の熱気にやられ部屋に閉じこもっています。

今回はとあるGemを作成したので、そちらの紹介をさせていただきます。

目次

前フリ

Webアプリケーションを開発されている皆さん。 外部のHTTP APIを呼び出すような要件が発生したらどのように実装されますか?

まずはHTTP Clientライブラリ(Gem)の選定からですよね。 無難なところでfaraday、最近?だとhttpなんかも選択肢にありますし、Gemを利用せずにRuby標準の Net::HTTP を直接使うなんてこともありますよね。

これらのいずれかを採用した後はどのような実装をされますか?

お行儀が良い方は以下のような形で、専用のHTTP API Clientクラスを作成しその中にfaradayなどGemの依存を閉じ込めるのではないでしょうか

class BookStoreClient
  class Error < StandardError; end
  class ClientError < Error; end
  class ServerError < Error; end

  def books(limit:, offset: 0)
    connection = Faraday.new("https://example.com") do |builder|
      builder.response :raise_error
    end

    connection.get("/books")
  rescue Faraday::Error::ClientError
    raise ClientError
  rescue Faraday::Error::ServerError
    raise ServerError
  rescue StandardError
    raise Error
  end
end

こうすることのメリットは主に二つあります。

一つ目は 「faradayなどの外部gemの影響範囲を狭めることで、外部gemのアップデート時の対応が容易になる」です。

これは分かりやすいですね。
上記の例で言うとfaradayに関する定数やメソッドの利用箇所がClient内部に限定されるのでfaradayに非互換な変更が発生した場合はここだけを修正すれば良くなります。

二つ目は「複数サービスのAPIを同一箇所で呼び出す際に、Clientクラス単位で例外が分かれているため捕捉制御がしやすくなる」です。

これに関してもHTTP Clientに限定された話ではありませんが、例外が表す意味が大きくならないように適切に捕捉し別の例外に置き換えるべきです。という話です。

別の例を挙げるとActiveRecordの ActiveRecord::RecordNotFound があります。 これは「"何か" のレコードが見つからなかった」としか例外レベルでは表現されていないため、例外を捕捉する際に「何のレコードが見つからなかったのか」を特定するには例外オブジェクトの中身まで参照する必要があります。 faradayの例外に関しても全て Faraday::Error のサブクラスとして定義されているため同種の問題があります。

このような問題に対処するために「ライブラリが発生する汎用的な例外を適切に捕捉し、意味が限定的になるような個別の例外に置き換える」といった制御を私は良くやります。(皆さんもやりますよね?)

こういった実装は私の好みなので、今までも散々実装してきましたし今後も継続して推進しています。

が、しかし、しかしですよ。

HTTP APIを呼び出す処理なんてもう何回も実装して、その内の大半は同じような「例外の定義」「ステータスコードによる例外制御」をコピペ実装していませんか? 私はしてきました。ツライ

と言うことで本題です。

Gemの概要

上記のようなツラミを解消するGemを作成しました。

github.com

詳細はREADMEに記載しているためそちらをご参照ください。 と言いたいところですが、拙い英語でニュアンスが伝わるかも怪しいのでこの場を借りて母国語で解説したいと思います。

まず、本Gemの利用方法からです。

READMEに記載しているコード片をそのまま転記します。

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com')

  # Class Method
  def self.delete_book(id)
    delete("/books/#{id}")
  end

  # Instance Method
  def books(limit:, offset: 0)
    get('/books', params: { limit: limit, offset: offset })
  end

  private

  def _default_headers
    h = super
    h['X-Request-Id'] = Current.request_id if Current.request_id.present?
    h
  end
end

BookStoreClient.new.post('/books', params: { title: 'Programming Ruby' })
#=> { id: 1, title: 'Programming Ruby' }

BookStoreClient.new.books(limit: 5)
#=> { books: [{ id: 1, title: 'Programming Ruby' }] }

BookStoreClient.delete_book(1)
#=> nil

BookStoreClient.get('/books/1')
#=> BookStoreClient::NotFoundError (status: 404, body: { message: 'Not Found' })

お察しの良い皆さまならこれだけで理解できるかと思います。

Ueki::HttpClient.new('https://example.com') で "良い感じ" のmoduleが作成されるので、これを自作のHTTP API Clientクラスにincludeするだけです。
そうすることで、getやpostなどのリクエストメソッドが定義され、レスポンスステータスに応じた例外ハンドリングも勝手に制御してくれます。

以上です!

あれ? 例外ハンドリングはfaradayの RaiseErrorMiddleware で十分と思ったそこのあなた! Uekiのウリはここからです。

Uekiがraiseする例外クラスは以下の階層構造となっています。

BookStoreClient
└── Error
    ├── RequestError
    │   ├── TimeoutError
    │   └── UnexpectedError
    └── UnsuccessfulResponseError
        ├── BadRequestError
        │   ├── UnauthorizedError
        │   ├── ForbiddenError
        │   ├── NotFoundError
        │   ├── RequestTimeoutError
        │   ├── ConflictError
        │   ├── UnprocessableEntityError
        │   └── TooManyRequestsError
        └── ServerError

こういったツリー構造って美しいですよね。

ですが、重要なのはそこではありません! これらのトップレベルには自作のHTTP API Clientクラスである BookStoreClient があります。

はい、もうお分かりの通りUekiは Faraday::Error のような汎用例外クラスではなく、includeしたクラスそれぞれに例外クラスも自動定義します。(これが欲しかった!)

これほど丁寧に例外ハンドリングをしてくれているので本Clientクラスの呼び出し側は安心して rescue BookStoreClient::NotFoundError のように例外を捕捉することができます。

ここまでをまとめると、Uekiは大きく分けて以下の二つの機能を提供します。

  • 例外クラスの自動定義
    • 400系, 500系などを考慮した捕捉制御のしやすい階層構造の例外
  • シンプルなget,postなどのリクエストメソッドの自動定義
    • ステータスコードに応じた例外ハンドリング機能付き

いかがでしょうか? とても薄い機能ですが痒い所に手が届くような機能提供となっているのではないでしょうか。

カスタマイズ性について

本Gemは元々弊社内の特定プロジェクトに依存した内部ライブラリとして開発していました。 そのため完全にfaradayに依存しており、faradayのミドルウェア設定なんかもビジネス要件に合わせたものを設定していました。 今回GemとしてOSS公開するにあたり、この点のカスタマイズ性について検討しました。

そこで私の中で出た結論としては「HTTPリクエスト処理に関してはUekiの責任でないから勝手に改変して良いと言う姿勢にしよう」です。

本件の詳細についてもREADMEに記載していますが、こちらでも解説します。

Uekiではデフォルトでfaradayの Net::HTTP adapter を利用し、ログ出力も デフォルトで有効にしています

例えば「Keep-Aliveを有効にしたい。そしてログ出力は不要だから抑制したい」のようなニーズがあったとします。 その場合は思い切ってメソッドを上書きして対処しましょう。

faradayのadapterを Faraday::NetHttpPersistent に変更し、好きなfaradayミドルウェアを自由に設定することが可能です。

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com')

  private

  def _initialize_faraday_connection(request_options)
    Faraday.new(url: self.class::ENDPOINT, headers: _default_headers, request: request_options) do |builder|
      builder.adapter :net_http_persistent, pool_size: 5 do |http|
        http.idle_timeout = 100
      end
    end
  end
end

「faradayのどのadapterを利用しどのミドルウェアを適用するか」というのはビジネス要件次第で組み合わせが膨大になります。 設定変更用のDSLを提供することも検討しましたが、中途半端なものになってしまいかねないため思い切って「勝手にメソッドを上書きして」を公式の対処法と明言することとしました。

意味のある細かい粒度でメソッドを分割しているため、比較的メソッドの上書きはしやすいのではないかなと考えています。

ここでもう一つのニーズについて考えてみます。 「ウチのプロジェクトではfaradayは利用していない。Gemに頼らずNet::HTTPでリクエストしたい。それに逐一各クラスでメソッドの上書きをするのも面倒だ。」

ありそうな話ですよね。 これについてもUekiは "そこそこ" 対応しています。

Uekiの姿勢は「自作のRequesterモジュールを作成し、それをUekiで利用するように設定しましょう」です。 以下のようなイメージです。

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com', requester: CustomRequester)
end

「最強のHTTP リクエスト処理」をUekiは目指しているわけではありません。 "皆さん" が思う「最強のHTTP リクエスト処理」を各々が実装すれば良いだけの話です。 Uekiは少しだけそこのお手伝いをします。

CustomRequester の作成方法など丁寧なドキュメントはまとめていませんが、おそらくここを自作したいと動く人は勝手にコードを読んで察するだろうと考えています。

また、Uekiがこの「HTTPリクエスト処理」に対しての想いが強くない表れとし、ueki.gemspecのdependencyには「faraday」を "あえて" 定義していません。 「faraday」の利用に関しても完全にオプショナルであるということです。

以上のように非常に柔軟で大胆なアプローチでUekiのカスタマイズ性を実現しています。

正直なところUekiに標準実装されている DefaultRequester に私が満足しているためここのカスタマイズ性に関してのモチベーションが低いです。 より洗練されたI/Fなど思いつく方いましたらIssueやPullRequestをお待ちしております。

まとめ

というわけで、今回は「HTTP API Clientライブラリの自作を手助けするGem」について紹介させていただきました。 このGemを利用せずとも、こういった考え方もあるのだなと思えていただけたら幸いです。

皆様のHTTP API人生に幸あれ

おまけ

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com')

少し特殊なI/Fですよね。 includeできるのはmoduleだけなのに、 Ueki::HttpClient クラスとは何者なのかと。

これは 認証Gemのshrine なんかでも利用されているModule Builderパターンという形で実装しています。

こちらの連載記事が詳しいので最後の紹介しておきます。 techracho.bpsinc.jp

Uekiのコードも少しは整理されているものとなっているので、Rubyビギナーの方なんかは勉強がてら読んでみると面白いかと思います。

github.com


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Capybaraとreg-cliを使ってお手軽にビジュアルリグレッションテストを行える環境を整備しました📸

こんにちは、MedPeerの開発を担当している森田です。 今回は私が開発に参画しているMedPeerに元々E2Eテストで利用していたCapybaraと、reg-cliを利用してビジュアルリグレッションテスト(以下VRT)を行える環境を整備したので、それについてご紹介させていただきます。

なぜ、VRTを導入するのか?

MedPeerでは元々System Specを活用したE2Eテストを利用してフロントエンドを含めて品質を担保しておりましたが、デザイン崩れの影響を検知するのは難しく、規模の大きい変更を行う際には手動での画面確認を行っておりました。

しかし、手動での画面確認は検証コストも高く、開発上のボトルネックになりがちであったのと、手動での検証は本来であれば検出されてほしい影響を検知できずにリリースされてしまうこともあり、検証精度を担保しつつスピード感を持った開発を実現する上で課題となっておりました。

そんな中で、VRTを既存のCIに組み込み、デザイン崩れを検知できれば、前述の課題の解決に寄与できるのでは?と思ったのが導入の背景です。

VRTの要件と技術選定

上述の通り、検証精度を担保しつつスピード感を持った開発に寄与するためにVRTを導入するにあたって、必要な要件を以下と考えました。

  • コストを掛けずにVRTを記述・組み込めるようにすること
    • 幅広くVRTを記述する必要があるので、記述のハードルをなるべく下げるために既存の仕組みの延長線上で実現する
  • メインブランチへのマージ前にデザイン崩れの発生に気づき修正できること
    • デザインへの影響がPRのstatusで判断でき、CIを落とすことでマージ前に気づいて対応できるようにする

この要件に合わせて、MedPeerではCapybarareg-cliを使って構築することにしました。

github.com

github.com

すでにSystem Specを使って行なっているE2Eテストで利用しているCapybaraの機能であるCapybara::Session#save_screenshotを使って任意のタイミングでスクリーンショットを取得し、ローカルでも実行できるreg-cliを使って取得したスクリーンショットの差分を検知することで、既存の仕組みを活かしコストを掛けずにCIでデザイン崩れをマージ前に検知できるのではないかと考えました。

実際に構築したVRT基盤の概要

構築したVRT基盤の概要が以下の通りです。

VRT基盤の概要フロー図

まず事前作業としてspec/systems/visual_regression/screenshots/masterに正となる現時点でのスクリーンショットを取得するSystem Specを作成し、メインブランチに配置しておきます。

そして実際にPRが作成された際に、PRのブランチにて追加したSystem Specを実行し、CI上のPRのブランチで取得したスクリーンショットをspec/systems/visual_regression/screenshots/compareに配置します。

そして、reg-cliを使って、それらのディレクトリに配置された同一パス・名称のファイルの差分をチェックし、差分があればCIを失敗させるようにしています。

成功時

CI status(成功時)

失敗時

CI status(失敗時)

VRT基盤の具体的な話

System Spec内でスクリーンショットを取得する

System Spec内でCapybaraを使ってVRT用のスクリーンショットを取得できるように以下のHelperを用意しました。

module VrtScreenshotHelper
  VRT_SCREENSHOT_BASE_PATH = 'spec/system/visual_regression/screenshots'

  def vrt_screenshot(page, path:, full: true)
    return unless screenshot_enabled?

    target = update_master? ? 'master' : 'compare'
    base_path = screenshot_base_path(target: target)
    if full
      save_full_size_screenshot(page, base_path.join(path))
    else
      page.save_screenshot(base_path.join(path))
    end
  end

  private

  def save_full_size_screenshot(page, path)
    original_size = Capybara.current_session.driver.browser.manage.window.size
    resize_window_to_fit_page
    page.save_screenshot(path)
    reset_window_size(original_size.width, original_size.height)
  end

  def screenshot_base_path(target:)
    Rails.root.join(VRT_SCREENSHOT_BASE_PATH, target)
  end

  def screenshot_enabled?
    ENV["VRT_SCREENSHOT_ENABLE"] != "false"
  end

  def update_master?
    ENV["VRT_SCREENSHOT_UPDATE_MASTER"] == "true"
  end

  # NOTE: フルサイズのスクリーンショットを取得するためにウィンドウサイズをページに合わせる
  def resize_window_to_fit_page
    width = Capybara.page.execute_script(<<~JS)
      return window.outerWidth
    JS

    height = Capybara.page.execute_script(<<~JS)
      return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
    JS

    reset_window_size(width, height)
  end

  def reset_window_size(width, height)
    Capybara.current_session.driver.browser.manage.window.resize_to(width, height)
  end
end

Helper内に実装しているvrt_screenshotを使うことで利用者側で以下の設定を行いVRT用のスクリーンショットを取得できるようにしています。

  • スクリーンショットを配置するディレクトリ
    • reg-cliで差分チェックを行うディレクトリspec/system/visual_regression/screenshotsを自動設定
    • 正となる画像を更新する場合には自動的にmasterに配置する
  • フルサイズでのスクリーンショットの取得
    • スクリーンショット取得時に画面サイズをフルサイズに変更してからページ全体のスクリーンショットを取得する

このヘルパーを使って利用する側は以下のような形でフルサイズのスクリーンショットを差分チェックするディレクトリ(spec/system/visual_regression/screenshots/compare/service_name/root_page.png or spec/system/visual_regression/screenshots/master/service_name/root_page.png)に自動配置できるようにしました 📸

require 'support/vrt_screenshot_helper'

RSpec.describe 'Service name', :js do
  include VrtScreenshotHelper

  it 'sample vrt' do
    visit root_path
    expect(page).to have_css '.sample-selector' # NOTE: ページが一定表示されるのを待つ
    vrt_screenshot(page, path: "service_name/root_page.png")
  end
end

reg-cliでスクリーンショットの差分をチェックする

reg-cliを使った以下のスクリプトをpackage.jsonに設定し事前作業で取得していた正となる画像とCI(またはローカルでも)上で取得した画像を比較して5%以上の差分があった場合にエラーにするようにしています🕵️

{
  "scripts": {
    "test:vrt": "reg-cli spec/system/visual_regression/screenshots/compare spec/system/visual_regression/screenshots/master spec/system/visual_regression/screenshots/diff -R spec/system/visual_regression/screenshots/diff/report.html -J spec/system/visual_regression/screenshots/diff/reg.json -T 0.05",

実行しているスクリプトの詳細は以下の通りです。指定できるオプションの詳細は公式のREADMEをご確認いただければと思います。

$ yarn run reg-cli \
  spec/system/visual_regression/screenshots/compare \ # チェック対象のスクリーンショットの配置先
  spec/system/visual_regression/screenshots/master \  # 正とするスクリーンショットの配置先
  spec/system/visual_regression/screenshots/diff \    # 差分を表す画像の出力先
  -R spec/system/visual_regression/screenshots/diff/report.html \ # 差分レポートの出力先
  -J spec/system/visual_regression/screenshots/diff/reg.json \    # 差分レポート(JSON)の出力先
  -T 0.05 # 許容する差分の閾値(%)

実際の実行結果は以下のように確認することができ、差分があった際にはexit code 1.となりCIが失敗します🍎

yarn run v1.22.22
$ reg-cli spec/system/visual_regression/screenshots/compare spec/system/visual_regression/screenshots/master spec/system/visual_regression/screenshots/diff -R spec/system/visual_regression/screenshots/diff/report.html -J spec/system/visual_regression/screenshots/diff/reg.json -T 0.05
✔ pass    spec/system/visual_regression/screenshots/compare/service_name/root_page.png
✘ change  spec/system/visual_regression/screenshots/compare/service_name/sub_page.png

✘ 1 file(s) changed.
✔ 1 file(s) passed.

Inspect your code changes, re-run with `-U` to update them. 
error Command failed with exit code 1.

分かりやすいコマンドでVRTを実行できるようにする

前述までの手順にて、CIでSystem Specを実行しスクリーンショットを取得後にreg-cliでの差分チェックのスクリプトを実行すれば、一定VRTとして機能するようになったかと思います😀

しかし、VRT実行のために複数のスクリプトを手動で実行するのは手間に感じたので、以下のようなRakeタスクを用意してbin/rails visual_regression:runでSystem Specによるスクリーンショットの取得、reg-cliによる画像比較を実行するようにしました。

require 'optparse'

namespace :visual_regression do
  desc 'Run visual regression tests'
  task run: :environment do
    options = {}
    option_parser = OptionParser.new do |parser|
      parser.banner = 'Usage: rake visual_regression:run [options]'

      parser.on('-t', '--target TARGET',
                'The directory to run the tests (default: spec/system/visual_regression)') do |v|
        options[:target] = v
      end

      parser.on('-u', '--update', 'Update the master screenshots (default: false)') do |_v|
        options[:update] = true
      end

      parser.on('-h', '--help', 'Show Help') do |v|
        options[:help] = v
        puts option_parser.help
        exit
      end
    end

    # NOTE: OptionParser#order! は optionに存在しない値があるとパースを中断してしまうので、
    # rake taskで利用する場合に指定するコマンド名とオプションのセパレーター`--`を削除する
    # https://docs.ruby-lang.org/ja/latest/class/OptionParser.html#I_PARSE--21
    option_parser.parse(ARGV - ["visual_regression:run", "--"])
    options[:target] ||= 'spec/system/visual_regression'
    options[:update] ||= 'false'
    env = {
      'VRT_SCREENSHOT_ENABLE' => 'true',
      'VRT_SCREENSHOT_UPDATE_MASTER' => options[:update].to_s,
    }

    rspec_success = system(env, 'bin/rspec', options[:target]) # System Specによるスクリーンショットの取得
    raise "Get ScreenShot command failed with exit code #{$CHILD_STATUS.exitstatus}" unless rspec_success
    
    vrt_success = system('yarn', 'run', 'test:vrt') # reg-cliによるスクリーンショットの差分比較
    raise "Check Image diff Command failed with exit code #{$CHILD_STATUS.exitstatus}" unless vrt_success
  end
end

特定のVRTの正となる画像ファイルを更新する際にも以下のコマンドで更新できるようにしました。

$ bin/rails visual_regression:run -- -u -t spec/visual_regression/your_test_spec.rb

CIで差分をチェックする

MedPeerではCircleCIを利用しているので先ほどのRakeタスクを実行し、結果をアーティファクトにアップロードするようなstepを設定することでCI上でVRTを実行するようにしています。

  visual_regression:
    steps:
      - run:
          name: run visual regression
          command: bin/rails visual_regression:run
      - store_artifacts:
          path: spec/system/visual_regression/screenshots

CircleCIのアーティファクトはブラウザ上で閲覧できるため、reg-cliで作成したhtmlレポートを以下のように、そのままブラウザで表示して差分の詳細を確認することができて非常に便利でした 👍

reg-cliによる画像差分のhtmlレポートのサンプル(一覧画面)
reg-cliによる画像差分のhtmlレポートのサンプル(詳細画面)
https://github.com/reg-viz/reg-cli/tree/main?tab=readme-ov-file#html-report

OS間での利用フォントによる違いを吸収する

当時MedPeerではfont-familyの指定が以下のようなユーザーのOSフォントを尊重するようなフォント指定になっておりました。

font-family: system-ui, sans-serif;

開発環境はDebian系のOSイメージを利用していますが、CIではUbuntu系のイメージを使用しており、実行環境によって適用されるフォントが変わってしまうことで、ローカルで事前に取得した正となるスクリーンショットとCIで取得したスクリーンショットを比較する現状の方式では、OSによって適用されるフォントが異なるため差分が発生してしまいました。

これが原因でVRTが失敗してしまうことが多かったので、以下のようにVRT実行時にのみtrueとなるカスタムコンフィグを設定して、

Rails.application.configure do
  # NOTE: VRTのスクリーンショット取得を判別するためのカスタム設定
  screenshot_enable = ENV["VRT_SCREENSHOT_ENABLE"] == "true"
  config.x.visual_regression.screenshot_enabled = Rails.env.test? && screenshot_enable
end

以下のようなVRT用のWebフォントを適用するCSSを用意し、

@import "https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap";

body {
  font-family: "Noto Sans JP", sans-serif !important;
}

VRT実行時だけ読み込むことで OS 間のフォント差分を無視できるようにしました。

<% if Rails.configuration.x.visual_regression.screenshot_enabled %>
  <%= stylesheet_pack_tag 'visual_regression/override' %>
<% end %>

おわりに

まだ拡充途中のため具体的な効果までは検証できていませんが、MedPeerにVRT基盤を構築したことによって、手動テストの削減やCSSリファクタリングを安全に行うための環境を整備できるようになりました🎉

MedPeerは医療を扱うサービスのため、こういった仕組みを利用して安定的なサービス提供を実現しつつ、スピード感も維持していきたいです💪

最後まで読んでいただきありがとうございました✨

参考にさせて頂いた資料

tech.speee.jp

engineering.linecorp.com


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Ruby 3.3(+YJIT)へのアップデートによるパフォーマンス変化の計測

こんにちは。サーバーサイドエンジニアの @atolix_です。

今回はメドピアで本番運用をしているアプリケーションの1つであるやくばと for Clinicにて、Ruby 3.2からRuby 3.3にアップデートを行った際のパフォーマンスの変化を計測しました。

Ruby 3.3ではYJITの大幅な改善が含まれているので、これによるアプリケーションへの影響を確認していきます。

www.ruby-lang.org

gihyo.jp

前提

本記事に記載されたデータは以下の条件で計測をしています。

  • Rails: 7.1.3.4
  • YJIT有効化時のオプションは特に付与していない状態(Dockerfileから環境変数を与えて有効化)
  • 3.2.4(+YJIT)から3.3.1(+YJIT)へのアップデート
  • 有効化前後の1週間を比較

パフォーマンスの変化

やくばと for Clinicではモノレポのアプリケーション内で、クリニックAPI・患者APIといった括りでエンドポイントおよびサーバーを分割しているので、それぞれのAPI Latency/CPU/Memoryの変化を確認していきます。

API Latency(低い方が性能が良い)

クリニックAPI

パーセンタイル before(avg) after(avg)
p50 49ms 45ms
p90 344ms 310ms
p95 518ms 470ms
p99 644ms 580ms

約9~10%の短縮がされているので、かなり改善されていることが分かります。

患者API

パーセンタイル before(avg) after(avg)
p50 132ms 104ms
p90 742ms 523ms
p95 888ms 619ms
p99 937ms 645ms

患者API側はより大きく効果が出ており、全体で30%弱の短縮が実現できています。

CPU使用率(低い方が性能が良い)

クリニックAPI

before(avg) after(avg)
0.42% 0.34%

0.08%減で改善されているようです。

患者API

before(avg) after(avg)
0.73% 0.51%

0.22%減でクリニックAPIよりも大きく効果が出ていそうです。

メモリ使用率(低い方が性能が良い)

クリニックAPI

before(avg) after(avg)
441MiB 455MiB

メモリ使用率は3%ほど増加しています。

患者API

before(avg) after(avg)
517MiB 533MiB

こちらもメモリ使用率は3%ほど増加しています。

RubyVM::YJIT.enableでの有効化

Ruby 3.3からのYJITの変更点として、RubyVM::YJIT.enableの実装も挙げられます。

これまでのRubyでは環境変数RUBY_YJIT_ENABLE=1の設定やコマンドラインで--yjitの付与をしないとYJITを有効化することができませんでしたが、Ruby 3.3からはコード内でRubyVM::YJIT.enableを呼び出すことでYJITの有効化が行えるようになりました。

開発中のRails7.2ではデフォルトでRubyVM::YJIT.enableを呼び出すinitializerが実装される予定です。

また、YJITの起動方法をRubyVM::YJIT.enableに切り替えることでメモリ消費量の点でもメリットがあります。

k0kubun.hatenablog.com

もう一つの利点は、YJITの起動を遅延させることで、 アプリ初期化後は使われないコードのコンパイルを避けメモリ消費量を削減できる点である。 Railsのイニシャライザでも効果はあるが、理想的にはUnicornのafter_forkやPumaのafter_worker_forkから呼び出すと良い。 これにより、起動するワーカーの半分だけYJITを有効化し、インタプリタと性能を比較する基盤として利用することもできる。

という訳でYJITの有効化方法をDockerfile上での環境変数からRubyVM::YJIT.enableに切り替えて、上記のメモリ消費量から更にどのような変化が現れるか1週間分の計測をします。

Rails7.2の実装と同じようにinitializer配下にRubyVM::YJIT.enableを呼び出すコードを配置します。

if defined? RubyVM::YJIT.enable
  Rails.application.config.after_initialize do
    RubyVM::YJIT.enable
  end
end

クリニックAPI

Ruby 3.2.4(avg) Ruby 3.3.1 環境変数によるYJIT有効化時(avg) Ruby 3.3.1 RubyVM::YJIT.enableによるYJIT有効化時(avg)
441MiB 455MiB 424MiB

455Mib -> 424MiBなので7%ほどの減少で効果が出ていそうです。

患者API

Ruby 3.2.4(avg) Ruby 3.3.1 環境変数によるYJIT有効化時(avg) Ruby 3.3.1 RubyVM::YJIT.enableによるYJIT有効化時(avg)
517MiB 533MiB 503MiB

533MiB -> 503MiBで6%ほど消費量が抑えられています。

まとめ

既にYJITを有効化しているRuby 3.2からのアップデートでも大幅なAPIレイテンシ/CPU使用率改善が確認できました。 また、RubyVM::YJIT.enableによってメモリ消費量の削減も確認できたので、まだ有効化方法を切り替えていないプロジェクトでは積極的に導入するのが良さそうです。 一方でプロジェクトによってはメモリの使用量が増加する場合も有り得るのでアップデート前後で継続的な監視が必要そうです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Matzさんを交えて、RubyKaigi 2024の社内共有会を実施しました!

こんにちは!メドピアの伏見(@fussy113)です!

メドピアでは、Rubyの父でありメドピアの技術アドバイザリーを務めていただいているMatzさん(@yukihiro_matz)をお招きして、オンライン会議を開催しています。

そのオンライン会議にて、5月に開催されたRubyKaigi 2024に参加したエンジニアからの共有会を実施しました。

この記事ではその共有会のレポートをお届けします!

RubyKaigi 2024 と メドピアのスポンサーの運用

1人目はVPoTの平川さん(@arihh)より、スポンサーブース運営や、RubyKaigi 2024全体についての振り返りのLTでした!

メドピアはブースでアンケートを行っており、その内容の共有であったり、他社さんの取り組みで良かったものなど、RubyKaigi 2024全体の雰囲気が伝わるような内容でした。

今後のスポンサーブース運営の際に、また見直したい内容のLTでした。

(ちなみにMatzさんからは、メドピアのスポンサーブースで配っていたデカバッグについて、とても良かったという感想をいただきました!)

ブースの振り返りは、MedPeer Styleでも記事を書いているので、ご覧ください。

style.medpeer.co.jp

RubyKaigiのプロポーザルを通したい。

2人目は榎本さん(@toshimaru_e)より、RubyKaigi の特殊性、熱量を伝える内容のLTでした!

RubyKaigi はRubyを"使って"いる人ではなく、Rubyを"作って"いる人が来る、"最高のTech カンファレンスを作りたい"という運営の強い思いを持って作り上げられていること。

そんな最高のTech カンファレンスにプロポーザルを通すには、どうすれば良いのかを過去の発表内容をもとに考えようといった内容でした。

RubyKaigi の特殊性や唯一無二性を聞き、そこに登壇しているエンジニアは本当にかっこいいなと素直に感じました。

自分も自慢できるコードを日々書き続けて、RubyKaigi にプロポーザルを出したいと刺激をいただきました。

発表された資料は、公開もされているので、ぜひご覧ください。

speakerdeck.com

ruby.wasm 最前線 2024 - wasmでMockServerをつくる

折り返し、3人目は草分さん(@lni_T)より、ruby.wasm についてのLTです!

RubyKaigi 2024で話されたruby.wasm に関連するセッションの共有と、その内容を活用してサーバー起動が不要なモックサーバーをruby.wasm × Service Worker で作ったというデモが実施されました。

bundler がruby.wasm に対応して、build の手順がどんどん楽になったり、Pure RubyなGemはruby.wasmに組み込むことが出来るようになっていたりと、開発者がどんどん触りやすくなっていることに驚きでした。

またMatzさんから、他のプログラミング言語でのwasm の試みの動向について聞くことができたのも興味深かったです。

発表された資料は、公開もされているので、ぜひご覧ください。

speakerdeck.com

Improved REXML XML parsing performance using StringScanner

最後は、RubyKaigi 2024のLT に登壇した内藤さん(@naitoh)のリバイバル講演でした!

REXML Gem内の実装をRegexpからStringScanner に書き換えることによって、処理速度を本番でも利用を検討できるレベルに改善されたという内容でした。

書き換えにあたってベンチマークを用意して計測を行い、ボトルネックを確認してから実装を進めるなど、ライブラリの速度改善についての進め方として参考にしたいなという印象を持ちました。

MatzさんからもLTではなく、30分のセッションでも良い内容と嬉しい言葉をいただきました!

発表された資料は、公開もされているので、ぜひご覧ください。

speakerdeck.com

終わりに

Meetで集まって、コメントや、リアクションで盛り上がりつつ、ワイワイと共有会を進めることができたので、とても楽しむ会ができたと思っています!

開催後アンケートより参加したエンジニアからも、"ダイジェスト的に内容を知ることができた"、"ruby.wasmなど、さらに進化していることを感じることができた"とポジティブな感想をいただき、よかったです。

今後もワイワイと社内を盛り上げていけたらと思います!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Nuxt 3 への移行に Nuxt Bridge を使うのはいかが?

こんにちは!フロントエンドエンジニアの土屋 (@tutti2612) です。

いよいよ Nuxt 2 の EOL が迫ってきましたね。

nuxt.com

先日、弊社でもとあるプロダクトの Nuxt 3 への移行を完了させました。

メドピアでは既に複数のプロダクトで Nuxt 3 への移行を行ってきましたが、今回の移行では今までとは違ったアプローチを取りましたので、その詳細をお伝えしたいと思います。

これから Nuxt 3 への移行を考えている方にとって、少しでもお役に立てれば幸いです。

今回の移行の特徴

今回の移行の特徴として、以下の2点が挙げられます。

  1. Nuxt Bridge の使用
  2. 積極的な新機能開発を行いながらのビッグバンリリース

それぞれ解説していきます。

Nuxt Bridge の使用

今回の Nuxt 3 移行における一番の特徴は、Nuxt Bridge を使用したことです。

Nuxt Bridge とは、Nuxt 2 で Nuxt 3 の機能の一部が使えるようになるライブラリです。これを使うことで、Nuxt 3 への段階的な移行が可能になります。 nuxt.config.ts でフィーチャーフラグを切り替えることで、Nuxt Bridge が提供する機能を簡単に有効/無効化することができます。 これにより、移行作業中に必要な機能を段階的に有効化し、Nuxt 3 の機能を試しながら安定した環境を維持することができます。

nuxt.config.ts

Nuxt Bridge を使った移行手順については、Nuxt Bridge のコアコントリビュータである wattanx さんのスライドや、Nuxt 公式のマイグレーションガイドで詳しく紹介されています。これらのリソースを参考にすることで、スムーズな移行を実現することができます。

nuxt.com

積極的な新機能開発を行いながらのビッグバンリリース

もう一つの特徴として、今回の移行では積極的な新機能開発を行いながらのビッグバンリリースを実施しました。

どのくらいのビッグバンリリースかというと、このくらいの規模になります。

リポジトリのほぼ全てのファイルに変更を加えています。

なぜこのようなビッグバンリリースになったかというと、このプロダクトは自動テストをあまり書いておらず、動作確認は手動で行う必要があったためです。Nuxt Bridge を使用しても、Nuxt 3 の破壊的変更を適宜リリースするには手動での動作確認に多大な工数がかかると判断しました。移行の前に自動テストを拡充することも考えましたが、それもまた多大な工数が必要で、EOL までに移行を完了するスケジュールが立てられませんでした。

さらに、成長途中のプロダクトであるため、新機能開発を止めることができませんでした。新機能の動作確認にも多くの工数が必要だったため、Nuxt 3 移行の細かなリリースによる動作確認に十分な工数を割くことができませんでした。

これらの点を考慮し、ビッグバンリリースを選択しました。

具体的には、マイグレーションブランチを用意し、Nuxt 3 移行はそのブランチ上で行いました。新機能開発での変更箇所は適宜マイグレーションブランチにマージしていくことで、develop ブランチとの乖離を防ぎました。

成功の要因

積極的な新機能開発を行いながらのビッグバンリリースというリスクのある移行方法でしたが、無障害で移行を完了することができました。

成功の要因としては、以下の点が挙げられます。

  1. Nuxt Bridge の使用
  2. 週 1 回の定例ミーティング
  3. エンジニア以外にも協力してもらった手厚い手動テスト
  4. 巨人の肩に乗ることができた

これらもそれぞれ解説していきます。

Nuxt Bridge の使用

成功の要因の一つとして、今回の移行の特徴でもある Nuxt Bridge の使用が挙げられます。

ビッグバンリリースを行ったため、段階的に移行ができる Nuxt Bridge のメリットが無いのでは?と思われるかもしれませんが、実際にはそうではありません。Nuxt Bridge を使用して段階的に移行することで、Pull Request の粒度を細かくすることができました。これにより、コードレビューのしやすさが格段に向上しました。

粒度の細かいPRの例

また、各破壊的変更の動作確認を Nuxt 2 が動いている状態で行えることも大きなメリットです。Nuxt Bridge を使わない場合、すべての破壊的変更に対応してからでないと動作確認ができませんが、Nuxt Bridge を使用することで、各変更の影響を個別に確認しながら進めることができました。

このように、Nuxt Bridge を活用することで、段階的な移行が可能となり、結果として無障害での移行に大きく貢献しました。

週 1 回の定例ミーティング

移行作業はのべ 3 人のエンジニアで行いましたが、新機能開発と同時進行だったため、3 人が常に移行作業に専念できるわけではありませんでした。

しばらく移行作業から離れていたメンバーが状況を把握できるように、移行関係者で週1回の定例ミーティングを実施しました。このミーティングは「Nuxt 3 作戦会議」と題し、移行作業の進捗状況や課題を共有しました。タスクの管理には GitHub Projects を用い、進捗状況を可視化していました。

GitHub Projects のロードマップ

このミーティングのおかげで、各エンジニアのタスクの進捗と次のステップが明確になり、効率的な作業を促進しました。

エンジニア以外の協力を得た手厚い手動テスト

無障害での Nuxt 3 移行に成功した要因のひとつに、エンジニア以外の協力を得た手厚い手動テストの実施があります。

先に述べた通り、このプロダクトは自動テストの量が少なく、手動テストに頼らざるを得ませんでした。エンジニア視点のテストだけだと、抜け漏れが発生する可能性を拭いきれませんでした。そこで、PdM やカスタマーサクセスなど、エンジニア以外のメンバーにも手動テストに協力してもらいました。

これにより、複数の視点から手動テストを実施することができ、無障害での移行に大きく貢献したと考えています。

巨人の肩に乗ることができた

最後に、この移行を成功させた要因として欠かせなかったのは、社内に既に複数の Nuxt 3 移行を成功させていたエキスパートたちがいたことです。彼らの経験と知見が社内に蓄積されていたおかげで、スムーズな移行が可能となりました。

移行の注意点を事前に彼らから聞くことができ、さらに彼らが残してくれたドキュメントを参照することで、予想される落とし穴を回避することができました。

これらの貴重なドキュメントの一部は、弊社ブログや Speaker Deck で公開しています。

tech.medpeer.co.jp

tech.medpeer.co.jp

tech.medpeer.co.jp

それぞれ異なるアプローチを紹介しているため、これから Nuxt 3 移行を考えている方はぜひ一読することをおすすめします。あなたのプロダクトに最適な手法が見つかることでしょう。

こうすれば良かった

無事に Nuxt 3 への移行が成功しましたが、振り返ってみると「こうすれば良かった」と思う点もあります。

今回の移行では、Nuxt のバージョンが 2.14 の状態から Nuxt 3 に移行したのですが、少なくとも 2.17 にアップデートしてからビッグバンリリースを行うべきだったと感じています。

マイグレーションブランチに develop ブランチをマージした際、コンフリクトの解消に非常に苦労しました。Nuxt 2.17 にアップデートしていれば、Nuxt で使用している Vue のバージョンが 2.7 に上がり、CompositionAPI が Vue 本体から提供されるため、コンフリクトの発生数を減らせたと考えています。

おわりに

メドピアの Vue 3 / Nuxt 3 移行の集大成として臨んだこのプロジェクト。 なんとか EOL までに Nuxt 3 に移行することができたと思ったのも束の間、Nuxt 4 のリリースが間近に迫っています。

nuxt.com

Nuxt 4 への移行においても、今回の Nuxt 3 移行で得た知見は大いに役立つはずです。これまでの経験を活かし、スムーズな移行を目指していきましょう。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

開発生産性の改善から1年経過したチームで考えていること

こんにちは。エンジニアの保立(@purunkaoru) です。

僕のチームでは、開発生産性の改善に取り組んでから1年経過しました。 開発生産性の改善系の記事やノウハウは世間によく出ていますが、1年経過した今、開発生産性に対してEMの立場で何を考えているかを言語化します。

チームメンバーの構成は、執筆時で以下の通りです。

  • フロントエンド: 5名
  • サーバーサイド: 9名
  • モバイルアプリ: 3名
  • EM(保立): 1名

弊社では、Findy Team+ を導入し、開発生産性を見えるようにしています。 まずはFindy Team+の画面を見ながら、改善結果を見ていきます。

  • 直近1年間

  • 直近2年間

直近2年で見ると、後半1年で生産性が改善されており、その改善が一定維持できていることがわかります。

ちなみに、このサイクルタイム分析について、数値的な目標を今まで一度も掲げてきませんでした。 どうしても指標に対する数値目標を掲げてしまうと、それに対するハックが進んでしまい、本質的な改善にならないと感じていたからです。チーム内では、開発に時間がかかる・レビューに時間がかかると誰かが感じた時に、Findy Team+の定量的な数値を見てチームに改善を促すコミュニケーションを取るケースが多かったです。

また、Four Keysの分析(Findy Team+でいうDevOps分析)は、botでリリースPRを作っているせいか、うまく集計できないので分析対象から外しています。 社内には、Four Keysの分析をOKRに組み込んでいるチームもあります。

それでは、ここから本題に入り、開発生産性の改善が進んでから1年経過して、何をチェックしているか記載していきます。

1. チーム別のアウトプット量の推移や負荷の確認

エンジニアチームとして、アウトプットの量の推移を確認することは不可欠です。アウトカムとしてのKPI(サブKPI)への貢献もチーム全体で把握していますが、開発に関わるアウトプット量の変化にも注目しています。

アウトプットの量は「1人あたりのプルリクエスト数」と「1プルリクエストあたりの平均変更行数」で測っています。以下の画像の棒グラフ(薄い色の方)で示されている「1人あたりのプルリクエスト数」は、昨年に比べて約20%増加していることが分かります。

一方で、「1プルリクエストあたりの平均変更行数」に関しては、別の画面で確認する必要がありますが、月ごとの推移を1画面で見る方法を見つけられなかったため、ここでは画像の提供は省略します。僕のチームの平均変更行数は、月ごとにばらつきはありますが、過去と比べて増加または減少した傾向は見られませんでした。

ここから読み取れることは2つあります。

1つ目は、チーム全体のアウトプット量が増加していること。これは、要件定義や設計のコミュニケーションが整備され、スムーズになったこと、またエンジニア自身の技術力やレビュー能力の向上が背景にあります。

2つ目は、12月にプルリクエスト数が急増しており、稼働時間が多い状況になったこと。主な要因は体制変更にありました。新しいチーム体制での見積もり精度が落ち、稼働が厳しくなってしまいました。11月末にはプルリクエスト数が増加する傾向にありましたが、対応が12月初旬まで遅れたことは、データチェックの不足や閾値設定の不備が原因です。以降、プルリクエストの数の増減にはこまめに目を光らせるようになりました。

このように、定量的なデータを活用して、チームの生産性と負荷のバランスを定量的な指標をもとに管理しています。

2. サイクルタイム分析による開発サイクルの確認

開発サイクルの改善を行ってから1年が経過し、サイクルは成熟しており、サイクルタイムの増減は限定的です。 サイクルタイムに大きな変動が生じる主な要因には、大規模な機能のリリース、メンバーの交代、チーム体制の変更、施策の追加や削除などがあります。これらの要因は、開発サイクルが改善されるか、逆に悪化するかを事前に予測できます。

たとえば、2024年4月から新しいメンバーが加わり、全エンジニアにメンターを配置する制度も導入しました。これにより、教育やMTGに費やす時間が増えました。このような新しい施策を実施したにもかかわらず、サイクルタイムや1人あたりのプルリクエスト数に変化が見られない場合、新たに増えた教育や会議の時間を考慮せずに見積もりを行い、チームメンバーが無理してタスクを完了させている可能性が考えられます。

そのため、新しい施策を導入する際には、いつごろから数値が改善するか、あるいは悪化するかを予測し、実際にその通りになっているかを後から確認するようにしています。

3. 十分な質のアウトプットが出せる量を超えてアウトプットをしていないかの確認

多くのサービスや機能を一人で担当することは効率が悪くなるとされています。エンジニアの世界でも、「チームトポロジー」などの書籍を通じて、シンプルで効率的な組織構造の重要性が説かれています。

同様に、一人が多数のプルリクエストやチケットを抱えると、対応を忘れてしまったり、ひとつひとつの作業に対する注意力が散漫になることがあります。チームによっては、レビュー待ち状態やテスト待ち状態のプルリクが放置されてしまう事象があると思います。

例えばメンバークラスのエンジニアが、タスク管理と開発の両方を行う役割に移行した場合を考えてみます。当然、タスク管理の責任が増えるため、割り当てられるチケット数やプルリクの数は自然と減少すべきです。しかし、変更前と同じ量を割り当ててしまうケースがよくあります。また、チームメンバーが増えた場合も、タスク管理の工数は増えますが、チームの増減を考慮せずにチケットを割り当てがちです。そのため、各人の役割に応じたタスク量を適切に考慮し、負荷を調整することが必要です。

現在、負荷量は「1日あたりのプルリク数」×「コミットからマージまでの日数」という計算式で把握しています。単純な「1日あたりのプルリク数」や「チケット消化数」では、プルリクやチケットの内容による重さを正確に反映できません。そのため、軽いプルリクはコミットからマージまでの時間が短く、重いプルリクは時間がかかると見込んで、この計算式を採用しました。

この計算式で算出される負荷の量を、各メンバーが理想的に対応できる数値と比較し、適切かどうかをチェックしています。自分自身の場合、集中的にプレイヤーとして活動できる時期は「18」が上限ですが、10名程度を管理していた際は「6」が限界でした。ただし、個人の生活リズムやその他の業務負荷により、「コミットからマージまでの日数」は変わるため、他人と比べるよりも過去の自分自身と比較して閾値を設定しています。

上記の例だと、 「1日あたりのプルリク数」は、52PR / 20営業日 = 2.6PR/営業日 となります。 「コミットからマージまでの日数」は、(2.5時間 + 8.5時間) / 8時間 = 1.4時間弱となります。 (「8時間」は、1日あたりの稼働時間です) 負荷量(「1日あたりのプルリク数」 * 「コミットからマージまでの日数」)は、2.6 * 1.4 = 約3.6 となります。 安全ですね。

この指標は改善が必要かなと思いつつ、ひとりひとりの負荷をアクティビティベースに定量的に確認する術が無く、今はこの方法で管理しています。

4. 新しいメンバーが機能しているか・無理していないかの確認

オンボーディング施策の一環として、新たにチームに参画したメンバーが機能しているか・無理していないかも確認しています。これは、主に業務時間で見ていますが、徐々にやれることが増えているかという点で、プルリク数・プルリクに対するコメント数・レビュー数などでも確認しています。

特に多いのが、急激に頑張ろうとしてプルリク数とレビュー数が急激に伸びているケースです。このケースでは、1プルリクあたりに対するレビューコメント数も多くなる傾向があり、レビュワー・レビュイーともに疲弊するケースがあります。こういった場合は、目標を落として、その分ペアプロをしたり、レビューコメント数が少なくなるための施策を一緒に考えます。

5. サービスの障害に関する確認

最後に、Findy Team+から離れた場で開発生産性に関して確認している数値について説明します。

サービスの障害を未然に防ぐため、自動テストだけでなく、エンジニアによる手動テストやプロダクトマネージャー、QA、CS担当者の手動テストも行っています。これらのテストにかかる期間や発見されたバグの数を記録し、各々の閾値を設定しています。もちろん障害の発生件数も確認していますが、障害発生前の手戻りやバグ検出に伴う工数も管理し、減少させる努力を続けています。これらの数値は品質管理の文脈で語られることが多いですが、結合テストや受入テストの工程の工数が減ると開発生産性は上がるので、今回の記事にも取り入れました。

この取り組みは、今期から始めたばかりですが、既に一定の成果が見られ、良い施策であったと感じています。

まとめ

数値化による生産性の管理は、チームのパフォーマンスや成長を明確に追跡することができ、またチームの負荷も把握することができます。これにより、客観的なデータに基づいて意思決定や評価を行うことができます。一方で、一度数値化してしまうと、数値を良くすることに目を向けすぎて、本質的な改善にならないケースもあります。そのため、前提を疑いながらウォッチしていくことが大切だと考えています。

今回記載した開発生産性の改善から1年経過したチームで考えていること・見ていることについて、参考になれば嬉しいです。メドピアの開発者ブログでは、技術的なことやチームマネジメントなど、参考になりそうなことを発信しているので、是非ブックマークをしていただけると嬉しいです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp