Skip to content

필사 모드: タイムスタンプとタイムゾーンを正しく扱う

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに — 時間はなぜこんなに難しいのか

時間を扱うコードは、一見かんたんそうに見えます。現在時刻を取得して、保存して、画面に表示すれば終わり——のように思えます。ところがある日、バグ報告が届きます。「予約が1時間ずれます」「レポートの日付が1日前になっています」「特定のユーザーだけログイン時刻がおかしい」。こうしたバグのほとんどは、時間を素朴に扱った代償です。

時間が難しいのは、それが純粋な物理量ではなく、人間の取り決めが幾重にも重なった概念だからです。地球の自転、標準時の政治的な境界、サマータイム、うるう秒、歴史的なオフセット変更まで。私たちが「ただの時間」と呼ぶものの裏には、驚くほど複雑な規則があります。

幸い、この複雑さを飼いならす堅牢な原則があります。この記事はその原則を一つずつ整理します。一行にまとめるとこうです。保存はUTCで、表示はローカルで。 残りは、この原則をなぜ、どう守るかという話です。概念を実際に確かめたいなら、このサイトのUnixタイムスタンプ変換UTC変換世界時計を一緒に開いて読むとよいでしょう。

大原則 — UTCで保存し、ローカルで表示する

時間を扱ううえで最も重要な規則は、保存と表示を分離することです。

  • 保存: 常にUTC(協定世界時)で保存します。UTCは地域やサマータイムに揺らがない単一の基準線です。
  • 表示: ユーザーに見せるときだけ、そのユーザーのタイムゾーンへ変換します。

なぜ保存をUTCにすべきなのでしょうか。もし「ソウル時刻 2026-06-14 15:00」のように地域時刻をそのまま保存すると、その値は単独では立てません。サマータイムのある地域なら、同じ壁時計の時刻が年に二度現れたり、まったく存在しなかったりしますし、サーバーとクライアントが別の地域なら解釈が割れます。UTCで保存すれば、こうした曖昧さがすべて消えます。UTCの特定の瞬間は、地球上のどこであっても、ただ一つの絶対的な瞬間を指します。

表示はその逆です。ユーザーはUTCを求めていません。ソウルのユーザーは韓国時刻を、ニューヨークのユーザーは東部時刻を見たいのです。だから保存されたUTCを画面に描くときだけ、ユーザーのタイムゾーンへ変換します。保存レイヤーは一つの真実を、表示レイヤーは複数のローカライズされたビューを持つ構造です。

epoch vs ISO 8601 — 二つの表現

時間をUTCで保存すると言うとき、実際にはどの形式で保存するのでしょうか。大きく二つの方式があります。

Unixタイムスタンプ(epoch)。 1970年1月1日00:00:00 UTCから流れた秒(またはミリ秒)の数です。たとえば1749884400のような一つの整数です。

1749884400  →  2025-06-14T07:00:00Z (UTC)

利点は明快です。タイムゾーンがまったくない、純粋な絶対の瞬間です。比較と計算が整数演算で済み、保存領域も小さくて済みます。欠点は、人間が読めないことです。

ISO 8601文字列。 2025-06-14T07:00:00Zのような、人間が読める標準形式です。末尾のZは「Zulu」、すなわちUTCを意味します。オフセットを明示するには2025-06-14T16:00:00+09:00のように書きます。

2025-06-14T07:00:00Z         ← UTC (Z = +00:00)
2025-06-14T16:00:00+09:00    ← 同じ瞬間をソウルのオフセットで表記

二つの文字列は同じ絶対の瞬間を指します。ISO 8601の利点は可読性とオフセットの明示です。ログ、APIレスポンス、人が読むデータにはISO 8601がよく、内部計算と保存効率が重要ならepochが便利です。大切なのは、どちらであってもタイムゾーン情報を失わないことです。2025-06-14 16:00:00のようにオフセットもZもない「naive」な文字列は、それ自体が曖昧で、時間バグの常連の原因です。

