本文へ移動

ホーム / App / 第4部 通信とバックエンド連携

第4部 通信とバックエンド連携・第7章

通信

★★★ 必須13トピック・面接 6問

モバイルの不安定なネットワークで、壊れにくい通信を作る方法を押さえる。

別のページで開く →

ネットワーク図を読み込んでいます…

7.1不安定な回線への対策

用語★★★ 必須
ひとことで

回線の切り替わり・圏外・遅い回線を前提に、タイムアウト・リトライ・再接続を設計する。

アプリの通信の不具合は、開発中の安定した Wi‑Fi ではほとんど再現しません。ところが利用者の多くは移動中に使うので、「固まる」「二重に注文された」といったレビューや問い合わせは、回線が不安定なときの作りの甘さから来ることがよくあります。

ここでは、待つ時間・やり直し方・待っている間の見せ方を、最初に決めておくことを押さえます。

スマホの回線は、電車・地下・エレベーター・Wi-Fi と 4G/5G の切り替わりで、切れたり極端に遅くなったりします。「通信は失敗するもの」として作るのが前提です。

失敗する前提の通信の流れ送るタイムアウトを決める失敗回線・5xx・429安全なら再試行間をあける諦める再試行のボタン再試行してよいのは GET と、冪等キーを付けた POST だけ(決済の二重送信を防ぐ)
待つ時間に上限を決め、安全なものだけを間をあけて再試行し、だめなら利用者に任せる
対策具体的に
タイムアウトを決める接続と全体の時間を分けて決める。何十秒も待たせず、画面には読み込み中と再試行のボタンを出す
再試行は安全なものだけGET や冪等キーを付けた POST だけ自動で再試行。決済の二重送信を防ぐ
待ってから再試行指数バックオフとジッター(下で説明)。429・503 の Retry-After は守る
つながるまで待つiOS は URLSessionConfiguration.waitsForConnectivity、Android は WorkManager のネットワーク条件
通信量を減らす画像は表示する大きさで取る。一覧はページ単位。キャッシュと ETag を使う

通信を待つ間・失敗したときに画面に何を見せるかは、次の回線が悪くても使える画面にする(キャッシュを先に見せる)で説明します。

(SRE)リトライの設計

(SRE)HTTP/1.1・HTTP/2・HTTP/3 の比較

下の「ブラウザで試す」の図では、電車の中で注文を送り、サーバーは処理したのに返事が途中で消える場面から始まります。アプリは1秒・2秒・4秒と待ち時間を倍にし、ランダムなずれ(ジッター)を足してやり直し(指数バックオフ)、同じ冪等キーで送り直すので、サーバーは注文を作り直さずに前回の結果を返します。上の表の「再試行は安全なものだけ」「待ってから再試行」が、どの手順にあたるかを見てください。

面接ではこう答える

スマホの回線は、電車や地下、Wi-Fi とモバイル回線の切り替わりで切れたり極端に遅くなったりするので、通信は失敗するものとして作ります。接続と全体のタイムアウトを決めて何十秒も待たせず、自動で再試行するのは GET や冪等キーを付けた POST だけにし、指数バックオフとジッターで間をあけます。つながるまで待つ仕組みや、画像を表示の大きさで取るなど通信量を減らす工夫も合わせて使います。

別のページで開く →

ネットワーク図を読み込んでいます…

ひとことで

前回のデータを端末に保存しておき、まずそれを表示して、裏で取り直して差し替える。失敗しても画面を空にせず、古さを伝える。

回線が悪いときに「読み込み中」のまま待たせたり、エラーで画面を空にしたりすると、使えないアプリに見えます。前回取ったデータを端末に保存しておき、まずそれを見せて、裏で新しいデータを取り直すのが基本の対策です(Web の stale-while-revalidate と同じ考え方)。

