[Golang] JSON marshal/unmarshal시 값의 오차

어떤 정보를 주고 받을때 JSON를 자주 사용합니다.
Golang은 JSON을 처리하기 위해 데이터를 JSON으로 Marshal, Unmarshal을 거칩니다.
이번 포스트에서는 큰 숫자값에 대해서 Marshal, Unmarshal시 값의 차이가 생겼던 경험과 해결에 대해서 적습니다.

기본 문법

Golang에서 map형태를 만드는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"encoding/json"
"fmt"
"math"
)

func main() {
maxInt64 := math.MaxInt64
mapObject := make(map[string]interface{})
mapObject["int64"] = maxInt64

fmt.Println(mapObject)
}
1
2
// 결과
map[int64:9223372036854775807]

API를 만드는 입장에서 연동하는 외부의 JSON의 모든 자료형을 처리해야 하는 경우가 많기 때문에
map[string]interface{}로 지정해서 mapkeystring이고 value는 모든 자료형을 담을 수 있도록 interface{}로 지정합니다.
이 말은 vlaue의 자료형을 명시하지 않는다는 것과 같습니다.
이렇게 만들어진 데이터를 byte arraymarshal 작업을 통해 전송하는게 일반적입니다.

json.Marshal()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"encoding/json"
"fmt"
"math"
)

func main() {
maxInt64 := math.MaxInt64
mapObject := make(map[string]interface{})
mapObject["int64"] = maxInt64

// marshal
var bytes []byte
var err error
if bytes, err = json.Marshal(mapObject); err != nil {
fmt.Println(err.Error())
}

fmt.Println(bytes)
}
1
2
// 결과
[123 34 105 110 116 54 52 34 58 57 50 50 51 51 55 50 48 51 54 56 53 52 55 55 53 56 48 55 125]

json.Unmarshal()

데이터를 받아 처리하는 입장에서는 byte arrayunmarshal하여 구조화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"encoding/json"
"fmt"
"math"
)

func main() {
maxInt64 := math.MaxInt64
mapObject := make(map[string]interface{})
mapObject["int64"] = maxInt64

// marshal
var bytes []byte
var err error
if bytes, err = json.Marshal(mapObject); err != nil {
fmt.Println(err.Error())
}
//fmt.Println(bytes)

// unmarshal
unmarshalMapObject := make(map[string]interface{})
if err := json.Unmarshal(bytes, &unmarshalMapObject); err != nil {
fmt.Println(err.Error())
}
fmt.Println(unmarshalMapObject)
}
1
2
3
// 결과
map[int64:9223372036854775807]
map[int64:9.223372036854776e+18]

문제 확인

문제점을 발견했나요?
9223372036854775807의 값이 JSON marshal/unmarshal을 거치니 9.223372036854776e+18로 표현되고있습니다.
분명 int64의 값을 담아 전송했고 받은 사람은 map[string]interface{}unmarshal했지만 Println의 호출된 결과가 달라진 것이죠.
map안의 int64값을 확인하기 위해 int64로 형변환하여 확인한 결과는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"encoding/json"
"fmt"
"math"
)

func main() {
maxInt64 := math.MaxInt64
mapObject := make(map[string]interface{})
mapObject["int64"] = maxInt64

// marshal
var bytes []byte
var err error
if bytes, err = json.Marshal(mapObject); err != nil {
fmt.Println(err.Error())
}
//fmt.Println(bytes)

// unmarshal
unmarshalMapObject := make(map[string]interface{})
if err := json.Unmarshal(bytes, &unmarshalMapObject); err != nil {
fmt.Println(err.Error())
}
fmt.Println(unmarshalMapObject)

// 형변환 확인
unmarshalMaxInt64 := unmarshalMapObject["int64"].(int64)
fmt.Println(unmarshalMaxInt64)
}
1
2
3
4
5
6
7
8
//결과
map[int64:9223372036854775807]
map[int64:9.223372036854776e+18]
panic: interface conversion: interface {} is float64, not int64

goroutine 1 [running]:
main.main()
/main.go:20 +0x3ba

이 에러를 해석해보면
unmarshal 하는 과정에서 int64가 아닌 float64형태로 암시적으로 변환된다는 얘기입니다.
그렇다면 float64로 형 변환 후 강제로 int64로 변환하면 어떻게 될까요?

