최근의 언어처리계라면 대부분 JSON 파싱은 표준 라이브러리로 처리할 수 있다. Go 언어도 표준 라이브러리에 encoding/json 패키지가 있어, JSON을 파싱할 수 있다.

 

대부분 JSON 파서는 객체형가 사전형 등 내장형 인스턴스를 생성하지만, Go 언어의 JSON 파서는 그와 달리 미리 만들어놓은 구조체에 대응하는 파서로 되어 있다. Go 언어에서 가장 많이 사용되는 구조체를 이용한 파싱을 알아보자.

 

1.0 Go 언어의 구조체 태그를 사용한 JSON 파싱

[도서 정보 JSON을 구조체로 변환]

package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Title string `json:"title"`
	Author string `json:"author"`
}

var jsonString = []byte (`
[
	{"title": "The Art of Community", "author": "Jono Baacon"},
	{"title": "Mithril", "author": "Yoshiki Shibukwa"}
]`)

func main() {
	var books []Book
	err := json.Unmarshal(jsonString, &books)
	if err != nil {
		panic(err)
	}
	for _,book := range books {
		fmt.Println(book)
	}
}

출력

 

1.1 생략됐는지 또는 제로 값인지 판정

 Go 언어는 변수를 반드시 기본값으로 초기화한다. 숫자 값인 경우는 0, 문자열이면 빈 문자열 ""로 초기화된다. 

type EditHistory struct {
	Id int `json: "id"`
	Name string `json: "name"`
	Price int `json: "price"`
}

위 구조체에 json.Unmarshal을 사용해 값ㄷ을 읽어오면, 가격이 0으로 설정된 것인지, 애초에 JSON에 포함되지 않았는지 구별할 수 없다. 이때, 포인터 변수로 만들어두면, JSON에 포함되지 않은 경우는 nil인지 판정할 수 있게 된다.

type EditHistory struct {
	Id int `json: "id"`
	Name *string `json: "name"`
	Price *int `json: "price"`
}

 

1.2 특별한 형 변환을 하고 싶을 때

JSON과 프로그램에서 다루고 싶은 표현이 항상 같은 것은 아니다. JSON에는 날짜를 나타내는 데이터형이 없지만, 프로그램 안에서는 각 언어의 날짜 표현 클래스나 구조체를 사용하고 싶은 경우를 생각해볼 수 있다. 그런 때는 직접 형을 정의하고 UnmarshalJSON() 메서드를 정의해서 형 변환을 구현할 수 있다. Go 언어에서 이 메서드를 가진 구조체는 json.Unmarshaler 인터페이스를 충족한다. Go 언어의 파서는 이 인터페이스를 가진 오브젝트에서는 이 변환 메서드를 사용한다.

[형 변환]

type DueDate struct {
	time.Time
}

func(d *DueDate) UnmarshalJSON(raw []byte) error {
	epoch, err := strconv.Atoi(string(raw))
	if err != nil {
		return err
	}
	d.Time = time.Unix(int64(epoch), 0)
	return nil
}

우선 type 선언으로 time.Time이 들어간 DueDate형을 만들었다. 이것으로 time.Time의 메서드도 모두 이용할 수 있게 된다.

 

다음으로 UnmarshalJSON() 메서드를 재정의한다. 이 메서드에는 바이트열이 전달되므로, 그 형의 규칙에 따라 해석하고 인스턴스를 초기화한다. 여기 전달되는 값은 에폭타임이므로 먼저 초를 나타내는 수치 형으로 변환하고, time.Unix() 함수로 time.Time 구조체의 오브젝트를 생성한다. 이제 해당 구조체를 사용할 구조체를 만든다. 아래를 참고하자.

type ToDo struct {
	Task string `json:"task"`
	Time DueDate `json:"due"`
}

var jsonString2 = []byte(`[
	{"task": "Go 언어 공부", "due": 140600200},
	{"task": "Java 언어 공부", "due": 140600400}
]`)

func main() {
	var todos []ToDo
	err := json.Unmarshal(jsonString2, &todos)
	if err != nil {
		panic(err)
	}
	for _,todo := range todos {
		fmt.Println(todo)
	}
}

UnmarshalJSON()을 구현하는 위치는 개개의 데이터 형뿐만 아니라, 하나 위 계층의 ToDo 구조체에 부여할 수도 있다.

 

2.0 JSON 응용하기

2.1 출력 시 출력을 가공하기

json_output처럼 구조체의 인스턴스를 json.Marshal()에 전달하면 JSON을 출력할 수 있다.

[JSON으로 출력하기]

	d, _ := json.Marshal(Book{"눈을 뜨자! JavaScript", "Cody Lindley"})
	log.Println(string(d))

출력

구조체에 붙인 태그는 읽기뿐만 아니라 쓰기에도 이용된다.

 

UnmarshalJSON()을 정의해 읽기 처리를 사용자화한 것처럼 MarshalJSON() 메서드를 정의하면 쓰기 처리도 사용화할 수 있다. ToDo에 종료 플래그 Done을 더해서 출력 시에 완료 항목은 JSON에서 제외하도록 해보자.

 

ToDo 배열을 ToDoList라는 형으로 선언하고, 이 형의 MarshalJSON을 정의한다. Go 언어에서는 type을 이용해 기존의 형 등을 바탕으로 새로운 형을 만든다. 배열도 형으로 만들 수 있다. 조금 전에 만든 Due 구조체에도 출력 변환 함수도 만들어두자.