オフセット ≠ タイムゾーン — 最もよくある誤解

ここで、時間を扱ううえで最も重要な概念的区別が出てきます。オフセットとタイムゾーンは違います。

  • オフセット(offset): UTCからの時差。+09:00-05:00のような単純な数値です。特定の瞬間の状態にすぎません。
  • タイムゾーン(time zone): ある地域が時間をどう決めるかという規則の集合。Asia/SeoulAmerica/New_Yorkのような名前で識別されます。

なぜこの区別が重要なのでしょうか。オフセットは時間とともに変わるからです。America/New_Yorkは冬は-05:00(EST)ですが、夏はサマータイムのため-04:00(EDT)です。つまり「ニューヨーク時刻」をオフセット一つに固定するのは誤りです。オフセットは瞬間ごとに変わり、その変化の規則を担っているのがタイムゾーンです。

America/New_York というタイムゾーン:
  2025-01-15  →  オフセット -05:00 (EST, 冬)
  2025-07-15  →  オフセット -04:00 (EDT, 夏, サマータイム)

実務的な含意は大きいです。未来の会議を保存するとき、オフセットだけを保存してはいけません。いまソウルが+09:00だからといって、半年後の会議を+09:00で固めてしまうと、その間に国がサマータイム政策を変えれば会議の時刻がずれます。未来のイベントはオフセットではなくタイムゾーン名(Asia/Seoul)とともに保存してこそ、規則が変わっても正しい壁時計の時刻に再計算されます。

IANA tzデータベース — 世界の時間規則の辞書

タイムゾーンが規則の集合なら、その規則はどこにあるのでしょうか。IANAタイムゾーンデータベース(tzデータベース、別名zoneinfo、Olsonデータベース)です。これは世界のあらゆる地域の時間規則を収めた、継続的に更新される公開データベースです。

このデータベースが収めているものは驚くほど膨大です。現在のオフセットとサマータイムの規則だけでなく、歴史的な変更まで記録します。たとえばある国がいつサマータイムを導入して廃止したか、いつ標準時のオフセットそのものを変えたかが入っています。だからAsia/Seoulで1988年のある瞬間を計算すると、その年ソウルにサマータイムがあったという歴史的事実まで反映されます。

核心の教訓はこれです。タイムゾーンの規則を自分でハードコードするな。 「韓国はUTC+9」とコードに埋め込むのは、いまは正しくても未来を保証しませんし、サマータイムのある地域では今でも誤りです。代わりに、OSと言語ランタイムが提供するIANAデータベースに基づくライブラリを使い、そのデータベースを最新に保つべきです。国は思っているより頻繁に時間政策を変え、そのたびにtzデータベースが更新されます。

サマータイムの地獄 — 存在しない時刻と二度ある時刻

サマータイム(DST)は時間バグの最も肥沃な土壌です。問題の本質は、サマータイムの切り替えの瞬間に、壁時計の時刻が連続でないことです。

春 — 消える時刻(gap)。 サマータイムが始まるとき、時計は一時間前へジャンプします。たとえば午前2時になる瞬間、ただちに3時になります。その結果、その日その地域には午前2時30分という時刻が存在しません。

春の切り替え (サマータイム開始):
  01:59:59  →  03:00:00   (2時台がまるごと消える)
  「02:30」はその日存在しない時刻

もしユーザーが毎日02:30に鳴るアラームを設定していたら、サマータイム開始日にそのアラームはいつ鳴るべきでしょうか。これは正解のない曖昧な状況で、ライブラリや方針ごとに扱いが違います。

秋 — 重なる時刻(overlap)。 サマータイムが終わるとき、時計は一時間後ろへ戻ります。その結果、午前1時30分という壁時計の時刻がその日二度現れます。

秋の切り替え (サマータイム終了):
  01:59:59 (サマータイム)  →  01:00:00 (標準時)   (1時台が繰り返す)
  「01:30」はその日二度存在する時刻