まず端末のデータを見せ、裏で取り直して差し替える0 秒時間 →① すぐ表示端末の DB・キャッシュ② 裏で取り直す「更新中」を小さく出す③ 成功:差し替え最新に。保存もする③ 失敗:そのまま古いデータ+時刻の表示利用者は待たずに読める。失敗しても画面が空にならず、「○分前の情報です・オフライン」と分かるデータが端末に1件もないとき(初回)だけ、読み込み中の表示とエラー・再試行の画面を出す
キャッシュを先に見せる(stale-while-revalidate の考え方)
やり方具体的に
キャッシュを先に見せる画面は端末の DB(Room・Core Data・SwiftData など)を見て表示し、通信の結果は DB に書く。DB が変われば画面も変わる(Single Source of Truth)
古さを伝える「最終更新 10:42」「オフラインです」の小さな表示。在庫・価格・残高など、古いと困る値は「確認中」にするか、操作の前に取り直す
楽観的な更新いいね・既読などは、送る前に画面を先に変える。失敗したら元に戻して知らせる(送信待ちは outbox に入れて、つながったら送る)
部分的に表示する一覧は出せたが画像はまだ、のように、取れたものから出す。画像は小さいサムネイルを先に
先読みWi‑Fi のときや、次に開きそうな画面のデータを先に取っておく(通信量と電池に注意)
回線の状態を見るAndroid の ConnectivityManager、iOS の NWPathMonitor で「つながっていない・従量制の回線」を知り、表示や先読みを変える(ただし「つながっている」でも実際は通らないことがあるので、通信の結果で判断する)

キャッシュを見せてはいけないもの・気をつけること

  • ログアウト・アカウントの切り替えのときは、前の人のキャッシュを必ず消す(他人のデータが見える事故)
  • 決済・在庫・予約の確定など、古い値で判断すると困る操作は、操作の直前にサーバーで確かめる
  • 個人情報を端末に保存するなら、端末の暗号化と保存の期間を決める
  • キャッシュの形(スキーマ)は、アプリの版が上がっても読めるようにする(古い版のデータの移行)

まとめると、「通信は失敗するもの」と考えて、タイムアウト・再試行(不安定な回線への対策)+ キャッシュを先に見せる + 送信待ちのキューを組み合わせます。

面接ではこう答える

不安定な回線への対策として、前回取ったデータを端末の DB に保存しておき、画面を開いたらまずそれを表示して、裏で新しいデータを取り直します。成功したら差し替え、失敗したら古いデータのまま「最終更新の時刻」や「オフライン」を小さく出します。いいねのような操作は楽観的に画面を先に変え、送信は送信待ちのキューに入れます。ただしログアウト時のキャッシュの削除や、決済のように古い値で判断すると困る操作は、必ずサーバーで確かめます。

ひとことで

失敗するたびに待つ時間を倍にし(指数バックオフ)、さらにランダムにずらす(ジッター)ことで、再試行の集中を防ぐ。

再試行の間隔の決め方です。たくさんの端末が同じサーバーを使うアプリでは、再試行のやり方しだいで、障害を長引かせてしまいます。

失敗したらすぐ再試行するのではなく、待つ時間を 1秒・2秒・4秒・8秒…と倍にしていきます(指数バックオフ)。さらに待つ時間をランダムにずらします(ジッター)。ずらさないと、障害から戻った瞬間に何万台もの端末が同じタイミングで一斉に再試行し、サーバーをまた倒してしまいます。

ジッターがないと、全端末の再試行が同じ瞬間に重なるジッターなし時間 →ジッターあり時間 →1秒2秒4秒8秒障害が直った瞬間に一斉に再試行 → また落ちる再試行がばらけて、サーバーが回復できる
ジッターがないと再試行が同じ瞬間に集中し、あると時間に散らばる
待つ時間 = random(0, min(上限, 基準 × 2^回数))
例:基準 1秒・上限 30秒・最大 5回で諦めて「再試行」ボタンを出す

サーバーが 429 や 503 に Retry-After を付けてきたら、その時間を優先して待ちます。

面接ではこう答える

