logo
홈블로그소개
4,874

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

·개인정보처리방침
Web

Go JSON v2: 문법과 데이터 의미를 나눠 안전하게 API 이전하기

Toma
2026년 11월 5일
약 14분
목차
🔄 Go JSON v2: 문법과 데이터 의미를 나눠 안전하게 API 이전하기
🧩 개념: JSON 문법과 데이터 의미는 다릅니다
🔎 중괄호가 맞는다고 같은 데이터는 아닙니다
🧱 두 패키지가 나누는 책임
⚙️ 동작 방식: streaming은 무엇을 줄이나
📦 전체 값을 다시 파싱하는 인터페이스의 한계
🌊 하나의 문서와 값의 연속도 구분합니다
🔀 이전 전에 확인할 기본값
🗃️ null과 빈 컬렉션은 같은 계약이 아닙니다
🏷️ omitempty와 omitzero의 기준을 나눕니다
🛡️ 더 엄격해져도 모든 검증을 대신하지는 않습니다
🛠️ 활용: API 교체와 의미 변경을 분리하는 순서
🔬 확인된 예제와 공개 사례
📖 문서 예제: 컴파일 성공 뒤 달라지는 Nicknames
🧪 역사적 사례: kube-openapi의 반복 파싱
⚖️ 성능 주장과 반대 근거를 함께 읽기
⏱️ 최대 10배는 서비스 응답 시간의 보장이 아닙니다
🔍 독립 측정도 버전과 데이터 형태를 봐야 합니다
🚧 더 단순한 대안과 검증 한계
🎯 정리
Sources
이전 포스트Transformers MIC: 작은 모델로 조용한 회귀 버그를 잡는 테스트 설계

목차

🔄 Go JSON v2: 문법과 데이터 의미를 나눠 안전하게 API 이전하기
🧩 개념: JSON 문법과 데이터 의미는 다릅니다
🔎 중괄호가 맞는다고 같은 데이터는 아닙니다
🧱 두 패키지가 나누는 책임
⚙️ 동작 방식: streaming은 무엇을 줄이나
📦 전체 값을 다시 파싱하는 인터페이스의 한계
🌊 하나의 문서와 값의 연속도 구분합니다
🔀 이전 전에 확인할 기본값
🗃️ null과 빈 컬렉션은 같은 계약이 아닙니다
🏷️ omitempty와 omitzero의 기준을 나눕니다
🛡️ 더 엄격해져도 모든 검증을 대신하지는 않습니다
🛠️ 활용: API 교체와 의미 변경을 분리하는 순서
🔬 확인된 예제와 공개 사례
📖 문서 예제: 컴파일 성공 뒤 달라지는 Nicknames
🧪 역사적 사례: kube-openapi의 반복 파싱
⚖️ 성능 주장과 반대 근거를 함께 읽기
⏱️ 최대 10배는 서비스 응답 시간의 보장이 아닙니다
🔍 독립 측정도 버전과 데이터 형태를 봐야 합니다
🚧 더 단순한 대안과 검증 한계
🎯 정리
Sources

🔄 Go JSON v2: 문법과 데이터 의미를 나눠 안전하게 API 이전하기

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 문법과 데이터 의미는 다릅니다

🔎 중괄호가 맞는다고 같은 데이터는 아닙니다

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]

⚙️ 동작 방식: streaming은 무엇을 줄이나

📦 전체 값을 다시 파싱하는 인터페이스의 한계

기존 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를 받는다는 사실만으로 문서 경계와 처리 방식이 같아지지는 않습니다.

🔀 이전 전에 확인할 기본값

🗃️ null과 빈 컬렉션은 같은 계약이 아닙니다

v1은 nil slice·map을 null로 쓰지만, v2는 각각 빈 배열 []과 빈 객체 {}로 씁니다.[111] 공식 migration guide의 Pet 예제에서도 import만 바꾸면 Nicknames가 null에서 []로 달라집니다.[111]

설계 예시: 화면에서 null을 “아직 조회하지 않음”, 빈 배열을 “조회했지만 결과 없음”으로 구분하고 있다면, 소비자와 합의 없이 이 변화를 적용하면 안 됩니다. 이는 가능한 서비스 설계의 예이지 Go 문서가 모든 앱에 요구하는 의미는 아닙니다.

🏷️ omitempty와 omitzero의 기준을 나눕니다

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]

🛠️ 활용: API 교체와 의미 변경을 분리하는 순서