このとき「01:30に起きたこと」というログは、二つのうちどちらの01:30なのか曖昧です。オフセットがなければ、二つの瞬間を区別できません。この二つの現象(gapとoverlap)こそ、地域時刻をそのまま保存してはいけない決定的な理由です。UTCで保存すれば、こうした曖昧さがそもそも生じません。UTCにはサマータイムがないからです。

実話 — サマータイム切り替え時刻に走ったバッチ

ある決済システムが、毎日午前2時30分に精算バッチを回していました。地域時刻基準でした。ふだんは何の問題もありませんでしたが、春のサマータイム切り替え日にその時刻が存在しなくなると、バッチはその日実行されませんでした。一日分の精算が欠落し、しかも誰もエラーログを見ませんでした。スケジューラーからすれば「そんな時刻は来なかった」だけだからです。

教訓は二つです。第一に、繰り返しスケジュールはサマータイム切り替え帯(たいてい午前1〜3時)を避けるのが安全です。第二に、スケジューラーが地域時刻を使うのかUTCを使うのかを必ず確認すべきです。多くの事故は「地域時刻で深夜に回るバッチ」から生まれます。

うるう秒 — 1分が61秒になる瞬間

時間についてのもう一つの迷信は「1分は常に60秒」というものです。事実ではありません。地球の自転は完璧に規則的ではないので、原子時計に基づく時間と、自転に基づく時間のあいだに微細な誤差が積もります。それを合わせるために、ときおり**うるう秒(leap second)**を挿入します。そんな日には、一日の最後の1分が61秒になります。

うるう秒の挿入時:
  23:59:58
  23:59:59
  23:59:60   ← 存在する時刻! (ふだんはない60秒)
  00:00:00

ほとんどのアプリケーションは、うるう秒を直接扱うことはありません。Unix時間そのものが、うるう秒を「無視」するように定義されているからです。しかしうるう秒は実際に事故を起こしてきました。過去にいくつかの大規模システムが、うるう秒の挿入の瞬間に時計がもつれて障害に見舞われました。近年ではこの問題のため、うるう秒をシステムになめらかに「塗り広げる」(leap smearing)手法が広く使われます。一日かけてほんの少しずつ時計を遅らせ、61秒の1分を避けるのです。要点は、「1秒は常に1秒で、1分は常に60秒」という仮定が物理的に誤りうると知ることです。

クライアントの時計を信用するな

分散システムで必ず刻むべき原則があります。クライアントの時計を信用するな。 ユーザーの端末の時計は狂っているかもしれません。バッテリーが切れて起動したばかりかもしれず、ユーザーがわざと変えたかもしれず、ただ数分ずれているだけかもしれません。

これが問題になる状況は数多くあります。

  • セキュリティトークンの失効: 失効の判定をクライアントの時計で行うと、時計を操作して失効を回避できます。失効はサーバーの時刻で判定すべきです。
  • イベントの順序: 複数の端末が送ったイベントをクライアントのタイムスタンプで並べると、時計がずれた端末のせいで順序が乱れます。
  • 先着順の処理: クーポンの先着のようなロジックをクライアントの時刻で判定すると公平ではありません。

権威ある時刻はサーバーが定めます。サーバー同士もNTP(Network Time Protocol)で時計を合わせますが、これも完璧ではないので、精密な順序が必要なら、単調増加のシーケンスや論理時計(logical clock)のような別の手段を使います。そして、サーバーの時計でさえNTP同期のためにときおり後ろへジャンプしうることを覚えておくべきです。

NTPと巻き戻る時計 — 時間は前へだけ進むわけではない

いま述べたとおり、時計は常に前へ進むわけではありません。サーバーはNTPで正確な時刻に合わせますが、もしローカルの時計が実際より進んでいたら、NTPは時計を後ろへ調整します。その瞬間、コードからすれば時間が逆に流れたように見えます。

