GoSuda

Go 1.25 encoding/json v1 vs v2 비교

By lemonmint
views ...

Go의 encoding/json 패키지 v2는 기존 v1의 여러 단점(일관성 부족, 놀라운 동작, 성능 문제)을 개선하기 위한 새로운 구현입니다. 이는 goexperiment.jsonv2 빌드 태그를 통해 활성화되는 실험적인 기능입니다.

가장 중요한 점은 v2가 활성화되면 v1은 v2 구현 위에 v1의 동작을 에뮬레이션하는 호환성 레이어로 동작한다는 것입니다. 이는 v2_v1.go 파일의 DefaultOptionsV1() 함수를 통해 이루어집니다. 즉, v2는 v1의 동작을 완벽히 재현할 수 있는 옵션을 제공하며, 동시에 더 엄격하고 예측 가능한 새로운 기본 동작을 제시합니다.

v2의 주요 목표는 다음과 같습니다.

  1. 정확성 및 예측 가능성 향상: 기본적으로 더 엄격한 규칙(예: 대소문자 구분, 중복 키 금지)을 적용하여 예기치 않은 동작을 줄입니다.
  2. 성능 개선: 파싱 및 인코딩 엔진을 재설계하여 효율성을 높였습니다.
  3. 유연성 및 제어권 확대: 상세한 옵션 시스템을 도입하여 개발자가 JSON 처리 방식을 세밀하게 제어할 수 있도록 합니다.

주요 의미/동작 차이점

v2_test.go 파일을 중심으로 v1과 v2의 동작 차이를 항목별로 정리했습니다.

1. 필드 이름 매칭 (대소문자 구분)

  • v1 동작: JSON 객체 멤버를 Go 구조체 필드에 언마샬링할 때, **대소문자를 무시(case-insensitive)**하여 매칭합니다. "FirstName""firstname" 모두 FirstName 필드에 매핑됩니다.
  • v2 동작: 기본적으로 **대소문자를 구분(case-sensitive)**하여 정확하게 일치하는 필드만 매핑합니다.
  • 변경 이유: 대소문자 무시 매칭은 예기치 않은 동작의 원인이 될 수 있고, 일치하지 않는 필드를 처리할 때 성능 저하를 유발합니다. v2는 더 명확하고 예측 가능한 동작을 기본으로 채택했습니다.
  • 관련 옵션: v2에서는 json:"...,case:ignore" 태그 옵션을 사용해 필드별로 대소문자 무시를 명시적으로 활성화하거나, json.MatchCaseInsensitiveNames(true) 옵션을 전역으로 적용할 수 있습니다.

2. omitempty 태그 옵션의 의미 변경

  • v1 동작: Go 값의 "빈(empty) 상태"를 기준으로 필드를 생략합니다. 여기서 "빈 상태"란 false, 0, nil 포인터/인터페이스, 길이가 0인 배열/슬라이스/맵/문자열을 의미합니다.
  • v2 동작: 인코딩된 JSON 값의 "빈 상태"를 기준으로 필드를 생략합니다. 즉, null, "", {}, []로 인코딩될 경우 생략됩니다.
  • 변경 이유: v1의 정의는 Go의 타입 시스템에 종속적입니다. v2는 JSON 타입 시스템을 기준으로 하여 더 일관된 동작을 제공합니다. 예를 들어 v1에서는 bool 타입의 false 값이 생략되지만, v2에서는 false는 비어있지 않은 JSON 값이므로 생략되지 않습니다. v2에서는 omitzero 옵션을 추가하여 v1의 omitempty0이나 false에 적용되던 동작을 대체할 수 있습니다.
  • 관련 옵션: v2에서 v1과 동일한 동작을 원할 경우 json.OmitEmptyWithLegacyDefinition(true) 옵션을 사용합니다.

3. string 태그 옵션의 동작 변경

  • v1 동작: 숫자, 불리언, 문자열 타입 필드에 적용됩니다. 해당 값을 JSON 문자열 안에 다시 인코딩합니다 (예: int(42) -> "42"). 복합 타입(슬라이스, 맵 등) 내부의 값에는 재귀적으로 적용되지 않습니다.
  • v2 동작: 숫자 타입에만 적용되며, 재귀적으로 적용됩니다. 즉, []int와 같은 슬라이스 내부의 숫자들도 모두 JSON 문자열로 인코딩됩니다.
  • 변경 이유: string 옵션의 주된 용도는 64비트 정수의 정밀도 손실을 막기 위해 숫자를 문자열로 표현하는 것입니다. v1의 동작은 제한적이고 일관성이 부족했습니다. v2는 이 핵심 용도에 집중하고 동작을 재귀적으로 확장하여 더 유용하게 만들었습니다.
  • 관련 옵션: json.StringifyWithLegacySemantics(true) 옵션으로 v1의 동작을 흉내 낼 수 있습니다.

4. nil 슬라이스 및 맵 마샬링

  • v1 동작: nil 슬라이스와 nil 맵은 null로 마샬링됩니다.
  • v2 동작: 기본적으로 nil 슬라이스는 [](빈 배열), nil 맵은 {}(빈 객체)로 마샬링됩니다.
  • 변경 이유: nil은 Go 언어의 구현 세부사항이며, 언어에 구애받지 않는 JSON 형식에 이를 노출하는 것은 바람직하지 않습니다. 빈 컬렉션을 나타내는 []{}가 더 보편적인 표현입니다.
  • 관련 옵션: v2에서 json.FormatNilSliceAsNull(true) 또는 json.FormatNilMapAsNull(true) 옵션을 통해 v1처럼 null로 마샬링할 수 있습니다.

