Go JSON에서 큰 정수 ID가 바뀌는 이유: UseNumber와 int64로 정밀도 지키기
Go encoding/json의 float64 변환으로 큰 정수 ID가 달라지는 문제를 재현한다. UseNumber·int64 비교와 12개 입력 검증 사례로 정확한 파싱 계약을 정리한다.
Go의 encoding/json으로 큰 정수 ID를 map[string]any에 읽는다면, 기본 float64 변환으로 원래 값이 바뀔 수 있다. 스키마를 아는 필드는 int64로 바로 읽고, 동적 객체는 Decoder.UseNumber로 숫자 리터럴을 보존한 뒤 범위를 검사한다. 이 글은 두 경로를 실행 코드로 비교하고, API 입력에서 무엇을 거부할지도 함께 정한다.
흐린 렌즈와 투명한 보호 통로로 숫자 정밀도의 손실과 보존을 표현한 개념 일러스트. 실제 데이터 흐름도는 아니다.
작성·문서 확인: 2026-10-30. 실행 검증: Go 1.24.7, Linux amd64, 표준 라이브러리 encoding/json. 최신 Go 버전의 실행 결과를 주장하지 않으며, 다른 JSON 패키지의 기본 동작으로 일반화하지 않는다.
숫자를 읽었을 뿐인데 ID가 달라지는 이유
예제 입력은 {"id":9007199254740993}이다. 기본 경로는 숫자를 float64로 저장한다. 이후 int64로 캐스팅해도 이미 없어진 자릿수 정보는 돌아오지 않는다. 이는 로그 표시 형식만의 문제가 아니다. 아래 프로그램은 캐스팅 결과까지 검사한다.
Go 공식 Unmarshal 문서는 인터페이스 값에 들어가는 JSON 숫자의 기본 타입을 설명한다. RFC 8259 §6은 binary64를 사용하는 구현 간에 정확한 정수 합의를 기대할 수 있는 범위를 [-(2^53)+1, (2^53)-1]로 제시한다. 범위 밖의 모든 정수가 틀린다는 뜻은 아니다. 특정 값이 정확히 표현되는지와 연속된 ID 전체를 안전하게 다루는지는 다른 조건이다.
스키마가 정해져 있는지 먼저 구분한다
| 입력·사용 조건 | 선택 | 별도로 확인할 것 |
| 숫자 ID 필드의 스키마를 안다 | 구조체의 int64 필드 | 누락·null·양수 여부 |
| 여러 종류의 필드가 섞인 동적 객체다 | UseNumber 후 Int64() | 타입·문법·범위 오류 |
| 브라우저 등 여러 소비자가 큰 ID를 주고받는다 | API 계약에서 ID를 문자열로 정의하는 방안 | 모든 생산자·소비자의 일관성 |
UseNumber 문서에 따라 숫자의 해석을 뒤로 미룬다. 임의 정밀도 정수 연산을 자동으로 제공하는 옵션은 아니다. Int64()의 오류를 버리지 않아야 한다. 문자열 ID 계약을 선택한다면 아래 숫자 전용 파서와는 다른 계약이므로 검증 코드도 바꿔야 한다.
실행 가능한 비교와 입력 검증
아래를 json_number.go로 저장하고 go run json_number.go로 실행한다. 외부 모듈이나 서비스는 필요 없다. parseID의 계약은 “객체 하나, 따옴표 없는 양의 int64 ID”다. 지수 표기 1e3도 이 계약에서는 거부한다.
두 번째 Decode는 첫 객체 뒤에 다른 값이나 잘못된 문자가 붙지 않았는지 확인한다. 이는 완결된 입력 문자열을 검증하는 예제다. 계속 열려 있는 네트워크 스트림에 그대로 적용하면 입력 종료를 기다릴 수 있다.
실제 출력과 경계 사례
이 글의 작성 과정에서 위 프로그램을 실행한 출력이다.
| 검사 입력의 id 또는 입력 형태 | 예제의 기대 결과 |
9007199254740993, 9223372036854775807 | 정확한 정수로 수락 |
9223372036854775808 | int64 범위 초과로 거부 |
1.5, 1e3 | 정수 표기 계약에 맞지 않아 거부 |
| 문자열 숫자, null, 필드 누락 | 숫자 타입이 아니므로 거부 |
0, -1 | 양수 조건에 맞지 않아 거부 |
| 객체 두 개, 객체 뒤의 broken | 단일 JSON 값 조건에 맞지 않아 거부 |
표의 12개 사례를 모두 검사했고, 기본 경로의 반올림과 구조체 경로의 정확한 값도 별도로 확인했다. 벤치마크, 사용자 서버, 최신 Go 버전 또는 브라우저 간 전송을 실험한 결과는 아니다.
적용 범위와 남는 검증
1e3은 유효한 JSON 숫자다. 여기서는 정수 문자열 문법으로 해석하는Int64()가 실패하므로 거부한다. 수학적으로 정수인 모든 JSON 표기를 허용하려면 별도 설계가 필요하다. 정수 문자열을 다룰 때도 strconv.ParseInt의 범위 오류를 무시하지 않는다.
- 구조체에
int64필드가 있다고 필수 입력 검증까지 끝나는 것은 아니다. 새 구조체에서는 누락이나null이 0으로 남을 수 있다. 포인터로 비어 있는 상태를 확인할 수 있지만, 누락과 명시적null을 서로 구별하려면 추가 상태가 필요하다.
- 이 예제는 모르는 필드를 허용하며 중복 키를 거부하지 않는다.
{"id":1,"id":2}같은 입력을 허용할지는 별도 계약으로 정해야 한다. 인증·서명 경계에서는 파서 간 해석 차이도 검토한다.
- HTTP 처리에서는 본문 크기와 읽기 시간 제한을 별도로 둔다. 숫자 정밀도 해결만으로 요청 자원 제한이 생기지 않는다.
- 앞단에서 이미 부동소수점으로 반올림한 ID는 수신부의
UseNumber로 복원할 수 없다. 계산하지 않는 식별자라면 처음 생성하는 지점부터 문자열 계약을 검토한다.
English version: Go JSON Large Integer Precision: Preserve IDs with UseNumber and int64