다음은 공식 migration guide를 바탕으로 한 적용 제안입니다. 실제 서비스 변경이나 여기서 수행한 테스트 결과는 아닙니다.[111]

  1. 기존 계약을 적습니다. nil·빈 배열·빈 map, 숫자 0·false, 필드 이름의 대소문자, 중복 이름, unknown member를 대표 입력과 응답으로 정리합니다.[111][133]
  2. API부터 옮깁니다. 필요한 호출에서 v2와 DefaultOptionsV1을 함께 사용하면 v1 동작을 유지하면서 API 이전을 시작할 수 있습니다.[111]
  3. 기본값은 하나씩 바꿉니다. 뒤에 전달한 option이 앞의 option을 덮어쓰므로, 호환성 설정 위에서 바꿀 동작만 선택할 수 있습니다.[111]
  4. 차이를 관찰하되 기존 출력을 유지합니다. jsonsplit의 CallBothButReturnV1은 두 경로의 차이를 보고하면서 호출자에게는 v1 결과를 반환하는 migration 방식입니다.[111]
  5. 관찰 비용과 로그를 제한합니다. 두 번 처리하면 그만큼 비용이 늘며, AutoDetectOptions는 원인을 좁히기 위해 추가 호출도 수행합니다.[111] 민감한 실제 payload를 무가공으로 로그에 남기지 말고, 필요한 필드와 집계 지표만 보관하는 편을 권합니다.
  6. 전환 후 임시 계층을 정리합니다. 차이를 처리한 뒤 v2로 전환하고, migration wrapper를 제거하는 순서가 공식 guide에 제시돼 있습니다.[111]

모든 서비스에 shadow 비교가 필요한 것은 아닙니다. 공식 guide도 작은 앱이나 높은 위험 감수 조건의 일괄 이전, 복잡한 앱의 option별 이전, 운영 서버의 비교 관찰을 서로 다른 선택지로 나눕니다.[111]

🔬 확인된 예제와 공개 사례

📖 문서 예제: 컴파일 성공 뒤 달라지는 Nicknames

앞서 본 Pet 예제는 공식 문서 예제입니다. guide는 v1 출력, v2 출력, v1 호환 option을 적용한 출력을 나란히 제시합니다.[111] 이 글에서 해당 프로그램을 실행한 것은 아닙니다.

이 예제의 학습 포인트는 새 문법을 익히는 데 있지 않습니다. 같은 타입과 호출 형태를 유지해도 외부 소비자가 보는 값은 바뀔 수 있다는 점입니다.[111]

🧪 역사적 사례: kube-openapi의 반복 파싱

kube-openapi의 2022년 공개 issue는 OpenAPI 타입의 사용자 정의 역직렬화가 같은 JSON을 여러 단계에서 반복 처리하는 문제를 다룹니다.[116] 후속 댓글에는 실험적 JSON module을 사용해 streaming 방식으로 바꾼 시험과 다른 참여자의 비교 결과가 남아 있습니다.[136]

이는 실제 개발자가 공개한 과거 구현 실험입니다.[136] Go 1.27을 도입한 모든 Kubernetes 서비스의 성능 개선이나, 현재 배포 버전 전체의 처리량을 입증하는 사례로 확대하면 안 됩니다.

⚖️ 성능 주장과 반대 근거를 함께 읽기

⏱️ 최대 10배는 서비스 응답 시간의 보장이 아닙니다

공식 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.3 package 문서를 대조했습니다.[132][133][134] 외부 사용기와 공개 issue도 읽었지만, 직접 Go 프로그램 실행·설치·benchmark 측정·운영 트래픽 검증은 하지 않았습니다. 과거 실험의 수치를 현재 환경의 성능 보장으로 사용하지 않았습니다.

🎯 정리

  • 구문과 의미를 분리합니다. JSON이 유효한지와 Go 값으로 어떻게 해석하는지는 다른 계층입니다.[133][134]
  • API와 계약을 따로 이전합니다. 먼저 호환 동작을 유지하고, 변경할 기본값만 명시적으로 선택할 수 있습니다.[111]
  • 속도보다 조건을 확인합니다. 데이터 형태·option·version이 다른 benchmark를 하나의 배수로 요약하지 않습니다.[118][131]

Go JSON v2에서 가져갈 원리는 더 새롭고 엄격한 기본값도 기존 소비자에게는 호환성 변경일 수 있다는 점입니다. import 교체가 아니라, 서비스가 주고받는 데이터의 의미를 이전 대상으로 삼으세요.

Sources

[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개