指数バックオフは、失敗したら待つ時間を1秒、2秒、4秒と倍にしていく再試行の方法で、ジッターはその待ち時間をランダムにずらすことです。ずらさないと、障害から戻った瞬間に何万台もの端末が同じタイミングで再試行し、サーバーをまた倒してしまいます。待ち時間には上限と最大の回数を決め、超えたら諦めて利用者に再試行のボタンを出します。Retry-After が返ってきたらそれに従います。

ひとことで

QUIC は UDP の上で動く通信の仕組みで、回線が変わっても接続を続けられ、接続も速い。HTTP/3 はその上で動く。

HTTP/3 は、スマホのように回線が切り替わる環境で特に効く新しい HTTP です。

HTTP/3 は TCP ではなく UDP の上の QUIC で動きます。モバイルで効くのは次の点です。

  • 回線が変わってもつながったまま:TCP は IP アドレスが変わると接続が切れますが、QUIC は接続 ID で相手を見分けるので、Wi-Fi から 4G に変わっても続けられます(コネクションマイグレーション)。
  • 接続が速い:暗号化の準備(TLS 1.3)と接続を一緒に行うので、往復が少なくて済みます。
  • 1つの取りこぼしで全体が止まらない:TCP では1つのパケットの再送待ちで同じ接続の全部が止まりますが、QUIC は流れごとに独立しています。

iOS の URLSession は、サーバーが HTTP/3 に対応していれば自動で使います。Android でよく使う OkHttp は単体では HTTP/3 を使わないので、使うなら Cronet(OkHttp から Cronet を使うための橋渡しのライブラリもある)を選びます。ただし UDP が通らない回線もあるので、HTTP/2 に戻れる前提です。

面接ではこう答える

HTTP/3 は TCP ではなく UDP の上の QUIC で動きます。モバイルで効くのは3点で、接続 ID で相手を見分けるので Wi-Fi からモバイル回線に変わっても接続を続けられること、暗号化の準備と接続を一緒に行うので往復が少ないこと、1つのパケットの取りこぼしで他の流れが止まらないことです。iOS の URLSession はサーバーが対応していれば自動で使い、Android の OkHttp は単体では使わないので Cronet を選びます。UDP が通らない回線もあるので HTTP/2 に戻れる前提で使います。

ひとことで

HTTP クライアントはアプリで1つにして接続を使い回し、ヘッダー・認証・ログはインターセプターに集める。

アプリの通信は、iOS では URLSession、Android では OkHttp(その上に Retrofit や Ktor)を使うのが一般的です。大事なのは、クライアントをアプリで1つにして使い回すことと、共通の処理を1か所に置くことです。

やることiOSAndroid
クライアントを使い回すURLSession を1つ作って共有するOkHttpClient を1つにする(DI で配る)
共通の処理を挟むURLProtocol や自前の包むクラスInterceptor(ヘッダー・認証・ログ)
タイムアウトtimeoutIntervalForRequest などconnectTimeout・callTimeout など
JSON の変換Codablekotlinx.serialization・Moshi

クライアントを API ごとに作ると、接続の使い回し(コネクションプール)が効かず、毎回 TLS の接続からやり直しになって遅くなります。ログを出すときは、トークンや個人情報が残らないよう、リリース版では出さないか、伏せ字にします。

共通の処理はインターセプターにAPI の呼び出しgetUser()共通ヘッダー版・言語認証トークン・401ログデバッグ版だけ通信OkHttp・URLSession応答は逆の順に戻る。401 ならトークンを更新して1回だけやり直す、などもここで扱う
共通の処理はインターセプターに置き、各 API の呼び出しには書かない

面接ではこう答える

アプリの通信は iOS では URLSession、Android では OkHttp とその上の Retrofit などを使います。クライアントはアプリで1つにして使い回すと、接続のプールが効いて TLS の接続をやり直さずに済みます。アプリの版や言語のヘッダー、トークンの付与と 401 の扱い、ログのような共通の処理は、各 API の呼び出しではなくインターセプターに集めます。ログにトークンや個人情報を残さないことも大事です。

7.6HTTP キャッシュと ETag

用語★★☆ 重要
ひとことで

Cache-Control で使ってよい期間を決め、ETag で「変わっていなければ 304」を返してもらう。

