【入門編】 Last-ModifiedヘッダーとIf-Modified-Sinceによる時刻ベースのキャッシュ検証 – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!ネットワークプロトコルの深淵から、今日もパケットの鼓動を聴いているエンジニアの私です。

皆さんは、Webサイトやアプリが「シュパッ!」と高速に表示される裏側で、どんなやり取りが行われているか考えたことはありますか?実はそこには、無駄な通信を極限まで削ぎ落とそうとする、先人たちの知恵が詰まった「思いやりのプロトコル」が流れているんです。

今回は、REST APIの設計において非常に重要でありながら、意外と「なんとなく」で済まされがちな「時刻ベースのキャッシュ検証」についてお話しします。

Last-Modified(最後にいつ直した?)と If-Modified-Since(その時より新しくなってる?)という、まるで郵便配達員さんと受取人のような、息の合ったやり取りの世界へ、一歩ずつ足を踏み入れてみましょう!

—

1. なぜ「キャッシュ検証」が必要なの?

例えば、あなたが毎日チェックする「お天気情報」のAPIがあるとします。

もし、データが1時間前と全く変わっていないのに、毎回すべてのデータを律儀にダウンロードしていたらどうでしょう?スマートフォンのギガ(通信量)ももったいないですし、サーバー側も「さっきと同じだよ!」と叫びたくなりますよね。

そこで登場するのが「キャッシュ検証」です。

1. クライアント(あなた): 「このデータ、いつのやつ?」
2. サーバー: 「これは 10月10日の12:00 に更新したやつだよ(Last-Modified)」
3. クライアント(次回のアクセス): 「手元に 10月10日の12:00 のがあるけど、これより新しいのはある?(If-Modified-Since)」
4. サーバー: 「あ、それ最新だから送らなくていいや! 304 Not Modified(変更なし)!」

このように、「持っているものが最新なら、中身を送らずに『最新だよ』という合図だけ送る」ことで、通信を劇的に軽くする仕組みなんです。

—

2. 登場人物は2つのヘッダー

この仕組みを支えるのは、HTTPヘッダーというお手紙の「宛名書き」のような部分に書かれる2つの項目です。

① Last-Modified(サーバーからの返事)

サーバーがリソース(データ)を返すときに、「このデータは 2023-10-10 12:00:00 に最後に更新されましたよ」と教えてくれる日付スタンプです。

② If-Modified-Since(クライアントからのお願い)

次に同じデータが欲しくなったとき、クライアントは「もし 2023-10-10 12:00:00 より新しいデータがあるなら送って。なければ『なし』って教えて!」と条件付きのリクエストを送ります。

—

3. 実際のやり取りを覗いてみよう(コード例)

では、実際にどのようなパケット(データ)が流れているのか、シンプルなPython(Flask)のコードで再現してみましょう。

from flask import Flask, request, make_response
from datetime import datetime, timedelta

app = Flask(__name__)

# 仮のデータの最終更新日時(2023年10月10日 12:00:00 GMT)
LAST_UPDATED = datetime(2023, 10, 10, 12, 0, 0)

@app.route('/api/weather')
def get_weather():
    # 1. クライアントが送ってきた「If-Modified-Since」ヘッダーを取得
    if_modified_since = request.headers.get('If-Modified-Since')

    # 2. もしヘッダーがあれば、手元の更新日時と比較する
    if if_modified_since:
        # 文字列の日付をプログラムで扱える形に変換(簡略化しています)
        client_date = datetime.strptime(if_modified_since, '%a, %d %b %Y %H:%M:%S GMT')
        
        # もしクライアントの持っている日時が、サーバーの更新日時と同じ(または新しい)なら
        if client_date >= LAST_UPDATED:
            # 「304 Not Modified」を返し、中身(Body)は空にする!
            return '', 304

    # 3. 新しいデータが必要な場合、または初回アクセスの場合は通常通り返す
    response_data = {"status": "晴れ", "temp": 25}
    response = make_response(response_data)
    
    # サーバーの最終更新日時を「Last-Modified」としてセット
    response.headers['Last-Modified'] = LAST_UPDATED.strftime('%a, %d %b %Y %H:%M:%S GMT')
    
    return response

if __name__ == '__main__':
    app.run(debug=True)

このコードのポイントは、データが更新されていなければ 304 というステータスコードだけを返して、大きなデータ本体を送っていないという点です。これがネットワークを軽くする魔法の正体です。

—

4. 知っておきたい「ETag」との優先順位

キャッシュ検証にはもう一つ、ETag(エンティティ・タグ)という「指紋」のような仕組みもあります。

  • Last-Modified: 「時刻」で判断する
  • ETag: 「データそのもののハッシュ値(指紋)」で判断する

もし両方のヘッダーが存在する場合、HTTPのルール(RFC)では、より厳密な ETag による検証が優先される ことが一般的です。

理由は簡単です。時刻だと「1秒以内に行われた2回の更新」を見逃してしまう可能性がありますが、指紋(ETag)なら1ビットでもデータが変われば確実に検知できるからです。

—

5. 現場の落とし穴:システムクロックの同期

ここで、インフラエンジニアとして絶対に避けて通れない「泥臭いけれど超重要な話」をします。それは「時刻のズレ」です。

Webサーバーが複数台ある環境で、もしサーバーAの時計が5秒進んでいて、サーバーBの時計が5秒遅れていたらどうなるでしょうか?

1. クライアントがサーバーAから「12:00:05」の Last-Modified を受け取る。
2. 次にサーバーBに「12:00:05より新しいのはある?」と聞く。
3. サーバーBは自分の時計が遅れているので、「え、俺の持ってるのは12:00:00(※時計が遅れているため)だから、お前の持ってるやつの方が新しいよ!」と勘違いして 304 を返してしまう。

本当はデータが更新されているのに、古いデータを使い続けてしまう……そんな恐ろしい事態を防ぐためにも、サーバー間の時刻同期(NTPなど)は、API設計以前の「鉄則」となるわけです。

—

まとめ:美しい設計は「優しさ」から生まれる

Last-Modified と If-Modified-Since を使った設計は、一見地味かもしれません。しかし、これがあるおかげで、世界中のネットワーク帯域が守られ、ユーザーの貴重な待ち時間が削減されています。

APIのエンドポイントを設計する際は、単にデータを返すだけでなく、「このデータはいつ更新されたものか?」「相手は既に持っていないか?」という視点を持ってみてください。その「優しさ」が、プロフェッショナルなAPIへと繋がります。

一歩ずつ、プロトコルの奥深さを楽しんでいきましょう。パケットの向こう側にいるユーザーの笑顔のために!

それでは、また次回の解説でお会いしましょう。ハッピー・パケット・トラベリング!

コメント

タイトルとURLをコピーしました