[MarshalJSON 정의]

// 날짜 시리얼라이즈
func (d * DueDate) MarshalJSON() ([]byte, error) {
	return []byte(strconv.Itoa(int(d.Unix()))), nil
}

type ToDoList []ToDo

// 리스트를 필터링해서 시리얼라이즈
func (l ToDoList) MarshalJSON() ([]byte, error) {
	tmpList := make([]ToDo, 0, len(l))
	for _, todo := range l {
		if !todo.Done {
			tmpList = append(tmpList, todo)
		}
	}
	return json.Marshal(tmpList)
}

 

2.2 상황에 따라 형이 변하는 JSON 파싱

JSON이 단순한 구조체에 매핑할 수 있으면 되지만, API에 따라서는 유연한 해석이 필요한 경우도 있다. 반환되는 JSON의 data 속성은 이벤트 종류에 따라 달라진다. JSON이라는 데이터를 다룰 때 객체지향으로 설계된 공통 인터페이스를 공유하는 다른 종류의 오브젝트가 들어가는 경우는 있을 수 있다. 이 경우는 json.Unmarshal(), json.RawMessage로 극복할 수 있다. 

{
    "created": 14422888882,
    "data": {
        "email": null,
        "id": "cus_a16c7b03492343de4",
        "object": "customer"
    },
    "id": "event_54b3dfe32452435ccf",
    "object": "event",
    "type": "customer"
}

형이 정해지지 않은 데이터는 json.RawMessage로 해둔다. 이것은 []byte의 별칭이다. 파싱 도중에 일시정지한 상태가 일단 저장된다. 나머지는 종류별 변환 메서드를 준비해두면 사용자는 파싱된 데이터를 이용할 수 있다.

type Event struct {
	Created EpochDate `json:"create"`
	Data json.RawMessage `json:"data"`
	Id string `json:"id"`
	Object string `json:"object"`
	Types string `json:"type"`
}

 

만약 공통 인터페이스를 제공한다면, 공개용과 읽기 쓰기용 구조체를 나눠 대응할 수 있다. Data에는 공통 인터페이스를 가진 구조체가 들어간다. 이쪽이 코드는 길어지지만, Go언어가 갖춘 형에 의한 분기를 이용할 수 있으므로 이용하는 쪽 코드는 Go 언어다워 진다.

 

2.3 일반 데이터형으로 변환

조금 더 간단하게는 구조체 선언을 하지 않는 방법도 있다. Go언어에서 베리언트 형으로서 사용되는 interface() 형을 사용하면, 파서가 JSON의 데이터 구조에 맞는 인스턴스를 생성해준다.

[interface()로 json 데이터 받기]

func main() {
	var books []interface{}
	err := json.Unmarshal(jsonString, &books)
	if err != nil {
		log.Fatal(err)
	}
	for _, book := range books {
		log.Println(book)
	}
}

위 코드는 JSON에 어떤 배열이 들어 있다는 것을 전제로 interface{} 배열을 전달하고 있다. 물론, 오브젝트가 오는 것을 알고 있으면, map[string]interface{}를 사용할 수도 있다. JSON 규격에서는 자바스크립트와 같은 형을 사용하기에 수치는 float64밖에 없다. 따라서 정수로 다룰 수가 없다.

 

실제 코드 안에서 처리하려면 형 변환을 해야 한다. Go 언어의 형 변환에는 크게 두 종류가 있다. 특정 형식이 올 것을 알고 있을 경우는 변수 하나로 결과를 받을 수 있지만, 만약 다른 형의 데이터가 들어오면 오류를 일으켜 프로그램이 종료되어 버린다. 두 번째 형식이라면, 만약 다른 형이 들어온 경우에는 ok 변수에 false가 들어가는 대신 프로그램이 비정상적으로 종료되진 않게 된다. 

 

어느 형이 오는지 모르는 때는 switch 문을 사용할 수 있다. JSON을 트리형 데이터의 표현에 이용하는 경우는 이 방법을 사용하게 될 것이다.

	// 특정 형으로 캐스팅하기 (1)
	bookList := books.([]interface{})

	// 특정 형으로 캐스팅하기 (2)
	bookMap, ok := books.(map[string]interface{})

	// switch 문
	switch v := value.(type) {
	case bool:
		log.Println("bool", value)
	case float64:
		log.Println("float64", value)
	case string:
		log.Println("string", value)
	case map[string]interface{}:
		log.Println("map[string]interface{}", value)
	case []interface{}:
		log.Println("[]interface{}", value)
	}

 

2.4 JSON 스키마

Go 언어의 JSON 라이브러리는 구조체에 대한 매핑을 하므로, 빠르게 변환될 것이라고 기대할 수 있을 것이다. 하지만 불행히도 그것을 보장하는 기능은 없다. 읽어들인 JSON과 구조체의 키가 일치하지 않아도, JSON 문법이 이상하지 않은 이상 오류가 발생하지 않는다. 예상대로 키가 있는지 검증은 구조체에 대한 매핑이 아니라 스키마를 사용한 확인이 필요하다. 

 

Go 언어의 표준 라이브러리에는 없지만, JSON 스키마 검증을 시행할 타사 라이브러리가 있다.

해당 github에 사용방법이 꽤 자세하게 나와 있다.

 

 

 

 

 

Reference

  • 리얼월드 HTTP

+ Recent posts