同じ API を何度も呼ぶアプリでは、HTTP のキャッシュの仕組みを使うと通信量と待ち時間を減らせます。サーバーが返すヘッダーで、取っておいてよいか、どれだけ使ってよいかが決まります。

ヘッダー意味
Cache-Control: max-age=6060秒は取り直さずに手元のものを使ってよい
Cache-Control: no-store保存しない(個人情報・お金の情報)
ETag と If-None-Match中身の版の印。変わっていなければ 304 で中身なし
Last-Modified と If-Modified-Since最終更新時刻での確認

iOS の URLSession は URLCache、Android の OkHttp は Cache を設定すると、これらのヘッダーに従って自動でキャッシュします。ただし、ログイン中の人ごとに違う結果を共有のキャッシュに入れないよう、サーバー側の設定(private など)も一緒に確かめます。オフラインで見せたいデータは、HTTP のキャッシュではなく DB に保存します。

変わっていなければ中身を送らないアプリサーバー1回目:GET /items200 と中身・ETag: "v7"(手元に保存)2回目:If-None-Match: "v7"304 Not Modified(中身なし)
変わっていなければ 304 で中身なしが返り、手元の結果をそのまま使う

面接ではこう答える

HTTP のキャッシュは、サーバーが返すヘッダーで動きます。Cache-Control の max-age の間は取り直さずに手元のものを使い、期限が過ぎたら ETag を If-None-Match で送って、変わっていなければ 304 で中身なしを返してもらいます。URLSession は URLCache、OkHttp は Cache を設定すれば自動で従います。個人ごとに違う結果は private にし、オフラインで見せたいデータは DB に保存します。

ひとことで

オフセット方式は何件目からを指定し、カーソル方式はどこの続きからを指定する。増え続ける一覧はカーソル方式にする。

一覧を少しずつ取るページ送りには、オフセット方式とカーソル方式があります。フィードやチャットのように新しい項目が次々増える一覧では、カーソル方式を選びます。

方式仕組み向き・不向き
オフセット?offset=40&limit=20。何件目から取るかページ番号で飛べる。途中に追加・削除があると重複や抜けが起きる。後ろのページほど DB が遅い
カーソル?after=最後の項目の印&limit=20。どこの続きから取るか増えてもずれない。速さが一定。ページ番号では飛べない

アプリ側では、スクロールの終わりが近づいたら(残り数件で)次を取りにいき、同じカーソルで二重に取りにいかないよう、取得中は次の要求を止めます。新しい項目は「上に引っ張って更新」で先頭側を取り、受け取った項目は ID で重複を消してから一覧に入れます。Android なら Paging ライブラリが、この流れと DB との組み合わせ(RemoteMediator)を用意しています。

新しい投稿が増えると、オフセット方式はずれる1ページ目ABC← この後、先頭に新しい X が増えるoffset=3CDEC がもう一度出る(重複)cursor=CDEFC の続きから取るのでずれない
オフセット方式は間に追加されると同じ項目が2回出る。カーソル方式は続きから取るのでずれない

面接ではこう答える

ページ送りには、何件目から取るかを指定するオフセット方式と、最後に受け取った項目の続きから取るカーソル方式があります。オフセット方式はページ番号で飛べますが、途中に新しい項目が増えると同じ項目が2回出たり抜けたりし、後ろのページほど DB も遅くなります。フィードやチャットのように増え続ける一覧ではカーソル方式にし、アプリ側では取得中の二重の要求を止め、ID で重複を消してから表示します。

7.8API との付き合い方

用語★★☆ 重要
ひとことで

古いバージョンのアプリが残り続けるので、API は後方互換を守り、強制アップデートの仕組みも持つ。

アプリは更新されない古い版が何年も残るので、API は「今の版」だけでなく「過去に出したすべての版」から呼ばれます。サーバーの変更で古いアプリが落ちないようにするのが、アプリと API の付き合い方の基本です。