강제로 int64 형변환 후 값

1
2
3
// ...
unmarshalMaxInt64 := int64(unmarshalMapObject["int64"].(float64))
fmt.Println(unmarshalMaxInt64)
1
2
// 결과
-9223372036854775808

산넘어 산

우리가 기대한 결과와는 전혀 다른 엉뚱한 값이 결과로 나왔습니다.

Overflow가 발생한 것으로 보임

int64의 최대 값에서 갑자기 음수가 되었다는 것은 Overflow가 발생한 것으로 보입니다.
앞의 과정에서 int64의 원본 값과 float64로 변환된 값을 보면 800에서 반올림이 발생하였네요..
이것은 큰 문제를 야기합니다.

해결 방법

진행중인 프로젝트에서 이렇게 큰 수를 사용할일은 없다고 정책적으로 말하고 있지만..
개발자 입장에서 운영단계에서의 이런 Overflow 발생으로 문제가 터진다면 골치아플게 뻔합니다.
따라서 해결 방법을 찾아보기로 했습니다.

vlaue의 자료형을 interface{} 대신 명시적으로 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"encoding/json"
"fmt"
"math"
)

func main() {
var maxInt64 int64
maxInt64 = math.MaxInt64
mapObject := make(map[string]int64)
mapObject["int64"] = maxInt64

// marshal
var bytes []byte
var err error
if bytes, err = json.Marshal(mapObject); err != nil {
fmt.Println(err.Error())
}
//fmt.Println(bytes)

// unmarshal
unmarshalMapObject := make(map[string]int64)
if err := json.Unmarshal(bytes, &unmarshalMapObject); err != nil {
fmt.Println(err.Error())
}
fmt.Println(unmarshalMapObject)
}
1
2
3
// 결과
map[int64:9223372036854775807]
map[int64:9223372036854775807]

하지만 위의 방법은 서두에 설명한 것과 같이 API를 개발하는 입장에서 map의 value에 어떤 자료형으로 들어올지 알 수 없기 때문에 실질적으로 사용이 어렵습니다.
제 입장에서 이 방법으로 해결하는건 옳지 않았습니다.

json decoder, UserNumber()를 이용해 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"encoding/json"
"fmt"
"math"
"strings"
)

func main() {
maxInt64 := math.MaxInt64
mapObject := make(map[string]interface{})
mapObject["int64"] = maxInt64

// marshal
var bytes []byte
var err error
if bytes, err = json.Marshal(mapObject); err != nil {
fmt.Println(err.Error())
}
fmt.Println(mapObject)

// decode
d := json.NewDecoder(strings.NewReader(string(bytes)))
d.UseNumber()
var decodeMapObject map[string]interface{}
if err := d.Decode(&decodeMapObject); err != nil {
fmt.Println(err.Error())
}
fmt.Println(decodeMapObject)

// 형변환 테스트
var castedMaxInt64 int64
if castedMaxInt64, err = decodeMapObject["int64"].(json.Number).Int64(); err != nil {
fmt.Println(err.Error())
}
fmt.Println(castedMaxInt64)
}
1
2
3
4
// 결과
map[int64:9223372036854775807]
map[int64:9223372036854775807]
9223372036854775807

이 방법이 decode 했을때도 값이 float64형태로 변하지 않고 int64 형변환도 문제없이 작동합니다.
데이터를 json.Number로 형변환 후 다시 Int64()를 호출해서 자료형을 변환해야 하지만 Overflow가 일어나는 것 보다 훨씬 낫군요.
이 방법으로 해결 방향을 잡았습니다.

결론

위의 문제는 프로젝트가 개발기간 중간에 발견한 문제입니다.
복잡한 로직이 한창 개발되는 상황에서 기초 함수에서 발견된 문제라 자칫 미궁으로 빠질수도 있었던 상황이였는데 정리해 두고 시간이 나는 지금에서야 블로그에 올리게 되었습니다.
항상 단위테스트를 통해 개발하는 습관을 가지고 경계값들도 꾸준히 체크한다면 사전에 방지할 수 있는 문제라고 생각됩니다.