5. 배열 언마샬링

  • v1 동작: Go 배열([N]T)로 언마샬링할 때, JSON 배열의 길이가 Go 배열의 길이와 달라도 오류를 발생시키지 않습니다. 길이가 짧으면 남는 공간은 제로 값으로 채우고, 길면 초과분은 버립니다.
  • v2 동작: JSON 배열의 길이가 Go 배열의 길이와 정확히 일치해야 합니다. 그렇지 않으면 오류가 발생합니다.
  • 변경 이유: Go에서 고정 크기 배열은 그 길이가 중요한 의미를 갖는 경우가 많습니다. v1의 동작은 데이터의 소리 없는 손실을 유발할 수 있습니다. v2는 더 엄격한 규칙으로 정확성을 높였습니다.
  • 관련 옵션: json.UnmarshalArrayFromAnyLength(true) 옵션으로 v1의 동작을 흉내 낼 수 있습니다.

6. time.Duration 처리

  • v1 동작: time.Duration은 내부적으로 int64로 취급되어 나노초 단위의 JSON 숫자로 인코딩됩니다.
  • v2 동작: time.Duration.String() 메서드를 사용하여 "1h2m3s"와 같은 형식의 JSON 문자열로 인코딩됩니다.
  • 변경 이유: 숫자 나노초는 가독성이 떨어지며, time.Duration의 표준 문자열 표현이 더 유용합니다.
  • 관련 옵션: json:",format:nano" 태그 옵션이나 json.FormatTimeWithLegacySemantics(true) 옵션을 통해 v1의 동작을 사용할 수 있습니다.

7. 유효하지 않은 UTF-8 처리

  • v1 동작: 마샬링/언마샬링 시 문자열 내에 유효하지 않은 UTF-8 바이트가 있으면 유니코드 대체 문자(\uFFFD)로 조용히 교체합니다.
  • v2 동작: 기본적으로 유효하지 않은 UTF-8을 만나면 오류를 반환합니다.
  • 변경 이유: 데이터의 소리 없는 손상을 방지하고, 더 엄격한 JSON 표준(RFC 7493)을 따르기 위함입니다.
  • 관련 옵션: jsontext.AllowInvalidUTF8(true) 옵션을 통해 v1의 동작을 흉내 낼 수 있습니다.

8. 중복된 객체 멤버 이름 처리

  • v1 동작: JSON 객체 내에 동일한 이름의 멤버가 중복되어 나타나는 것을 허용합니다. 마지막에 나타난 값으로 덮어씁니다.
  • v2 동작: 기본적으로 중복된 멤버 이름이 있으면 오류를 반환합니다.
  • 변경 이유: RFC 8259 표준은 중복 이름의 동작을 정의하지 않아 구현마다 다르게 동작할 수 있습니다. 이는 보안 취약점의 원인이 될 수 있습니다. v2는 이를 명시적으로 거부하여 정확성과 보안을 높였습니다.
  • 관련 옵션: jsontext.AllowDuplicateNames(true) 옵션으로 v1의 동작을 흉내 낼 수 있습니다.

구현 및 아키텍처 차이점

  • v1: decode.godecodeStatescanner.go의 수동으로 작성된 상태 머신(state machine)에 크게 의존합니다. 이는 파싱 로직과 의미 분석이 강하게 결합된 모놀리식 구조입니다.
  • v2: 아키텍처가 더 모듈화되었습니다.
    • encoding/json/jsontext: 저수준의 고성능 JSON 토크나이저(Decoder)와 인코더(Encoder)를 제공합니다. 이 패키지는 JSON의 구문(syntax)적인 측면에만 집중합니다.
    • encoding/json/v2: jsontext를 기반으로 Go 타입과 JSON 값 사이의 의미(semantic) 변환을 처리합니다.
    • 이러한 분리를 통해 구문 분석과 의미 분석이 분리되어 코드의 명확성과 성능이 향상되었습니다.

v2의 새로운 API 및 기능

v2는 json.Options 시스템을 통해 매우 유연한 제어 기능을 제공합니다.

  • json.Options: 마샬링/언마샬링 동작을 변경하는 옵션들의 집합입니다.
  • json.JoinOptions(...): 여러 옵션을 하나로 병합합니다.
  • WithMarshalers / WithUnmarshalers: Marshaler/Unmarshaler 인터페이스를 구현하지 않고도 특정 타입에 대한 직렬화/역직렬화 로직을 주입할 수 있는 강력한 기능입니다. 이는 외부 패키지의 타입을 처리할 때 특히 유용합니다.
  • 새로운 옵션들: RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull 등 v1에서는 불가능했던 다양한 동작 제어가 가능해졌습니다.

결론

encoding/json v2는 v1의 경험을 바탕으로 정확성, 성능, 유연성을 크게 향상시킨 현대적인 구현입니다. 기본 동작이 더 엄격해졌지만, 정교한 Options 시스템을 통해 v1의 모든 동작을 완벽하게 지원하므로 기존 코드와의 호환성을 유지하면서 점진적으로 v2의 장점을 도입할 수 있습니다.

  • 새로운 프로젝트라면 v2를 기본으로 사용하는 것이 좋습니다.
  • 기존 프로젝트jsonv1을 그대로 사용하거나, jsonv2로 마이그레이션하면서 DefaultOptionsV1()을 통해 점진적으로 v2의 엄격한 동작을 도입하는 전략을 사용할 수 있습니다.