出したすべての版が、同じ API を呼び続けるアプリ v3.2最新アプリ v2.8アプリ v1.9古いAPI全部の版に応えるアプリ側の決まり知らない項目は無視enum に「不明」を用意強制アップデートの画面最低の版より古ければ
出したすべての版から API が呼ばれる。動かせない版だけ強制アップデートに回す

バージョンごとの挙動

  • 足すのはよい、消す・変えるはだめ:項目を増やすのは安全ですが、名前や型を変える、必須の項目を消すと古い版が壊れます。どうしても変えるなら新しい項目を足し、古い項目は使われなくなるまで残します。
  • アプリ側は知らない項目を無視する:Kotlin の kotlinx.serialization は ignoreUnknownKeys = true にしておきます。Swift の Codable は知らない項目は無視しますが、知らない enum の値が来ると解析ごと失敗するので、「不明」の値を用意して受け止めます。
  • アプリの版を送る:ヘッダー(User-Agent や X-App-Version など)でアプリの版と OS を送ると、サーバーは版ごとに返す内容を変えられ、どの版がまだ使われているかも分かります。

それでも古い版を動かせないときは、強制アップデートで更新してもらいます(次の章で説明)。

Feature Flag と強制アップデート

面接ではこう答える

アプリは更新されない古い版が何年も残るので、API は過去に出したすべての版から呼ばれる前提で後方互換を守ります。項目を足すのは安全ですが、名前や型を変える、必須の項目を消すと古い版が壊れるので、新しい項目を足して古い項目は使われなくなるまで残します。アプリ側は知らない項目や enum の値を受け止められるようにし、ヘッダーでアプリの版を送ってどの版が使われているかを分かるようにします。

7.9REST・GraphQL・BFF

★★☆ 重要

アプリから呼ぶ API の形にもいくつかの選び方があります。アプリならではの視点は、通信の往復の回数と量、そして古い版が残っても変えやすいかです。

形特徴アプリから見た注意点
RESTURL ごとにリソースを返す。HTTP のキャッシュが効きやすい1画面に何本も呼ぶと往復が増える。画面に要らない項目も返る
GraphQLアプリが欲しい項目を問い合わせで指定する取りすぎ・取り足りないを減らせる。キャッシュは専用のライブラリで。重い問い合わせの制限はサーバーで
gRPC型を定義したファイルからコードを作る。データが小さいアプリでも使えるが、ブラウザーやプロキシとの相性で HTTP/JSON を併用することが多い
BFF(Backend For Frontend)アプリ専用の窓口を置き、裏の複数の API をまとめて返す画面に合わせて形を変えやすく、古い版への対応もここで吸収できる

どの形でも、アプリ側ではAPI のモデルと画面のモデルを分けて変換します。API の形が変わっても、変換の部分を直すだけで画面のコードは変わりません。

ひとことで

開いている間は WebSocket や SSE、閉じている間はプッシュ通知。戻ったら再接続して差分を取り直す。

チャットやライブの配信のように、サーバーからすぐ知らせたいときの方式です。アプリでは、開いている間と裏に回っている間で使える方式が違うのがポイントです。

方式仕組み向いている場面
ポーリング数秒ごとに取りにいく更新が少ない・作りを簡単にしたい。電池と通信量は増える
SSE(Server-Sent Events)HTTP の接続を開けたままサーバーから流す通知や進み具合など一方向の更新
WebSocket1本の接続で双方向にやり取りするチャット・共同編集など
プッシュ通知OS の配信の窓口から届くアプリが閉じているとき

アプリが裏に回ると接続は OS に切られるので、戻ってきたら再接続し、切れていた間の差分を取り直す作りにします。再接続は指数バックオフで間をあけ、受け取ったメッセージは ID で重複を消します。WebSocket で受け取ったものだけを頼りにせず、DB と差分の同期を正にするのが安全です。

開いている間と、閉じている間で使い分けるポーリング数秒ごとに取りにいくSSEサーバー → アプリの一方向WebSocket双方向・常時接続アプリが開いている間:上のどれか(チャットなら WebSocket)閉じている間:プッシュ通知裏に回ると接続は切られる前提。戻ったら再接続して、切れていた間の差分を取る
アプリが開いている間は WebSocket、閉じている間はプッシュ通知で知らせる

