import를 바꿨는데 코드가 그대로 컴파일된다고 해서 API 응답도 같지는 않습니다. Go가 11월 4일 공개한 JSON v2 해설은 이 차이를 이해하기 좋은 자료입니다.[105] 다만 Go 1.27 자체는 2026년 8월 릴리스이므로, 이번 글의 계기는 새 버전 출시가 아니라 새 공식 해설입니다.[132]
이 글은 JSON API를 만드는 BE 개발자와 그 응답을 소비하는 FE 개발자를 위한 설명입니다. Go를 쓰지 않더라도 문법 검사와 데이터 해석의 차이, 기존 소비자를 깨뜨리지 않는 이전 방법을 가져갈 수 있습니다.
💡 💡 핵심: JSON v2의 가치는 단순한 속도 향상이 아니라 더 엄격한 기본값과 명시적인 호환성 선택입니다.[105][111] 새 API로 이전하는 일과 외부에 보이는 데이터 의미를 바꾸는 일을 분리하세요.
JSON은 문자열·숫자·배열·객체 등을 표현하는 데이터 교환 형식입니다.[133] 하지만 두 시스템이 같은 JSON을 받아도 중복 이름, 필드 이름의 대소문자, 큰 정수 등을 다르게 해석할 수 있습니다.[133]
공식 문서는 인증을 담당하는 서비스와 실제 작업을 수행하는 서비스가 같은 요청을 다른 사용자로 해석하는 상황을 보안 문제의 예로 듭니다.[133] 이는 특정 서비스를 여기서 공격해 확인했다는 뜻이 아니라, parser 사이의 해석 차이가 왜 중요한지를 설명하는 문서 예시입니다.
encoding/json/jsontext는 구문 처리를 담당합니다. JSON token과 완결된 value를 읽고 쓰며, 그 순서가 유효한 JSON 문법을 이루는지 state machine으로 확인합니다.[134]
encoding/json/v2는 Go 값과 JSON 값 사이의 의미적 변환을 담당합니다. JSON 객체를 Go struct나 map으로 읽고, Go slice를 JSON 배열로 쓰는 규칙이 이 계층에 있습니다.[133]
따라서 동작 흐름은 JSON 입력의 구문 처리, Go 값으로의 매핑, 애플리케이션 판단으로 나뉩니다.[133][134] 구문을 허용할지와 어떤 필드에 어떤 값을 넣을지를 별도 정책으로 다룰 수 있는 구조입니다.[133]
기존 UnmarshalJSON은 사용자 정의 타입에 완결된 JSON을 byte slice로 전달해야 합니다.[105] 바깥 parser가 값의 경계를 찾은 다음, 사용자 코드가 같은 내용을 다시 읽게 됩니다.[105]
중첩된 타입이 이 작업을 반복하면 재귀적인 재파싱 비용이 커질 수 있습니다.[105] JSON v2의 UnmarshalJSONFrom과 MarshalJSONTo는 decoder·encoder를 전달받아 streaming 방식으로 처리하도록 설계됐습니다.[105]
그렇다고 모든 호출이 자동으로 같은 이익을 얻는 것은 아닙니다. 기존 사용자 정의 메서드를 계속 쓰는 경우와 새 streaming 인터페이스를 구현한 경우를 구분해야 합니다.[105]
공식 package 문서에서 UnmarshalRead는 입력이 하나의 JSON value인지 확인하며, 그 뒤의 비공백 데이터도 오류로 취급합니다.[133] 반면 UnmarshalDecode는 decoder에서 다음 JSON value를 읽는 API입니다.[133]
적용 판단 제안: 하나의 요청 본문을 받는 endpoint와 여러 JSON 값을 연속해서 받는 수집기를 같은 코드 교체 대상으로 보지 마세요. io.Reader를 받는다는 사실만으로 문서 경계와 처리 방식이 같아지지는 않습니다.
v1은 nil slice·map을 null로 쓰지만, v2는 각각 빈 배열 []과 빈 객체 {}로 씁니다.[111] 공식 migration guide의 Pet 예제에서도 import만 바꾸면 Nicknames가 null에서 []로 달라집니다.[111]
설계 예시: 화면에서 null을 “아직 조회하지 않음”, 빈 배열을 “조회했지만 결과 없음”으로 구분하고 있다면, 소비자와 합의 없이 이 변화를 적용하면 안 됩니다. 이는 가능한 서비스 설계의 예이지 Go 문서가 모든 앱에 요구하는 의미는 아닙니다.
v2의 omitempty는 Go의 zero value가 아니라 JSON으로 표현했을 때 비었는지를 기준으로 합니다. 해당 JSON 값은 null, 빈 문자열, 빈 객체, 빈 배열입니다.[133]
반대로 omitzero는 IsZero 메서드 또는 Go의 zero value를 기준으로 합니다.[133] 따라서 숫자 0이나 false를 빠뜨릴지, 빈 컬렉션을 남길지까지 출력 계약에 맞춰 검토해야 합니다.[133]
v2는 기본적으로 잘못된 UTF-8과 객체의 중복 이름을 거부하고, JSON 이름과 struct 필드는 대소문자를 구분해 매칭합니다.[133] 그러나 알 수 없는 필드는 여전히 기본적으로 무시합니다.[133]
알 수 없는 필드도 오류로 처리하려면 RejectUnknownMembers를 명시해야 합니다.[133] 대소문자가 다른 필드가 매칭되지 않는다는 사실과, 그 요청 전체가 자동으로 거부된다는 주장은 다릅니다.[133]
💡 ⚠️ 주의: 더 안전한 기본값은 비즈니스 규칙 검증의 대체물이 아닙니다. 공식 문서도 큰 정수의 정밀도와 unknown member를 별도로 주의하라고 설명합니다.[133]
다음은 공식 migration guide를 바탕으로 한 적용 제안입니다. 실제 서비스 변경이나 여기서 수행한 테스트 결과는 아닙니다.[111]
DefaultOptionsV1을 함께 사용하면 v1 동작을 유지하면서 API 이전을 시작할 수 있습니다.[111]jsonsplit의 CallBothButReturnV1은 두 경로의 차이를 보고하면서 호출자에게는 v1 결과를 반환하는 migration 방식입니다.[111]AutoDetectOptions는 원인을 좁히기 위해 추가 호출도 수행합니다.[111] 민감한 실제 payload를 무가공으로 로그에 남기지 말고, 필요한 필드와 집계 지표만 보관하는 편을 권합니다.모든 서비스에 shadow 비교가 필요한 것은 아닙니다. 공식 guide도 작은 앱이나 높은 위험 감수 조건의 일괄 이전, 복잡한 앱의 option별 이전, 운영 서버의 비교 관찰을 서로 다른 선택지로 나눕니다.[111]
앞서 본 Pet 예제는 공식 문서 예제입니다. guide는 v1 출력, v2 출력, v1 호환 option을 적용한 출력을 나란히 제시합니다.[111] 이 글에서 해당 프로그램을 실행한 것은 아닙니다.
이 예제의 학습 포인트는 새 문법을 익히는 데 있지 않습니다. 같은 타입과 호출 형태를 유지해도 외부 소비자가 보는 값은 바뀔 수 있다는 점입니다.[111]
kube-openapi의 2022년 공개 issue는 OpenAPI 타입의 사용자 정의 역직렬화가 같은 JSON을 여러 단계에서 반복 처리하는 문제를 다룹니다.[116] 후속 댓글에는 실험적 JSON module을 사용해 streaming 방식으로 바꾼 시험과 다른 참여자의 비교 결과가 남아 있습니다.[136]
이는 실제 개발자가 공개한 과거 구현 실험입니다.[136] Go 1.27을 도입한 모든 Kubernetes 서비스의 성능 개선이나, 현재 배포 버전 전체의 처리량을 입증하는 사례로 확대하면 안 됩니다.
공식 11월 해설은 Unmarshal benchmark에서 최대 10배 개선을 소개하지만, Marshal은 경우에 따라 빠르거나 느리다고 설명합니다.[105] 이는 Go 측 benchmark 설명이지 이 글에서 독립 측정한 결과가 아닙니다.
연결된 jsonbench README도 확인했습니다. 그 표는 Go 1.23.5와 2025년 실험 module revision을 명시합니다.[118] 따라서 표의 배수를 현재 Go 1.27.3의 측정값으로 옮겨 쓰지 않았습니다.
Daniel Lemire는 2026년 8월, Go 1.27.0에서 Apple M4 Max·Intel Xeon, 단일 core와 반복 측정 조건을 공개했습니다.[131] 그의 시험에서는 any 중심 처리와 작은 typed struct 배열의 marshal이 같은 방향으로 움직이지 않았습니다.[131]
관련 Go issue에는 별도 사용자의 재시험과 후속 최적화 제안도 있습니다.[115][135] 확인 시점에 issue는 open이지만, 이것만으로 모든 현재 build가 처음 보고된 만큼 느리다거나 제안된 개선이 모두 배포됐다고 단정하지 않았습니다.
실무 판단 제안: 자기 서비스의 입력 크기·중첩·타입·custom marshaler, 사용 option, 실제 Go patch version을 고정해 비교하세요. 의미가 다른 기본 설정끼리의 속도 비교와 동일한 계약 아래의 속도 비교도 나누는 편이 유용합니다.[131]
기존 encoding/json을 유지하는 것도 공식적으로 지원되는 선택입니다. v1은 계속 지원되며, v1과 v2는 사용자 정의 marshaling 등에서 서로 호환되도록 설계됐습니다.[111]
도입 의견: 새 endpoint나 명확한 JSON 병목이 있는 구간부터 검토하세요. 현재 성능이 충분하고 외부 소비자와의 계약이 복잡하다면, 전면 이전보다 기존 구현 유지와 입력 검증·대표 payload 테스트 보강이 단순할 수 있습니다.
💡 🔎 검증 노트: 2026년 11월 5일 KST에 공식 발표·migration guide·Go 1.27 release notes와
go1.27.3package 문서를 대조했습니다.[132][133][134] 외부 사용기와 공개 issue도 읽었지만, 직접 Go 프로그램 실행·설치·benchmark 측정·운영 트래픽 검증은 하지 않았습니다. 과거 실험의 수치를 현재 환경의 성능 보장으로 사용하지 않았습니다.
Go JSON v2에서 가져갈 원리는 더 새롭고 엄격한 기본값도 기존 소비자에게는 호환성 변경일 수 있다는 점입니다. import 교체가 아니라, 서비스가 주고받는 데이터의 의미를 이전 대상으로 삼으세요.
[105] https://go.dev/blog/jsonv2 — Go 공식 JSON v2 해설 — 2026-11-04
[111] https://go.dev/doc/jsonv2-migration — Go JSON v2 Migration Guide
[115] https://api.github.com/repos/golang/go/issues/81315 — Go issue #81315 — Go 1.27.0 재시험 보고
[116] https://api.github.com/repos/kubernetes/kube-openapi/issues/315 — kube-openapi issue #315 — 2022년 반복 파싱 문제
[118] https://github.com/go-json-experiment/jsonbench — jsonbench README — Go 1.23.5·2025 실험 revision
[131] https://lemire.me/blog/2026/08/29/the-new-go-json-api-twice-as-fast-or-1-5x-slower — Daniel Lemire — Go 1.27.0 독립 benchmark, 2026-08-29
[132] https://go.dev/doc/go1.27 — Go 1.27 Release Notes — 8월 릴리스와 구분
[133] https://pkg.go.dev/encoding/json/v2@go1.27.3 — encoding/json/v2 — go1.27.3 package 문서
[134] https://pkg.go.dev/encoding/json/jsontext@go1.27.3 — encoding/json/jsontext — go1.27.3 package 문서
[135] https://api.github.com/repos/golang/go/issues/81315/comments?per_page=100&page=1 — Go issue #81315 후속 댓글 — live API 전체 14개
[136] https://api.github.com/repos/kubernetes/kube-openapi/issues/315/comments?per_page=100&page=1 — kube-openapi issue #315 후속 댓글 — live API 전체 17개