なぜこれが危険なのでしょうか。経過時間を測る素朴なコードを考えてみましょう。

start = now()          // 壁時計の時刻
... 作業を実行 ...
elapsed = now() - start

もしその間にNTPが時計を後ろへ引くと、elapsedが負になりうります。タイムアウトのロジックなら、ただちに失効するか永遠に失効しないバグが生じます。だから経過時間の測定には壁時計ではなく単調時計(monotonic clock)を使うべきです。 単調時計は決して後ろへ進まず、経過量だけを測ります。壁時計は「いま何時か」に、単調時計は「どれだけ経ったか」に使うのが原則です。

日付 vs 瞬間 — 誕生日にタイムゾーンはない

もう一つ重要な区別は、「日付」と「瞬間」が別の種類の値だということです。

  • 瞬間(instant): 時間軸上の一点。「2025-06-14T07:00:00Z」のように絶対的で、世界共通です。イベントの発生時刻やログのタイムスタンプがここに属します。
  • 日付のみ(date-only / civil date): タイムゾーンと無関係な暦上の日。誕生日、祝日、契約満了日といったものです。「6月14日」はソウルでもニューヨークでも同じ6月14日です。

この二つを混同するとバグが出ます。誕生日を瞬間(たとえば2000-06-14T00:00:00Z)として保存すると、サマータイムやタイムゾーン変換の過程で一日ずれ、「誕生日が一日ずれる」という古典的なバグが生じます。ユーザーが西のタイムゾーンにいると、UTCの真夜中がその地域では前日になるからです。

原則はこうです。特定の瞬間を扱うならUTCの瞬間として、暦上の日付を扱うならタイムゾーンのない日付型(LocalDatedateなど)として保存してください。「これは絶対の瞬間なのか、それとも暦の日なのか」をまず問うことで、正しい型を選べます。

実務チェックリスト

ここまでの原則を実務の規則に圧縮します。

  • 保存はUTC、表示はローカル。 データベースとAPIの内部はUTCの瞬間に統一します。
  • naiveなdatetimeを保存するな。 オフセットもZもない時刻は曖昧です。常にタイムゾーン情報を一緒に保管します。
  • 未来のイベントはタイムゾーン名とともに保存せよ。 オフセットだけの保存は規則変更に弱いです。
  • タイムゾーンの規則をハードコードするな。 IANA tzデータベースに基づくライブラリを使い、最新に保ちます。
  • 経過時間は単調時計で測れ。 壁時計はNTPで後ろへ進みうります。
  • クライアントの時計を信用するな。 失効・順序・公平性はサーバーの時刻で判定します。
  • 日付と瞬間を区別せよ。 誕生日と祝日はタイムゾーンのない日付で。
  • 繰り返しスケジュールはサマータイム切り替え帯を避けよ。 午前1〜3時は危険区間です。

おわりに

時間を扱うことは、物理と人間の取り決めが交わる地点を扱うことです。だから素朴な直観がしばしば裏切ります。一日が常に24時間であることも、1分が常に60秒であることも、時計が前へだけ進むことも、タイムゾーンが固定オフセットであることも、事実ではありませんでした。

しかしこの複雑さの前で私たちがつかむべき原則は、意外にも単純です。保存はUTCで、表示はローカルで、未来のイベントはタイムゾーン名とともに、経過の測定は単調時計で、そしてクライアントの時計は信用しないこと。これらの原則を守れば、時間関連のバグの大多数を予防できます。概念を自分で触ってみたいなら、Unixタイムスタンプ変換UTC変換世界時計で値がどう変換されるかを確かめてみてください。

参考資料

현재 단락 (1/81)

時間を扱うコードは、一見かんたんそうに見えます。現在時刻を取得して、保存して、画面に表示すれば終わり——のように思えます。ところがある日、バグ報告が届きます。「予約が1時間ずれます」「レポートの日付が...

작성 글자: 0원문 글자: 7,275작성 단락: 0/81