JSON 에서 ID 가 문자열이어야 하는 이유
문자열이다. JSON 에는 수 타입이 하나뿐이고, 거의 모든 파서가 그것을 배정밀도 부동소수점으로 읽는다. 그 표현이 정수를 정확히 담는 한계는 2^53 − 1, 약 9.007 × 10^15 이다. 64비트 데이터베이스 ID 는 그 한계를 넘을 수 있고, 넘는 순간 파서는 가장 가까운 표현 가능한 값을 조용히 돌려준다. 그러면 하나 어긋난 식별자로 레코드를 조회하고, 비교하고, 지우게 된다. 그 과정 어디에서도 오류가 나지 않는다.
실제로 무엇이 어긋나나
JSON.parse("9007199254740993") 은 9007199254740992 를 돌려준다. 보낸 값보다 하나 작은데, 그 과정에서 문제를 알리는 것이 하나도 없다. JSON 은 유효했고, 파싱은 성공했고, 결과는 지극히 평범한 수다. 다만 직렬화된 그 수가 아닐 뿐이다.
그 경계 — 9,007,199,254,740,991 — 가 Number.MAX_SAFE_INTEGER 다. 그 아래에서는 모든 정수가 정확히 왕복한다. 그 위로는 표현 가능한 값 사이의 간격이 벌어지고, 서로 다른 두 ID 가 같은 double 로 뭉개질 수 있다. Snowflake ID, 트위터 상태 ID, 카프카 오프셋, bigint 기본키가 일상적으로 그 위에 있다.
이 실패는 구조적으로 조용하다. 잡을 예외도, 확인할 플래그도 없다. 파서 입장에서는 특별한 일이 일어나지 않았기 때문이다. 처음 눈에 보이는 증상은 대개 찾을 수 없는 레코드이거나, 더 나쁘게는 엉뚱한 레코드가 돌아오는 것이다.
규격이 이걸 허용하는 이유
RFC 8259 는 JSON 수의 크기에 제한을 두지 않는다 — 문법은 아무 자릿수나 받는다. 대신 상호운용성은 IEEE 754 배정밀도가 정확히 표현하는 범위 안에서만 보장된다고 경고한다. 구현들이 실제로 그것을 쓰기 때문이다.
그래서 64비트 정수를 JSON 수로 쓰는 직렬화기는 유효한 JSON 을 만들고 있는 것이 맞다. 문서는 옳고 왕복이 옳지 않다. 규격은 문서의 정확성과 값의 정확성이 서로 다른 질문이라고 말하고 있는 셈이다.
그럼 어떻게 하나
식별자를 JSON 문자열로 직렬화한다. 문자열은 길이에 상관없이 정확히 왕복하고, 정렬이 예측 가능하며, 두 바이트를 더 쓴다. 이 API 를 쓰는 어떤 언어에도 값을 조용히 바꾸지 않는 문자열 타입이 이미 있다.
이 문제를 겪은 API 들이 지금 그렇게 한다. 트위터는 숫자 필드가 자바스크립트 클라이언트를 깨뜨린 뒤 id 옆에 id_str 을 더했고, Stripe·Discord 를 비롯해 그 뒤에 만들어진 시스템들은 처음부터 식별자를 문자열로 보낸다.
받는 쪽인데 보내는 쪽을 고칠 수 없다면, double 로 변환되기 전에 값을 보존하는 방식으로 파싱한다 — 큰 정수를 BigInt 로 읽는 reviver 를 쓰거나, 수를 문자열로 돌려주도록 설정한 파서를 쓴다. JSON.parse 가 한 번 돌고 나면 원래 값은 사라졌고 되찾을 수 없다.
수량이 아니라 식별자인 것에는 모두 같은 논리가 적용된다. 계좌번호, 주문 참조, 0 으로 채운 코드. 산술을 할 일이 영영 없다면 그건 수가 아니다.
관련 도구
이 도구가 따르는 규격
- RFC 8259 §6 — 수 — 상호운용성이 IEEE 754 배정밀도 범위 안에서만 보장된다고 적어 둔 곳.
- MDN — Number.MAX_SAFE_INTEGER — 2^53 − 1 경계와 그 위에서 벌어지는 일.
- IEEE 754 — 부동소수점 산술 — 주요 JSON 파서가 수에 쓰는 표현.
고친 날 2026-09-05