Trawling

Go 1.27의 JSON v2, 구문 처리와 Go 값 변환을 분리

Go 1.27에는 `encoding/json/v2`와 `encoding/json/jsontext`가 안정판으로 포함됐습니다. `jsontext`는 JSON 토큰과 값을 스트리밍 처리하고, `json/v2`는 이를 Go 값과 연결합니다. 옵션과 사용자 정의 메서드도 이 구조를 따르며, 기존 `encoding/json`이 계속 지원돼 호출부별로 이전할 수 있습니다.

본문

Go 블로그는 2026년 11월 4일 encoding/json/v2와 encoding/json/jsontext를 소개하는 갱신 글을 게시했습니다. 두 패키지는 Go 1.27에서 안정판으로 제공됩니다. Go 1.27은 2026년 8월 공개됐으며, 11월 글은 앞서 소개한 실험판을 바탕으로 새 API의 설계를 설명합니다.

새 구조의 핵심은 JSON 문법 처리와 JSON 값·Go 값 사이의 변환을 나눈 데 있습니다. jsontext는 JSON의 문법에 따라 토큰과 값을 읽고 씁니다. 그 위의 json/v2는 Go 값을 JSON으로 마샬링하거나 JSON을 Go 값으로 언마샬링합니다. 기존 encoding/json이 한 패키지에서 두 역할을 맡았다면, 새 API는 각 계층의 역할을 드러냅니다.

구문 처리와 값 변환을 나눈 이유

기존 encoding/json은 임의의 Go 타입에 기본 JSON 표현을 부여하고 사용자 정의 메서드로 그 표현을 바꿀 수 있어 폭넓게 쓰였습니다. 하지만 기존 동작과 인터페이스가 Go 1 호환성 약속에 묶여 있어, 이미 그 동작에 의존하는 프로그램을 깨뜨리지 않고 API의 제약을 고치기 어려웠습니다. Go 프로젝트는 기존 패키지의 동작을 바꾸는 대신 새 이름 공간인 encoding/json/v2를 만들었습니다.

기존 사용자 정의 MarshalJSON 메서드는 완성된 JSON을 []byte로 반환합니다. UnmarshalJSON 메서드는 완전한 JSON 값 전체를 입력받습니다. 이에 따라 호출 패키지는 반환한 바이트가 유효한 JSON인지 검사하고 지정된 형식에 맞춰 다시 정리해야 합니다. 입력 쪽에서는 값의 끝을 찾으려고 먼저 파싱한 뒤 사용자 메서드가 내용을 다시 파싱합니다. 기존 Encoder와 Decoder는 JSON 값 전체를 메모리에 버퍼링했고, Decoder.Token은 할당이 많았으며 토큰을 쓰는 대응 API도 없었습니다.

jsontext는 이런 흐름에서 문법 처리를 맡습니다. NewEncoder는 io.Writer를 받아 Value나 Token을 쓰고, NewDecoder는 io.Reader에서 이를 읽습니다. 두 객체는 상태를 유지해 주고받는 토큰 흐름이 유효한 JSON 문법을 이루도록 처리합니다. 패키지는 JSON 문법만 다루므로 Go 리플렉션에 의존하지 않습니다. JSON 값은 []byte를 바탕으로 한 Value, 토큰은 생성자와 접근자를 갖춘 Token으로 표현됩니다.

입력과 출력 방식에 맞춰 호출부를 선택합니다

json/v2의 Marshal과 Unmarshal은 기존과 비슷한 바이트 슬라이스 기반 API입니다. MarshalWrite와 UnmarshalRead는 각각 Writer와 Reader를 직접 받아 별도 Encoder나 Decoder를 만들지 않고도 쓸 수 있습니다. MarshalEncode와 UnmarshalDecode는 jsontext.Encoder와 jsontext.Decoder를 받아 구문 처리 계층에 직접 연결합니다. 각 함수는 옵션을 인자로 받아 동작을 설정합니다.

옵션은 사용자 정의 타입의 처리에도 이어집니다. v2는 기존 Marshaler와 Unmarshaler 메서드를 유지하면서 MarshalJSONTo와 UnmarshalJSONFrom을 추가했습니다. 새 메서드는 Encoder나 Decoder를 받아 JSON을 스트리밍으로 처리합니다. 호출자가 정한 옵션은 Encoder와 Decoder의 접근자 메서드로 조회할 수 있어 사용자 정의 메서드 안에서도 이어서 사용할 수 있습니다. 기존 메서드만 구현한 타입도 계속 사용할 수 있지만, Go 프로젝트는 스트리밍 처리의 성능 이점을 얻으려면 새 인터페이스도 구현하도록 안내합니다.

Go 프로젝트가 소개한 벤치마크에서 v2의 마샬링 성능은 v1과 대체로 비슷했고, 언마샬링은 크게 빨라졌습니다. 일부 벤치마크에서는 언마샬링 성능이 최대 10배 개선됐습니다.

기본 동작도 달라졌습니다. v2는 잘못된 UTF-8과 중복된 객체 이름을 오류로 처리합니다. JSON 표준은 중복 이름의 처리 방식을 하나로 정하지 않아 구현마다 값을 선택하거나 병합·제거할 수 있습니다. v2는 Go 구조체 필드와 JSON 이름을 대소문자를 구분해 대응합니다. nil 슬라이스와 맵은 각각 null 대신 []와 {}로 직렬화합니다. Go 프로젝트는 동작 변화 대부분을 옵션이나 구조체 태그로 조정할 수 있게 했습니다.

기존 encoding/json도 계속 지원됩니다. Go 1.27 릴리스 노트에 따르면 기존 패키지는 v2 구현을 바탕으로 동작하면서 마샬링·언마샬링 동작을 보존합니다. 다만 오류 메시지의 정확한 문구는 달라질 수 있습니다. v2는 기존 MarshalJSON·UnmarshalJSON 메서드를 지원하고, v1은 내부적으로 새 스트리밍 메서드도 지원합니다. 이에 따라 의존 패키지를 한꺼번에 바꾸지 않고 특정 호출부부터 v2로 옮기거나, 필요한 동작에 v1 호환 옵션을 적용할 수 있습니다.

자료: Go Blog·Go 1.27 Release Notes / 정리·해설: Trawling

참고 자료

오류 제보