電話番号の保存形式の不一致に起因するログイン失敗が、一つの欠陥を露呈させました。あるユーザーのアカウントがブロックされた原因は、データベース内に同じドイツの携帯電話番号が2つの形式で保持されていたことでした。1つの行には 0171 5550134、別の行には +49 171 5550134 と記録されており、システムがこれらを別々のエントリとして扱ってしまったのです。その結果、OTP(ワンタイムパスワード)が届かず、「コードを送信」ボタンを押すことがギャンブルのような状態になってしまいました。
なぜ電話番号は日付よりも扱いが難しいのか
開発者は電話番号の入力を制御するために、しばしば正規表現(regex)を信頼します。しかし、その信頼は番号が国境を越えたり、国内のプランが変更されたりした瞬間に崩れ去ります。日付は予測可能なカレンダーに従いますが、電話番号は通信事業者、規制、文化的な慣習によって変化し続けるのです。
生の文字列(raw strings)に潜むコスト
電話番号を単なる文字列として保存しておけば、一見ユニークに見えます。しかし、2つの表現が同じ番号を指していることが判明するまでは、です。データベースに対して「その」番号を照会するOTPシステムは、結局、ユーザーに届くことのない重複した番号にコードを送信してしまうことになります。
問題は、番号が数値フィールドに格納されている場合に悪化します。BIGINT 型の列は、先頭の + や先頭のゼロを削除してしまい、+49 171 5550134 を 491715550134 に変えてしまいます。プラス記号がなければ、元の形式を復元することは推測の域を出ません。
E.164 という規約
国際電話番号計画である E.164 は、単一のポータブルな表現形式を定義しています。
+で始まる- 1〜3桁の国番号が続く
- その後に加入者番号が続く
- 合計で15桁以内
- スペース、ドット、ハイフンは使用しない
E.164 は、その番号が「有効(active)」であることを保証するものではありません。あくまで、文字列が有効な構造パターンに従っていることを保証するものです。E.164 は、到達可能性を判定する神託(oracle)ではなく、フォーマットの規約として扱うべきです。
正規表現では解決できない一般的な落とし穴
- 数値による保存 –
BIGINTは+や先頭のゼロを破棄します。代わりにテキスト型の列(TEXTまたはVARCHAR)を使用してください。 - ハードコードされた正規表現 – 国内のプランは変わります。メキシコは2019年にトランクプレフィックスを廃止しました。アルゼンチンでは現在、携帯電話回線の国番号の後に
9が必要です。静的なパターンはすぐに時代遅れになります。 - 無差別なゼロの削除 – イタリアの固定電話は先頭のゼロを維持しますが、ドイツの場合はそうではありません。「先頭のゼロを削除する」という一律のルールを適用すると、ドイツの番号はそのままですが、イタリアのデータが破損してしまいます。
- フォーマットが届くことを保証すると誤解すること –
libphonenumberは構造を検証しますが、端末の電源が入っているか、あるいは番号がMNP(番号ポータビリティ)されているかどうかまでは判断できません。
信頼性の高いパイプラインの構築
- 国を選択させる – サインアップフォームに国選択機能を追加し、その地域をパーサーに渡します。
- リアルタイムのフォーマット表示 – 「AsYouType」フォーマットを使用し、ユーザーが入力している最中に正しいパターンが見えるようにします。
- フォーカスが外れた時に検証する(Validate on blur) – キー入力のたびに検証するのではなく、ユーザーがフィールドを離れた後に検証を実行します。これにより、入力の摩擦を軽減できます。
- E.164 文字列のみを永続化する – 正規化された
+プレフィックス付きの番号をデータベースに保存します。 - エッジでフォーマットする – 人間に読みやすいレイアウトへの変換は、UIやメールテンプレート内でのみ行います。
レガシーデータを移行する場合は、検証に失敗した行をそのまま残しておいてください。既存の各エントリを新しい列にパースし、失敗したものにフラグを立ててレポートを作成します。サイレントにデータを破棄してしまうと、後にコストのかかる修正作業が必要となるサポートチケットが発生することになります。
開発者のためのクイックチェックリスト
- 番号は E.164 形式の
TEXT/VARCHARとして保存する。 - Google の libphonenumber ライブラリを使用する。これにより、世界中の複雑なプランの実態に対応できます。
- 国番号を省略したユーザーのために、デフォルトの地域を設定する。
- 新規登録時には
is_valid_numberを呼び出す。これにより、番号が地域のルールに適合しているかを確認できます。 - バルクデータのクリーニング時には
is_possible_numberを使用する。これにより、境界線上のケースを拒否することなく、明らかに形式が崩れたエントリを捕捉できます。
適切な電話番号の扱いは、「あれば良いもの(nice-to-have)」ではなく、信頼できるユーザー連絡手段に依存するあらゆるシステムにとっての「前提条件」です。E.164 に正規化し、パースを実績のあるライブラリに委ねることで、開発者は信頼と収益を静かに蝕む一連のバグを排除できます。電話番号を自由形式のテキストではなく、構造化されたデータとして扱い、標準規格に重労働を任せましょう。