面接ではこう答える

サーバーからすぐ知らせたいときは、更新が少なければポーリング、一方向なら SSE、チャットのような双方向なら WebSocket を使います。ただしアプリが裏に回ると接続は切られるので、閉じている間はプッシュ通知で知らせ、戻ったら再接続して切れていた間の差分を取り直します。受け取ったメッセージは ID で重複を消し、DB と差分の同期を正にしておくと、取りこぼしがあっても表示が正しくなります。

ひとことで

サーバーはキャッシュ・レート制限・セッションに Redis を使う。古いデータ・429・突然のログアウトの裏側を知っておく。

アプリから直接 Redis につなぐことはありませんが、API サーバーの裏側でよく使われています。概要を知っておくと、サーバー側のエンジニアとの会話や、不具合の切り分けが速くなります。

アプリで見える現象裏側の Redis
更新したのに、少しの間古い情報が返るAPI の結果のキャッシュ(期限が切れるか、消されるまで古いまま)
429 Too Many Requests が返るレート制限のカウンター。Retry-After を見て待ってから再試行する
突然ログアウトされるセッション・トークンの保存先。期限切れや、メモリ不足で消えた可能性
ランキング・既読数がすぐ反映されるソート済みセット・カウンター
プッシュ通知・メールが少し遅れて届くジョブのキュー(ワーカーが順番に処理する)

アプリ側では、古いデータが返ることがある前提で表示を作り(引っ張って更新・最終更新時刻)、429 や 503 には待ってから再試行(指数バックオフ)で応えます。

面接ではこう答える

アプリから直接 Redis につなぐことはありませんが、API サーバーの裏ではキャッシュ、レート制限、セッション、ジョブのキューなどに Redis がよく使われています。更新したのに少しの間古い情報が返るのはキャッシュ、429 が返るのはレート制限、突然ログアウトされるのはセッションの期限切れや消失、といった裏側を知っておくと、サーバー側との会話や不具合の切り分けが速くなります。アプリでは古いデータを前提にした表示と、429 や 503 への待ってからの再試行で応えます。

別のページで開く →

DBラボを読み込んでいます…

ひとことで

TLS は必須。証明書ピンニングは強いが、更新で通信できなくなるリスクもある。

アプリの通信は、OS の設定で「暗号化されていない通信を禁止する」のが基本です。iOS と Android でそれぞれ仕組みがあります。

観点iOS:ATS(App Transport Security)Android:Network Security Config
既定HTTPS(TLS 1.2 以上など)が必須。HTTP は止められるAndroid 9(API 28)以降を target にすると平文の HTTP は既定で禁止
例外の書き方Info.plist の NSAppTransportSecurity でドメインごとに例外res/xml/network_security_config.xml でドメインごとに設定
注意すべてを許す NSAllowsArbitraryLoads は審査で理由を聞かれるAndroid 7(API 24)以降を target にすると、利用者が入れた証明書は既定で信頼しない
偽の証明書で中継されても、検証で接続を切るアプリ盗聴する中継偽の証明書本物のサーバー接続偽の証明書を見せる① OS の検証(ATS・NSC)信頼できる CA の証明書かAndroid は利用者が入れた CA を既定で信頼しない② ピン留め(必要なときだけ)CA が正しくても公開鍵が違えば切る予備の鍵がないと鍵の更新で全員つながらないまずは①を緩めない(平文の HTTP を許さない・例外は最小限)。②は効果と事故の危険をはかって決める
偽の証明書を使う中継は、OS の検証とピン留めで止める

開発中にプロキシで通信を見たいときは、Android ならデバッグ版だけで利用者の証明書を信頼する設定(debug-overrides)を使い、リリース版に例外を残さないようにします。

(SRE)暗号と証明書

面接ではこう答える

