Go 1.25의 새로운 실험적 JSON API
개요
JavaScript Object Notation (JSON)은 단순한 데이터 교환 형식으로, 거의 15년 전 Go에서 JSON 지원이 도입된 이후 인터넷에서 가장 인기 있는 데이터 형식이 되었습니다. encoding/json은 현재 Go에서 5번째로 많이 import되는 패키지입니다.
Go 1.25에서는 오랫동안 기다려온 개선 사항과 수정 사항을 제공하는 새로운 실험적 encoding/json/v2와 encoding/json/jsontext 패키지를 도입했습니다.
encoding/json의 문제점
동작상의 결함
JSON 구문 처리의 부정확성
-
잘못된 UTF-8 허용: 최신 인터넷 표준(RFC 8259)은 유효한 UTF-8을 요구하지만, 현재 encoding/json은 잘못된 UTF-8을 허용하여 데이터 손상을 야기할 수 있음
-
중복 멤버 이름 허용: JSON 객체에서 중복된 이름을 허용하여 보안 취약점(CVE-2017-12635)으로 악용될 수 있음
슬라이스와 맵의 nilness 누출
JSON은 종종 JSON 배열이나 객체로 예상되는 데이터 타입에 null을 unmarshaling할 수 없는 JSON 구현을 사용하는 프로그램과 통신하는 데 사용됩니다. 대부분의 Go 사용자는 nil 슬라이스와 맵이 기본적으로 빈 JSON 배열 또는 객체로 marshaling되기를 선호합니다.
대소문자 구분 없는 unmarshaling
JSON 객체 멤버 이름을 Go 구조체 필드 이름으로 해결할 때 대소문자를 구분하지 않는 매치를 사용하는데, 이는 놀라운 기본값이자 잠재적인 보안 취약점, 그리고 성능 제한 사항입니다.
메서드 호출의 비일관성
구현 세부 사항으로 인해 포인터 리시버에 선언된 MarshalJSON 메서드가 encoding/json에 의해 일관되지 않게 호출됩니다.
API 결함
io.Reader에서 올바른 unmarshaling의 어려움
사용자들은 종종 json.NewDecoder(r).Decode(v)를 작성하는데, 이는 입력 끝의 trailing junk를 거부하지 못합니다.
옵션 설정의 제한
-
Encoder와 Decoder 타입에서는 옵션을 설정할 수 있지만 Marshal과 Unmarshal 함수에서는 사용할 수 없음
-
Marshaler와 Unmarshaler 인터페이스를 구현하는 타입들은 옵션을 활용할 수 없음
함수의 제한된 유연성
Compact, Indent, HTMLEscape 함수들이 bytes.Buffer에 쓰기만 하고 byte나 io.Writer 같은 더 유연한 방식을 지원하지 않습니다.
성능 제한 사항
MarshalJSON의 할당 강제
MarshalJSON 인터페이스 메서드는 구현체가 반환된 byte를 할당하도록 강제하며, 결과가 유효한 JSON인지 확인하고 지정된 들여쓰기에 맞게 재포맷해야 합니다.
UnmarshalJSON의 완전한 JSON 값 요구
완전한 JSON 값을 제공해야 하므로 encoding/json이 JSON 값을 완전히 파싱한 후 UnmarshalJSON을 호출해야 하며, 이후 UnmarshalJSON 메서드 자체가 제공된 JSON 값을 다시 파싱해야 합니다.
스트리밍 부족
Encoder와 Decoder 타입이 io.Writer나 io.Reader에서 작동하지만 전체 JSON 값을 메모리에 버퍼링합니다.
중요: MarshalJSON이나 UnmarshalJSON 메서드의 구현이 Marshal이나 Unmarshal 함수를 재귀적으로 호출하면 성능이 이차적이 됩니다.
encoding/json/v2 계획
개발 배경
-
2020년 후반: Daniel Martí가 가상의 v2 패키지에 대한 초안 작성
-
Joe Tsai의 참여: Protocol Buffer 작업 후 encoding/json의 한계를 경험하고 Daniel과 협력 시작
-
5년간의 개발: Roger Peppe, Chris Hines, Johan Brandhorst-Satzkorn, Damien Neil 등이 합류하여 설계 검토, 코드 검토, 회귀 테스트 제공
주목할 점
Google 직원이 아닌 사람들에 의해 주로 개발되고 홍보되어, Go 프로젝트가 Go 생태계 개선에 전념하는 번영하는 글로벌 커뮤니티와의 협력적 노력임을 보여줍니다.
encoding/json/jsontext 기반 구축
기능 분리
JSON 직렬화를 두 가지 주요 구성 요소로 분리:
-
구문적 기능: JSON 문법에 기반한 처리 ("encode"와 “decode”)
-
의미적 기능: JSON 값과 Go 값 간의 관계 정의 ("marshal"과 “unmarshal”)
jsontext 패키지의 기본 API
package jsontext
type Encoder struct { ... }
func NewEncoder(io.Writer, ...Options) *Encoder
func (*Encoder) WriteValue(Value) error
func (*Encoder) WriteToken(Token) error
type Decoder struct { ... }
func NewDecoder(io.Reader, ...Options) *Decoder
func (*Decoder) ReadValue() (Value, error)
func (*Decoder) ReadToken() (Token, error)
type Kind byte
type Value []byte
func (Value) Kind() Kind
type Token struct { ... }
func (Token) Kind() Kind
핵심 개념
-
JSON 값: 완전한 데이터 단위로 Go에서 명명된 byte로 표현 (v1의 RawMessage와 동일)
-
JSON 토큰: 불투명한 Token 타입으로 표현되며 할당 없이 임의의 JSON 토큰을 나타냄
-
스트리밍 방식: jsontext의 Encoder와 Decoder는 진정한 스트리밍 방식으로 작동
새로운 인터페이스 메서드
v2에서는 MarshalJSON과 UnmarshalJSON의 성능 문제를 해결하기 위해 MarshalJSONTo와 UnmarshalJSONFrom 인터페이스 메서드를 도입했습니다. 이들은 Encoder나 Decoder에서 작동하여 순수 스트리밍 방식으로 JSON 처리가 가능합니다.
encoding/json/v2 소개
기본 API
package json
func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error
주요 개선 사항
-
옵션 우선 지원: marshal과 unmarshal 함수의 일급 인수로 옵션 제공
-
직접 io 지원: MarshalWrite와 UnmarshalRead 함수가 io.Writer나 io.Reader에서 직접 작동
-
유연성 확장: v1과 달리 옵션이 v2의 유연성과 구성 가능성을 크게 확장
타입별 사용자 정의
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
type UnmarshalerFrom interface {
UnmarshalJSONFrom(*jsontext.Decoder) error
}
호출자별 사용자 정의
func WithMarshalers(*Marshalers) Options
type Marshalers struct { ... }
func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
func WithUnmarshalers(*Unmarshalers) Options
type Unmarshalers struct { ... }
func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers
동작 차이점
주요 변경 사항
-
잘못된 UTF-8에 대한 오류 보고
-
JSON 객체에 중복 이름이 있을 경우 오류 보고
-
nil Go 슬라이스나 Go 맵을 각각 빈 JSON 배열이나 JSON 객체로 marshaling
-
대소문자 구분 매치를 사용하여 JSON 객체를 Go 구조체로 unmarshaling
-
omitempty 태그 옵션 재정의: “빈” JSON 값(null, “”, , {})으로 인코딩될 필드를 생략
-
time.Duration 직렬화 시 오류 보고: 현재 기본 표현이 없으므로 호출자가 결정할 수 있는 옵션 제공
호환성 옵션
대부분의 동작 변경에 대해 v1이나 v2 의미론으로 작동하도록 구성할 수 있는 구조체 태그 옵션이나 호출자 지정 옵션이 제공됩니다.
성능 최적화
성능 비교
-
Marshal 성능: v2는 v1과 대략 동등한 성능 (때로는 약간 빠르거나 느림)
-
Unmarshal 성능: v2가 v1보다 상당히 빠름, 벤치마크에서 최대 10배 개선 입증
성능 향상 방법
더 큰 성능 향상을 얻으려면 기존 Marshaler와 Unmarshaler 구현을 MarshalerTo와 UnmarshalerFrom도 구현하도록 마이그레이션해야 합니다.
실제 사례
Kubernetes의 특정 서비스에서 OpenAPI 사양의 재귀적 파싱이 UnmarshalJSON 메서드에서 성능을 크게 저하시켰으나(kubernetes/kube-openapi#315), UnmarshalJSONFrom으로 전환하여 성능이 몇 배나 향상되었습니다.
encoding/json의 소급 개선
구현 통합
Go 표준 라이브러리에서 두 개의 별도 JSON 구현을 피하기 위해 v1을 v2를 기반으로 구현합니다.
이점
점진적 마이그레이션
v1 또는 v2의 Marshal 및 Unmarshal 함수는 v1 또는 v2 의미론에 따라 작동하는 기본 동작 세트를 나타냅니다. 완전히 v1부터 완전히 v2까지의 다양한 조합으로 구성할 수 있는 옵션을 지정할 수 있습니다.
기능 상속
v2에 역호환 기능이 추가되면 v1에서도 본질적으로 사용할 수 있게 됩니다. 예를 들어:
-
새로운 구조체 태그 옵션 (inline, format)
-
MarshalJSONTo와 UnmarshalJSONFrom 인터페이스 메서드 지원
유지보수 부담 감소
널리 사용되는 패키지의 유지보수는 상당한 노력이 필요합니다. v1과 v2가 동일한 구현을 사용하면 유지보수 부담이 줄어들며, 일반적으로 단일 변경으로 두 버전 모두에서 버그 수정, 성능 개선 또는 기능 추가가 가능합니다.
호환성 보장
Go 프로젝트는 v1에 대한 지원을 중단하지 않습니다. v2로의 마이그레이션이 권장되지만 필수는 아닙니다.
jsonv2 실험
실험 활성화
encoding/json/jsontext와 encoding/json/v2 패키지의 새로운 API는 기본적으로 표시되지 않습니다. 사용하려면:
-
환경에서
GOEXPERIMENT=jsonv2설정 -
또는
goexperiment.jsonv2빌드 태그 사용
테스트 방법
코드를 변경하지 않고도 jsonv2에서 테스트를 실행할 수 있습니다:
GOEXPERIMENT=jsonv2 go test ./...
실험의 중요성
-
API는 불안정하며 향후 변경될 수 있음
-
구현은 고품질이며 여러 주요 프로젝트에서 프로덕션에 성공적으로 사용됨
-
Go 1.25에서 실험이 되는 것은 encoding/json/jsontext와 encoding/json/v2를 표준 라이브러리에 공식적으로 채택하는 길의 중요한 이정표
참여 방법
v2의 reimplement된 v1은 Go 1 호환성 약속 범위 내에서 동일한 동작을 제공하는 것을 목표로 하지만, 오류 메시지의 정확한 문구와 같은 일부 차이점을 관찰할 수 있습니다.
피드백 제출: go.dev/issue/71497에서 경험을 공유하여 Go의 미래를 결정하는 데 도움을 주세요.
실험 결과 가능성
이 실험의 결과는 다음과 같을 수 있습니다:
-
노력의 포기
-
Go 1.26의 안정적인 패키지로 채택
-
기타 중간 결과
여러분의 피드백이 다음 단계를 결정할 것입니다.