
Go 1.25 JSON v2 완벽 가이드 변화된 기능과 성능 분석
Go 1.25에 포함될 json 패키지의 두 번째 버전은 단순한 업데이트가 아닌 대격변 수준의 변화를 예고하고 있는데요.
새로운 기능의 추가는 물론이고 기존 API의 문제점 수정, 그리고 놀라운 성능 향상까지 포함된 대규모 업데이트입니다.
다만 이 과정에서 기존 코드와 호환되지 않는 변경 사항(Breaking Changes)도 다수 포함되어 있어 주의가 필요한데요.
지금부터 무엇이 바뀌었고 어떻게 준비해야 하는지 하나씩 살펴보겠습니다.
가장 먼저 안심해도 될 점은 Marshal과 Unmarshal을 사용하는 기본적인 패턴은 그대로 유지된다는 것입니다.
아래 코드는 v1과 v2 모두에서 완벽하게 동작합니다.
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 25}
// Alice 마샬링
b, err := json.Marshal(alice)
fmt.Println(string(b), err)
// Alice 언마샬링
err = json.Unmarshal(b, &alice)
fmt.Println(alice, err)
하지만 이 부분을 제외한 나머지는 꽤 많이 달라졌는데요. v1과 비교했을 때 주요 변경 사항들을 핵심 위주로 정리해 드립니다.
쓰기와 읽기(Write/Read)
v1에서는 io.Writer에 마샬링하거나 io.Reader에서 언마샬링할 때 Encoder와 Decoder를 사용해야 했는데요.
이는 중간 단계가 필요해 다소 번거로운 면이 있었습니다.
// v1 방식: Encoder/Decoder 사용
// Alice 마샬링
alice := Person{Name: "Alice", Age: 25}
out := new(strings.Builder) // io.Writer
enc := json.NewEncoder(out)
enc.Encode(alice)
fmt.Println(out.String())
// Bob 언마샬링
in := strings.NewReader(`{"Name":"Bob","Age":30}`) // io.Reader
dec := json.NewDecoder(in)
var bob Person
dec.Decode(&bob)
fmt.Println(bob)
이제 v2에서는 MarshalWrite와 UnmarshalRead를 통해 중간 객체 없이 직접 스트림을 처리할 수 있게 되었습니다.
// v2 방식: 직접 쓰기/읽기
// Alice 마샬링
alice := Person{Name: "Alice", Age: 25}
out := new(strings.Builder)
json.MarshalWrite(out, alice)
fmt.Println(out.String())
// Bob 언마샬링
in := strings.NewReader(`{"Name":"Bob","Age":30}`)
var bob Person
json.UnmarshalRead(in, &bob)
fmt.Println(bob)
여기서 주의할 점은 두 방식이 완전히 동일하게 동작하지는 않는다는 것인데요.
MarshalWrite는 기존 Encoder.Encode와 달리 끝에 개행 문자(newline)를 추가하지 않습니다.
또한 UnmarshalRead는 io.EOF를 만날 때까지 리더의 모든 내용을 읽어들이는 반면, 기존 Decoder.Decode는 다음 JSON 값 하나만 읽는다는 차이가 있습니다.
인코딩과 디코딩(Encode/Decode)
기존의 Encoder와 Decoder 타입은 새로운 jsontext 패키지로 이동했으며, 저수준 스트리밍 작업을 지원하기 위해 인터페이스가 크게 변경되었습니다.
이제 JSON 스트림을 읽고 쓰려면 json 패키지의 함수와 jsontext의 타입을 함께 사용해야 하는데요. 매핑 관계는 다음과 같습니다.
- v1
Encoder.Encode→ v2json.MarshalEncode+jsontext.Encoder - v1
Decoder.Decode→ v2json.UnmarshalDecode+jsontext.Decoder
스트리밍 인코더의 사용 예시는 다음과 같습니다.
people := []Person{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 30},
{Name: "Cindy", Age: 15},
}
out := new(strings.Builder)
enc := jsontext.NewEncoder(out)
for _, p := range people {
json.MarshalEncode(enc, p)
}
fmt.Print(out.String())
스트리밍 디코더 역시 비슷한 방식으로 동작합니다.
in := strings.NewReader(`
{"Name":"Alice","Age":25}
{"Name":"Bob","Age":30}
{"Name":"Cindy","Age":15}
`)
dec := jsontext.NewDecoder(in)
for {
var p Person
// 호출할 때마다 Person 객체 하나씩 디코딩
err := json.UnmarshalDecode(dec, &p)
if err == io.EOF {
break
}
fmt.Println(p)
}
UnmarshalRead와 달리 UnmarshalDecode는 호출할 때마다 값 하나씩을 순차적으로 디코딩하는 진정한 스트리밍 방식을 지원합니다.
옵션(Options)
v2에서는 마샬링과 언마샬링 동작을 제어하기 위한 다양한 '옵션' 기능이 대폭 강화되었는데요.
주요 옵션들은 다음과 같습니다.
FormatNilMapAsNull,FormatNilSliceAsNull: nil 맵과 슬라이스를 어떻게 인코딩할지 정의합니다.MatchCaseInsensitiveNames: 필드명 매칭 시 대소문자를 구분하지 않도록 설정합니다.Multiline: JSON 객체를 여러 줄로 펼쳐서 출력합니다.OmitZeroStructFields: 제로 값(zero values)을 가진 필드를 출력에서 제외합니다.SpaceAfterColon,SpaceAfterComma: 콜론이나 쉼표 뒤에 공백을 추가합니다.StringifyNumbers: 숫자 타입을 문자열로 변환하여 출력합니다.WithIndent,WithIndentPrefix: 중첩된 속성에 들여쓰기를 적용합니다(기존MarshalIndent함수는 제거됨).
이러한 옵션들은 함수 호출 시 인자로 전달하여 사용합니다.
alice := Person{Name: "Alice", Age: 25}
b, _ := json.Marshal(
alice,
json.OmitZeroStructFields(true),
json.StringifyNumbers(true),
jsontext.WithIndent(" "),
)
fmt.Println(string(b))
여러 옵션을 JoinOptions를 사용해 하나로 묶어서 전달할 수도 있습니다.
alice := Person{Name: "Alice", Age: 25}
opts := json.JoinOptions(
jsontext.SpaceAfterColon(true),
jsontext.SpaceAfterComma(true),
)
b, _ := json.Marshal(alice, opts)
fmt.Println(string(b))
태그(Tags)
v2는 omitzero, omitempty, string, -와 같은 v1의 태그들을 그대로 지원하는데요.
여기에 더해 더욱 강력한 기능을 제공하는 새로운 태그들이 추가되었습니다.
case:ignore또는case:strict: 대소문자 처리 방식을 지정합니다.format:template: 템플릿에 맞춰 필드 값을 포맷팅합니다.inline: 중첩된 객체의 필드를 부모 객체 레벨로 끌어올려 평탄화(flatten)합니다.unknown: 정의되지 않은 필드들을 잡아내는 '캐치올(catch-all)' 역할을 합니다.
특히 inline과 format 태그의 활용 예시는 다음과 같습니다.
type Person struct {
Name string `json:"name"`
// 날짜를 yyyy-mm-dd 형식으로 포맷팅
BirthDate time.Time `json:"birth_date,format:DateOnly"`
// Address의 필드들을 Person 객체 안으로 인라인(평탄화)
Address `json:",inline"`
}
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
unknown 태그를 사용하면 구조체에 정의되지 않은 여분의 JSON 필드들을 별도 맵에 모아서 처리할 수 있어 매우 유용합니다.
type Person struct {
Name string `json:"name"`
// 알 수 없는 필드들을 Data 맵에 수집
Data map[string]any `json:",unknown"`
}
커스텀 마샬링(Custom marshaling)
Marshaler와 Unmarshaler 인터페이스를 구현하는 기존 방식도 여전히 유효한데요.
아래 코드는 v1과 v2 모두에서 잘 동작합니다.
// true는 "✓", false는 "✗"로 표현하는 커스텀 타입
type Success bool
func (s Success) MarshalJSON() ([]byte, error) {
if s {
return []byte(`"✓"`), nil
}
return []byte(`"✗"`), nil
}
하지만 Go 표준 라이브러리 문서는 성능상의 이점을 위해 새로운 MarshalerTo와 UnmarshalerFrom 인터페이스 사용을 권장하고 있습니다.
이들은 순수하게 스트리밍 방식으로 동작하기 때문입니다.
func (s Success) MarshalJSONTo(enc *jsontext.Encoder) error {
if s {
return enc.WriteToken(jsontext.String("✓"))
}
return enc.WriteToken(jsontext.String("✗"))
}
더 혁신적인 변화는 제네릭 함수인 MarshalFunc와 UnmarshalFunc를 사용해 커스텀 타입을 만들지 않고도 특정 타입의 마샬링 로직을 주입할 수 있다는 점입니다.
예를 들어 bool 타입을 별도의 래퍼 타입 없이 커스텀하게 처리할 수 있습니다.
// bool 값을 위한 커스텀 마샬러 생성
boolMarshaler := json.MarshalFunc(
func(val bool) ([]byte, error) {
if val {
return []byte(`"✓"`), nil
}
return []byte(`"✗"`), nil
},
)
// 옵션으로 전달
val := true
data, err := json.Marshal(val, json.WithMarshalers(boolMarshaler))
여러 마샬러를 JoinMarshalers로 결합하여, 특정 조건(예: "on"/"off" 문자열)에 따라 다르게 동작하도록 체이닝할 수도 있습니다.
SkipFunc 에러를 반환하면 다음 마샬러로 넘어가는 유연함까지 갖췄습니다.
기본 동작의 변화(Default behavior)
v2는 API뿐만 아니라 기본 동작(Default behavior)에서도 몇 가지 중요한 변화가 있었는데요.
마이그레이션 시 가장 주의해야 할 부분들입니다.
마샬링 차이점:
- v1은 nil 슬라이스를
null로 변환했지만, v2는[]로 변환합니다. (FormatNilSliceAsNull옵션으로 변경 가능) - v1은 nil 맵을
null로 변환했지만, v2는{}로 변환합니다. (FormatNilMapAsNull옵션으로 변경 가능) - v1은 바이트 배열을 숫자 배열로 변환했지만, v2는 Base64 문자열로 변환합니다. (
format:array태그로 변경 가능) - v2는 문자열 내의 유효하지 않은 UTF-8 문자를 허용하지 않습니다.
언마샬링 차이점:
- v1은 필드명 매칭 시 대소문자를 구분하지 않았지만, v2는 정확히 일치해야 합니다. (
MatchCaseInsensitiveNames옵션으로 변경 가능) - v1은 중복된 필드를 허용했지만, v2는 허용하지 않습니다. (
AllowDuplicateNames옵션으로 변경 가능)
이러한 변화는 더 엄격하고 예측 가능한 JSON 처리를 위한 것이지만, 기존 시스템과의 호환성을 깰 수 있으므로 옵션을 적절히 사용하여 v1의 동작을 모방해야 할 수도 있습니다.
성능(Performance)
마샬링 성능은 v1과 비슷하거나 데이터셋에 따라 소폭의 차이가 있지만, 언마샬링 성능은 비약적으로 향상되었습니다.
v2의 언마샬링은 v1 대비 약 2.7배에서 10.2배까지 더 빠르다고 보고되고 있습니다.
또한 MarshalJSON 대신 스트리밍 기반의 MarshalJSONTo를 사용하면 특정 시나리오에서 시간 복잡도를 O(n²)에서 O(n)으로 줄일 수 있는데요.
실제로 쿠버네티스(k8s) OpenAPI 명세 처리에서 40배 이상의 속도 향상을 기록하기도 했습니다.
마치며
지금까지 Go 1.25의 json v2 패키지의 방대한 변화를 살펴봤는데요. json과 jsontext 서브 패키지로 분리되면서 구조는 다소 복잡해졌지만, 그만큼 더 강력한 기능과 유연성, 그리고 압도적인 성능을 제공합니다.
마지막으로 기억해야 할 점은 Go 1.25 시점에서 json/v2 패키지는 실험적 기능이라는 것입니다.
빌드 시 GOEXPERIMENT=jsonv2 태그를 설정해야 활성화되며, API는 향후 릴리스에서 변경될 수 있습니다.
이 태그를 활성화하면 기존 v1 패키지도 내부적으로 새로운 구현체를 사용하게 되어 성능 향상 효과를 누릴 수 있습니다.
'Go' 카테고리의 다른 글
| 예외 처리가 없는 Go 언어 구시대적 발상일까 혁신일까 (0) | 2026.04.01 |
|---|---|
| 당신의 URL이 구린 이유와 세련된 API 디자인을 위한 황금률 (0) | 2026.03.28 |
| Go 언어 부동소수점 완벽 정복 오차 없는 계산을 위한 필독 가이드 (0) | 2025.10.18 |
| Go 언어 로깅 완벽 가이드, 라이브러리 4가지 비교 분석 (0) | 2025.10.18 |
| Go 1.25 신기능 총정리 실전 예제와 마이그레이션 가이드 (4) | 2025.08.24 |