アプリの通信は、OS の設定で暗号化されていない通信を禁止するのが基本です。iOS の ATS は HTTPS を必須にし、例外は Info.plist でドメインごとに書きます。Android は 9 以降を target にすると平文の HTTP が既定で禁止され、Network Security Config でドメインごとに設定し、7 以降を target にすると利用者が入れた証明書は既定で信頼しません。開発中に通信を見たいときはデバッグ版だけの設定にして、リリース版に例外を残さないようにします。

7.13証明書のピン留め

用語★★☆ 重要
ひとことで

「この公開鍵の相手とだけ話す」とアプリに決めておく守り。予備の鍵を入れないと、証明書の変更で古い版が通信できなくなる。

OS の標準の検証より一段強い守りが、証明書のピン留めです。強い反面、運用を誤るとアプリが一切通信できなくなるので、入れるかどうかを慎重に決めます。

予備の鍵を一緒に入れておくアプリのピン今の公開鍵予備の公開鍵今のサーバー証明書鍵を変えた後一致予備と一致予備がないと古い版は一切通信できないAndroid ではピンに期限(expiration)を付け、期限後は通常の検証に戻す
今の鍵と予備の鍵の両方を入れておけば、証明書を変えても古いアプリが通信できる

ピン留めは、「この公開鍵を持つ証明書の相手とだけ話す」とアプリに決めておく方法です。不正な証明書を使った盗聴(中間者攻撃)への守りが強くなります。iOS は Info.plist の NSPinnedDomains、Android は Network Security Config の <pin-set> で書けます。

ただし、サーバーの証明書を変えたときにピンが合わないと、古い版のアプリが一切通信できなくなる危険があります。入れるなら、証明書そのものではなく公開鍵を留め、予備の鍵を必ず一緒に入れ、Android では期限(expiration)を付けます。銀行など特に守りたいアプリ以外では、OS の標準の検証だけで十分なことも多いです。

面接ではこう答える

証明書のピン留めは、特定の公開鍵を持つ相手とだけ通信するとアプリに決めておく方法で、不正な証明書を使った中間者攻撃に強くなります。ただし、サーバーの鍵を変えたときにピンが合わないと、古い版のアプリが一切通信できなくなります。入れるなら証明書ではなく公開鍵を留め、予備の鍵を一緒に入れ、Android では期限を付けます。銀行のように特に守りたいアプリ以外では、OS の標準の検証で十分なことも多いです。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • 不安定な回線を前提にしたタイムアウト・リトライ・冪等性
  • 通信量と電池への配慮

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1電車の中で使われるアプリの通信で気をつけることは?

模範回答

電波が途切れたり、Wi-Fi と 4G が切り替わったりする前提で作ります。まずタイムアウトを接続と全体で分けて適切に決め、長く待たせずに読み込み中と再試行のボタンを出します。自動の再試行は、指数バックオフにジッターを加えて回数の上限を決め、429 や 503 なら Retry-After に従います。

再試行するのは安全なものだけです。GET はそのまま、投稿や決済のような POST には冪等キーを付けて、応答が届かず再送してもサーバーで二重に処理されないようにします。さらに、読む画面は端末の DB やキャッシュから先に表示し、書く操作は送信待ちのキューに積んで、つながったら送ります。画像を表示する大きさで取るなど通信量を減らすことと、HTTP/3 が使えれば回線が変わっても接続を保てることも効きます。

深掘りされたら

  • 冪等キーはどこで作り、いつまで有効にしますか?
  • オフラインのときの画面はどう見せますか?

見られているポイントタイムアウト・リトライ・冪等性・オフライン

Q2指数バックオフとジッターとは何ですか? なぜ必要ですか?

模範回答

指数バックオフは、失敗したら待つ時間を 1秒・2秒・4秒と倍にしながら再試行するやり方です。すぐに何度も再試行すると、弱っているサーバーにさらに負担をかけてしまうからです。ジッターは、その待つ時間をランダムにずらすことです。

ずらさないと、障害が直った瞬間に何万台もの端末が同じタイミングで再試行し、サーバーがまた落ちることがあります。モバイルは利用者が多いので特に効きます。あわせて、回数の上限を決めて「再試行」ボタンに切り替えることと、429 や 503 の Retry-After があればそれに従うことも大事です。

