전화번호 저장 방식의 불일치로 인해 발생한 로그인 실패 사례는 하나의 결함을 드러냈습니다. 한 사용자의 계정이 차단되었는데, 그 이유는 데이터베이스에 동일한 독일 모바일 번호가 두 가지 형태—한 행에는 0171 5550134, 다른 행에는 +49 171 5550134—로 저장되어 시스템이 이를 서로 다른 항목으로 처리했기 때문입니다. 결과적으로 OTP가 전달되지 않았고, "코드 전송" 버튼은 도박과 다름없는 상황이 되었습니다.

전화번호가 날짜보다 다루기 까다로운 이유

개발자들은 종종 전화번호 입력을 제어하기 위해 regex를 신뢰합니다. 하지만 번호가 국경을 넘거나 국가별 통신 계획이 변경되는 순간 그 신뢰는 깨집니다. 날짜는 예측 가능한 달력을 따르지만, 전화번호는 통신사, 규제, 문화적 관습에 따라 계속 변합니다.

원시 문자열(raw strings)의 숨겨진 비용

전화번호를 일반 문자열로 저장하면 고유해 보이지만, 두 가지 표현 방식이 동일한 번호를 가리키게 되면 문제가 발생합니다. 데이터베이스에서 "특정" 번호를 조회하는 OTP 시스템은 결국 사용자에게 도달하지 않는 중복된 번호로 코드를 보내게 됩니다.

번호가 숫자 필드에 저장되어 있으면 문제는 더 심각해집니다. BIGINT 컬럼은 맨 앞의 +와 모든 앞자리 0을 제거하여 +49 171 5550134491715550134로 바꿔버립니다. 플러스 기호가 없으면 원래 형식을 복구하는 것은 추측에 의존할 수밖에 없습니다.

E.164 규약

국제 전화번호 체계인 E.164는 단일하고 이식 가능한 표현 방식을 정의합니다:

  • +로 시작
  • 1~3자리의 국가 코드 뒤에 이어짐
  • 그 다음 가입자 번호
  • 총 15자리 이하
  • 공백, 마침표, 대시 없음

E.164는 번호가 활성 상태임을 보장하지 않습니다. 단지 문자열이 유효한 구조적 패턴을 따르는지만 보장합니다. 이를 연결 가능 여부를 알려주는 예언자가 아닌, 형식 규약(format contract)으로 취급하십시오.

regex로 해결할 수 없는 흔한 함정들

  • 숫자형 저장 – BIGINT는 +와 앞자리 0을 버립니다. 대신 텍스트 컬럼(TEXT 또는 VARCHAR)을 사용하세요.
  • 하드코딩된 regex – 국가별 계획은 변경됩니다. 멕시코는 2019년에 트렁크 접두사를 폐지했고, 아르헨티나는 현재 모바일 회선의 경우 국가 코드 뒤에 9를 요구합니다. 정적인 패턴은 금방 쓸모없게 됩니다.
  • 무분별한 0 제거 – 이탈리아 유선 전화는 앞자리 0을 유지하지만, 독일은 그렇지 않습니다. "앞자리 0 제거"라는 일괄적인 규칙은 독일 번호는 그대로 두면서 이탈리아 데이터를 손상시킵니다.
  • 형식이 곧 도달 가능성이라고 가정 – libphonenumber는 구조를 검증하지만, 단말기가 켜져 있는지 또는 번호가 이동되었는지는 알 수 없습니다.

신뢰할 수 있는 파이프라인 구축하기

  1. 국가 요청하기 – 회원가입 양식에 국가 선택기를 추가하고 해당 지역 정보를 파서(parser)에 전달하세요.
  2. 실시간 포맷팅 보여주기 – 사용자가 입력하는 동안 올바른 패턴을 볼 수 있도록 “AsYouType” 포맷팅을 사용하세요.
  3. Blur 시점에 검증하기 – 매 키 입력마다 검증하는 대신 사용자가 필드를 벗어날 때 검증을 실행하여 마찰을 줄이세요.
  4. E.164 문자열만 저장하기 – 정규화된 + 접두사 번호를 데이터베이스에 저장하세요.
  5. 가장자리(Edge)에서 포맷팅하기 – UI나 이메일 템플릿에서만 사용자가 보기 편한 레이아웃으로 다시 변환하세요.

레거시 데이터를 마이그레이션할 때는 검증에 실패한 행을 그대로 유지하세요. 기존의 각 항목을 새 컬럼으로 파싱하고, 실패 항목에 플래그를 지정한 뒤 보고서를 생성하세요. 조용히 데이터를 누락시키면 나중에 비용이 많이 드는 수정 작업으로 이어지는 고객 지원 티켓이 발생하게 됩니다.

개발자를 위한 빠른 체크리스트

  • 번호를 E.164 형식의 TEXT/VARCHAR로 저장하세요.
  • Google의 libphonenumber 라이브러리를 사용하세요. 전 세계 통신 계획의 복잡한 현실을 잘 처리해 줍니다.
  • 국가 코드를 생략한 사용자를 위해 기본 지역(default region)을 제공하세요.
  • 실시간 회원가입 시 is_valid_number를 호출하세요. 번호가 지역 규칙에 맞는지 확인합니다.
  • 대량 데이터를 정리할 때는 is_possible_number를 사용하세요. 경계선에 있는 사례를 거부하지 않으면서 명백하게 잘못된 형식을 잡아냅니다.

적절한 전화번호 처리는 있으면 좋은 기능(nice-to-have)이 아니라, 신뢰할 수 있는 사용자 연락에 의존하는 모든 시스템의 필수 전제 조건입니다. E.164로 정규화하고 검증된 라이브러리에 파싱을 위임함으로써, 개발자는 신뢰와 수익을 조용히 갉아먹는 버그를 제거할 수 있습니다. 전화번호를 자유 형식의 텍스트가 아닌 구조화된 데이터로 취급하고, 표준이 복잡한 작업을 처리하도록 맡기십시오.