条件付き呼び出しで削減したAPIコスト

条件付き呼び出しで削減したAPIコスト

1. 概要

前回の記事では、マイデータサービスの運用中に直面したメイン画面の応答遅延を、キャッシュ優先レンダリングとバックグラウンド更新、つまり現在の Stale-While-Revalidate(SWR) パターンと同じ方向性の戦略によって解決した経験を扱いました。今回は、同じサービスの運用で次に直面した課題を扱います。今回は性能ではなく、コストが問題でした。

外部機関照会 API の課金体系が変更され、呼び出し件数そのものが運用コストになったため、「必要なときに毎回すべて照会する」という従来の方式をそのまま維持することが難しくなりました。これを解決するために適用したのが、照会 Timestamp に基づく条件付き呼び出しと、呼び出しタイミングの再配置(バッチ更新 + Lazy Loading)でした。そして今回も、後から振り返ってみると、当時は名前を知らなかったこのアプローチが、HTTP プロトコルが以前から標準で提供してきた条件付きリクエスト(Conditional Request)と同じ発想であることに気づきました。この記事では、問題の状況と解決の過程、そして標準メカニズムの観点からの再解釈を順に整理します。

2. 問題の状況:呼び出し件数がそのままコストになる

マイデータサービスは、複数の外部金融機関の標準 API を呼び出し、ユーザーの資産・取引情報を収集する構造になっています。1つの更新フローでも、機関一覧の取得、口座一覧の取得、口座ごとの詳細取得、取引履歴の取得など、下位 API が連鎖的に呼び出されるため、1人のユーザーの情報を1回更新するだけでも、かなりの数の呼び出しが発生します。

サービス初期には、この呼び出し量は大きな負担ではありませんでした。しかし、呼び出し件数に基づく課金体系へ移行したことで、状況が変わりました。更新のたびに変更の有無にかかわらず全体を再取得する従来方式では、実際には何も変わっていないデータを再び取得する呼び出しにも、同じコストが発生していました。ユーザー数が増えるほど、この無駄も同時に拡大する構造でした。

前回の記事の応答遅延がユーザーの直接的な体感に関わる問題だったとすれば、今回の課題はユーザーの目には見えないものの、運用の持続性に直結する問題でした。問題を改めて定義すると、次のようになります。「どうせ変わっていないデータを確認するために、毎回全体を再取得する必要があるのか」。結局、解決の方向性は呼び出しそのものをなくすことではなく、不要な呼び出しを特定して省略することでした。

3. 解決戦略 1:Timestamp に基づく条件付き呼び出し

マイデータ標準 API の仕様には、照会の基準時点を扱う Timestamp という概念があります。これを利用すると、「前回の照会時点以降に変更があったか」を先に確認し、変更がなければ下位 API の呼び出しを省略したうえで、既に保存してあるデータをそのまま利用できます。変更がある場合にのみ、詳細照会を実行するのです。

処理の流れを疑似コードで整理すると、次のようになります。

function 자산정보갱신(사용자ID, 기관ID):
  이전조회시점 = 조회이력.최근조회시점(사용자ID, 기관ID)
  변경여부 = 외부기관API.변경확인(사용자ID, 이전조회시점)
 
  if 변경여부 == 없음:
     return 저장소.기존데이터(사용자ID, 기관ID)
 
  최신데이터 = 외부기관API.상세조회(사용자ID)  // 과금 대상 호출
  저장소.갱신(사용자ID, 기관ID, 최신데이터)
  조회이력.기록(사용자ID, 기관ID, 현재시각)
  return 최신데이터

この構造の核心は、「尋ねるコストは安く、受け取るコストは高い」という非対称性を活用した点にあります。変更の有無を確認する軽い呼び出しでまず絞り込み、実際にデータ全体を取得する重い呼び出しは、変更が確認された場合に限定しました。データが頻繁に変わらないユーザーほど、削減効果が大きくなる構造でもあります。

4. 解決戦略 2:呼び出しタイミングの再配置

条件付き呼び出しが「呼び出すかどうか」を選別する仕組みだとすれば、2つ目の戦略は「いつ呼び出すか」を改めて設計することでした。2つの方式を組み合わせました。

1つ目は、周期ベースのバッチ更新です。ユーザーが選択した更新周期を基準に、画面への प्रवेशとは関係なく、あらかじめ定めた時点でデータを更新しておく方式です。更新時点が予測可能になるため、呼び出し量が特定の時点に集中せず、ユーザーが画面に入ったときには、すでに更新されたデータをすぐに表示できます。

2つ目は、詳細データの遅延呼び出し(Lazy Loading)です。口座ごとの取引履歴のように、ユーザーが実際に開いて確認して初めて意味を持つ詳細データは、メインの更新フローであらかじめ取得せず、その詳細画面に入った時点で呼び出すように分離しました。すべてのユーザーがすべての口座の詳細まで確認するわけではないため、実際には参照されない詳細データに対する呼び出しを、構造的に削減できました。

まとめると、「必要な時点に、必要な分だけ」呼び出すということです。頻繁に見る概要情報は周期バッチで事前に取得し、ときどき見る詳細情報は画面への遷移時に遅延呼び出しし、さらにすべての呼び出しの前に条件付き確認を置くという3段構成でした。

4-1. 構造の比較

[表 1] 呼び出し構造の改善前後の比較

区分

従来の構造 (As-Is)

改善後の構造 (To-Be)

呼び出しの判断

更新時点ごとに全体を一括取得

変更を確認した後、必要な場合のみ取得

呼び出しのタイミング

画面への遷移・更新リクエスト時に即時実行

周期バッチ + 詳細画面への遷移時に遅延呼び出し

呼び出し量の特性