深掘りされたら

  • 再試行してはいけない通信は?
  • 上限の回数や時間はどう決めますか?

見られているポイントサーバーを守るという目的まで説明できるか

Q3API に項目を追加・変更するとき、古いアプリを壊さないためには?

模範回答

基本は「足すのはよい、消す・変えるはだめ」です。項目を足すのは安全ですが、名前や型を変えたり必須の項目を消したりすると、古い版のアプリの解析が失敗します。変えたいときは新しい項目を足し、古い項目は使っている版がなくなるまで残します。

アプリ側も、知らない項目を無視する設定にし、enum には「不明」の値を用意しておきます。Swift の Codable は知らない enum の値で解析ごと失敗するからです。アプリの版をヘッダーで送ってもらえば、サーバーは版ごとに返す内容を変えられ、どの版がまだ使われているかも分かります。

深掘りされたら

  • API のバージョンを URL に付けるのはどうですか?
  • 古い項目をいつ消してよいと判断しますか?

見られているポイント古い版が残る前提での互換性の考え方

Q4HTTP/3(QUIC)はモバイルにどんな利点がありますか?

模範回答

一番の利点は、回線が変わっても接続を保てることです。TCP は IP アドレスが変わると切れますが、QUIC は接続 ID で相手を見分けるので、Wi-Fi から 4G に切り替わっても通信を続けられます。また、暗号化の準備と接続を一緒に行うので、接続までの往復が少なく、電波の弱い場所で効きます。

さらに、1つのパケットの取りこぼしで同じ接続の通信全部が止まる、TCP の問題も起きにくくなります。iOS の URLSession はサーバーが対応していれば自動で使います。ただし UDP が通らない回線もあるので、HTTP/2 に戻れる前提で使います。

深掘りされたら

  • QUIC が UDP の上で動くのはなぜですか?
  • 0-RTT の注意点は?

見られているポイントモバイルならではの利点を挙げられるか

Q5証明書のピン留めは入れるべきですか?

模範回答

まず、入れなくても OS の標準の検証と、ATS や Network Security Config で平文の通信を禁止することで、普通の盗聴はかなり防げます。ピン留めは、それでも足りない、例えば金融のように特に守りたいアプリで検討します。

入れる場合の一番の危険は、サーバーの証明書を変えたときにピンが合わず、古い版のアプリが一切通信できなくなることです。証明書ではなく公開鍵を留め、予備の鍵を必ず入れ、Android では期限も付けます。証明書を更新する手順にアプリのピンの確認を組み込むことも必要です。効果と事故の危険を比べて決めます。

深掘りされたら

  • ピン留めをすると開発中のデバッグはどうなりますか?
  • ピンが合わなくなったら、どう復旧しますか?

見られているポイント効果と運用の危険を比べて判断できるか

Q6電車の中など回線が不安定な場所でも使いやすくするには、どう設計しますか?

模範回答

通信は失敗するものとして、3つを組み合わせます。1つ目は、前回取ったデータを端末の DB に保存し、画面を開いたらまずそれを表示して、裏で取り直して差し替えることです。失敗しても画面を空にせず、「最終更新の時刻」や「オフライン」を小さく出します。

2つ目は通信そのものの扱いで、タイムアウトを決め、GET や冪等キー付きの POST だけを指数バックオフで再試行します。3つ目は送る側で、いいねなどは楽観的に画面を先に変え、送信待ちのキューに入れてつながったら送ります。例えば在庫や決済のように古い値で困るものは、操作の直前にサーバーで確かめ、ログアウトのときはキャッシュを消します。

深掘りされたら

  • キャッシュが古すぎるとき、どう扱いますか?
  • 楽観的な更新が失敗したとき、利用者にどう見せますか?

見られているポイントキャッシュを先に見せる・再試行・送信待ちのキューを組み合わせられるか。古いデータで困る場面やログアウト時の削除まで考えているか。