ユーザーのアクティビティ量に比例して増加

データの変更頻度に収束

詳細データ

メインフローで先行取得

実際に遷移した画面でのみ取得

5. 今振り返ると:HTTP 条件付きリクエストとのつながり

5-1. HTTP がすでに備えていたメカニズム

HTTP には、条件付きリクエスト(Conditional Request)という標準メカニズムがあります。クライアントが以前のレスポンスとともに受け取って保存しておいた更新日時(Last-Modified)やバージョン識別子(ETag)を、次のリクエストに If-Modified-Since、If-None-Match ヘッダーとして設定して送信すると、サーバーはその後に変更がなかった場合、本文なしで 304 Not Modified レスポンスのみを返します。「変更されたかを先に尋ね、変更されていなければ本文の送信を省略する」という発想です。

今回の事例における Timestamp に基づく条件付き呼び出しは、このメカニズムと同じ発想です。前回の照会時点を基準に変更の有無を先に確認することは If-Modified-Since と同じ役割を果たし、変更がない場合に下位 API の呼び出しを省略して保存済みデータを再利用することは、304 レスポンスと同じ効果を持ちます。ただし、前回の記事で区別したのと同様に、HTTP ヘッダーを通じてプロトコルレベルで自動処理されたのではなく、標準 API の Timestamp 仕様を利用してアプリケーションコードで直接実装したという点で、レイヤーが異なります。プロトコルが提供するメカニズムを借用したのではなく、同じ発想をアプリケーションレベルで独立して実装した事例です。

6. 前回の記事との関係:同じキャッシュモデルを構成する2つの軸

興味深いのは、前回の記事の SWR 戦略と今回の記事の条件付き呼び出しが、どちらも HTTP キャッシュモデルの中に並んで存在する2つの軸だということです。方向性は互いに反対です。SWR は「まず古いデータでも先に返し、検証はバックグラウンドで後から行う」という戦略で、応答速度を優先します。条件付きリクエストは「まず検証し、変更がなければそもそも送信しない」という戦略で、送信と呼び出しのコストを優先します。

[表 2] 2つのキャッシュ戦略の比較

項目

Stale-While-Revalidate (前回の記事)

条件付きリクエスト (今回の記事)

基本動作

古いデータを即時に返した後、バックグラウンドで検証

変更を確認した後、変更がある場合のみデータを受信

優先する価値

応答速度、ユーザーが体感する性能

呼び出し・送信コストの削減

節約するもの

ユーザーの待ち時間

不要な呼び出しと転送量

適用した箇所

メイン画面への遷移時の応答

外部機関への照会呼び出し

実際のサービスでは、2つの戦略が異なる箇所で共存していました。ユーザーが目にする画面の応答にはSWR方向の戦略を、外部へ送る呼び出しには条件付き呼び出しを適用したのです。1つのキャッシュ理論のもとで、「何を節約するか」に応じて、反対方向の戦略がそれぞれの場所で使われていたというわけです。

7. 限界とトレードオフ

この構造の前提は、変更の有無を判断する基準となるTimestampが信頼できることです。外部機関が変更時点の情報を正確に提供しなければ、変更を見逃したり、逆に不要な再照会が発生したりする可能性があります。実際の運用では、外部機関ごとに仕様の遵守レベルにばらつきがあり、これによるデータ品質の問題は、それ自体が別途対応を要するテーマでした。

更新遅延の許容範囲も、継続的に調整が必要なポイントでした。条件付きの省略や定期バッチが増えるほどコストは下がりますが、データの鮮度はその分後退します。前回の記事のキャッシュ保持時間の基準と同様に、データの性質ごとにどの程度の遅延まで許容するかを決めることは、一律の正解がない個別判断の領域でした。バッチの周期も、頻繁すぎれば削減効果が薄れ、間隔が空きすぎれば鮮度が低下するという綱引きでした。

8. 運用基準としての定着

前回の記事で画面側のキャッシュ保持時間をデータの種類ごとの基準表として整理したのと同じように、今回は外部呼び出し側についても、データの種類ごとの呼び出し戦略の基準を整理しておきました。新しいデータ項目が追加されるたびに呼び出し方法を最初から検討するのではなく、データの性質を基準に照らして決定できるようにしたのです。

[表 3] データの種類ごとの呼び出し戦略の基準(例)

データの種類

呼び出し時点

条件付き確認

資産サマリー(メイン表示)

周期ベースのバッチ更新

適用(変更がなければ省略)

口座詳細・取引履歴

詳細画面への遷移時に遅延呼び出し

適用(変更がなければ省略)

新規連携機関のデータ

連携直後に即時で全件照会

未適用(比較する履歴がないため)

この基準表は、前回の記事のキャッシュ運用基準表と対を成します。画面側には「どの程度古いデータまで表示するか」という基準が、呼び出し側には「いつ、どのような条件で呼び出すか」という基準がそれぞれ定着したことで、データの収集から表示までの全工程について、チームが同じ言葉で判断できるようになりました。

9. まとめ

前回の記事の応答遅延と今回の記事の呼び出しコストは、一見すると異なる問題でした。しかし解決してみると、結局は同じ問いに収束しました。「この呼び出しは、今この時点で本当に必要なのか」。その問いに画面側から答えたのがキャッシュ優先レンダリングであり、外部呼び出し側から答えたのが条件付き呼び出しと呼び出し時点の再配置でした。

そして今回も、当時は条件付きリクエストという標準メカニズムの名前を知らないまま問題に取り組み、同じ構造にたどり着きました。標準とパターンは、結局のところ同じ問題を先に経験した人々が整理しておいた答えなのだということを、2度の経験を通じてより明確に確認できました。

cryss

Site footer