메뉴

문서정보

목차

JSON

 JSON

JSON(JavaScript Object Notation)는 속성-값 쌍의 형식을 가지는 개방형 표준 파일 포맷으로 데이터 교환에 사용 하고 있다. JSON은 언어 독립적인 데이터 형식이지만 JavaScript 프로그래밍 언어에서 직접 생성하고 읽을 수 있기 때문에, 웹 애플리케이션에서 사용하기에 적합하다. 이런 이유로 특히 웹 애플리케이션에서 널리 사용한다.

Marshalling

마샬링은 메모리에 저장된 객체를 다른 시스템어 전송하기에 적합한 데이터 형식으로 변환하는 과정이다. 예를 들어 C 언어의 구조체를 파이선 애플리케이션으로 보내는 경우 파이선에서 읽을 수 있도록 변환해야 한다. 이를 마샬링이라고 한다.

마샬링된 데이터는 다시 시스템이 쉽게 처리 할 수 있는 객체로 변환해야 하는데, 이를 언마샬링(unmarshalling)이라고 한다.

 마샬링 언마샬링

Json과 마샬링

마샬링은 데이터를 다른 시스템이 사용 할 수 있도록 변환하는과정이다. 정보의 상호운용성이 목적이라고 할 수 있다. 따라서 마샬링은 널리 사용하며, 사용하기 쉽고, 범용적인 포맷을 사용해야 할 것이다. 특히 인터넷 애플리케이션이라면 오픈 표준 포맷을 사용해야 한다. 가장 널리 사용하는 포맷이 "XML"과 "JSON"이고, 특히 요즘에는 JSON을 널리 사용한다.

이 문서에서는 Go의 데이터 스트럭처를 JSON으로 마샬링 & 언마샬링 하는 것을 살펴볼 것이다.

Go Struct를 JSON으로 마샬링

사용자 정보를 인터넷을 통해서 제공하는 Go 애플리케이션을 만들려고 한다. 이 애플리케이션의 사양은 아래와 같다.
  1. HTTP(S) 1.1 로 제공 한다.
  2. REST API 로 제공한다.
  3. 웹 브라우저와 안드로이드 앱 모두에서 데이터를 사용 할 수 있도록 하기 위해서 JSON을 포맷으로 사용하기로 했다.
  4. 사용자 정보에는 "사용자 고유 식별번호", "이메일주소", "나이", "블로그 페이지", "페이스북 페이지"를 포함한다.
  5. 유저 정보를 저장하고 출력하는 기능을 가진다.
사용자 정보는 아래와 같은 스트럭처로 정의 했다.
type UserInfo struct {
	UserID   int
	Email    string
	Age      int
	Blog     string
	FaceBook string
}
애플리케이션 요구사항에 따라서 이 스트럭처를 JSON으로 마샬링 하기로 했다.
package main

import (
	"encoding/json"
	"fmt"
)

type UserInfo struct {
	UserID   int
	Email    string
	Age      int
	Blog     string
	FaceBook string
}

func main() {
	u := UserInfo{
		UserID:   1001,
		Email:    "foo@example.com",
		Age:      32,
		Blog:     "foo.blog.com",
		FaceBook: "foo.facebook.com",
	}
	byteData, _ := json.Marshal(u)
	fmt.Println(string(byteData))
}
프로그램을 실행해보자. json_pp는 json 데이터를 보기좋게 출력해주는 유틸리티 프로그램이다.
# go run main.go | json_pp     
{
   "Age" : 32,
   "Blog" : "foo.blog.com",
   "Email" : "foo@example.com",
   "FaceBook" : "foo.facebook.com",
   "UserID" : 1001
}

golang의 encoing 패키지는 데이터를 바이트 수준 및 텍스트 수준에서 변환하는 다양한 서브 패키지들을 제공한다. 여기에는 JSON, ascii85, asn1, base32, base64, gob, hex, pem, xml 등이 널리 사용되는 포맷들이 포함된다. json의 마샬/언마샬을 위해서 "encoding/json" 패키지를 import 한다.
import "encoding/json"

json 패키지의 Marsal 함수를 이용해서, 입력 데이터를 JSON 형태로 마샬링 할 수 있다.
func Marshal(v interface{}) ([]byte, error)
입력 값은 interface이므로 primitive, array, slice, struct 모두 사용 할 수 있다. 마샬링에 성공하면 []byte를 리턴한다.
u := UserInfo{
	UserID:   1001,
	Email:    "foo@example.com",
	Age:      32,
	Blog:     "foo.blog.com",
	FaceBook: "foo.facebook.com",
}
byteData, _ := json.Marshal(u)
fmt.Println(string(byteData))

slice를 먀살링 해보자.
a := []int{1, 2, 3, 4, 5, 6}
byteData, _ := json.Marshal(a)
fmt.Println(string(byteData))  // [1,2,3,4,5,6]

byteData, _ = json.Marshal(a[2:])
fmt.Println(string(byteData)) // [3,4,5,6]

JSON Unmarshaling

우리가 만들 애플리케이션은 유저정보를 저장하기 위한 기능을 제공한다. 유저 정보는 JSON 형태로 전달된다. 애플리케이션은 JSON 데이터를 언마샬링 해서, 사용 할 수 있는 객체로 만들어야 한다. JSON을 Go 스트럭처로 언먀살링 해보자.
data := `{"UserID":1001, "Email":"foo@example.com",
"Age":32,"Blog":"foo.blog.com",
"FaceBook":"foo.facebook.com"}`

inputData := UserInfo{}
json.Unmarshal([]byte(data), &inputData)
fmt.Printf("%#v", inputData)
Unmarshal 함수를 이용해서, JSON 형식 데이터를 Go 스트럭처로 언마샬 할 수 있다

JSON Tag

Go 스트럭처는 Tag 기능을 지원한다. Tag를 이용해서 스트럭처를 구성하는 필드에 메타 정보를 설정 할 수 있다. encoding/json 패키지의 경우 아래와 같이 Tag를 이용해서 마샬/언마샬 방식을 설정 할 수 있다.
type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         int    `json:"age"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string `json:"-"`
}
이렇게 스트럭체에 json 태그를 사용해서, 마샬/언마샬 방식을 지정 할 수 있다. 의미하는 바를 하나씩 살펴보자.

omitempty

아래 코드를 실행해보자.
type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         int    `json:"age"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string
}
data := UserInfo{
	UserID:      1001,
	Email:       "foo@example.com",
	Blog:        "https://blog.example.com",
	FaceBook:    "https://foo.facebook.com",
	Description: "Hello World",
}
sndData, _ := json.Marshal(data)
fmt.Println(string(sndData))
코드 실행 결과는 아래와 같다.
{
   "Description" : "Hello World",
   "age" : 0,
   "blog" : "https://blog.example.com",
   "email" : "foo@example.com",
   "facebook" : "https://foo.facebook.com",
   "user_id" : 1001
}

이 코드의 문제는 아래와 같다. Go 스트럭처의 각 필드는 값을 설정하지 않을 경우 "기본 값"이 입력된다. int 타입은 기본 값은 0이다. json 태그에 omitempty르 설정하면 해당 필드를 json에서 제외 할 수 있다. 코드를 수정했다.
type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         int    `json:"age,omitempty"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string
}
코드를 실행해 보자.
{
   "Description" : "Hello World",
   "blog" : "https://blog.example.com",
   "email" : "foo@example.com",
   "facebook" : "https://foo.facebook.com",
   "user_id" : 1001
}

이제 json을 언마샬링 해보자. 위 출력 데이터를 사용했다.
inputData := `{
	"Description" : "Hello World",
	"blog" : "https://blog.example.com",
	"email" : "foo@example.com",
	"facebook" : "https://foo.facebook.com",
	"user_id" : 1001
 }`
out := UserInfo{}
json.Unmarshal([]byte(inputData), &out)
fmt.Printf("%#v", out)

코드를 실행해 보자.
main.UserInfo{UserID:1001, Email:"foo@example.com", Age:0, Blog:"https://blog.example.com", FaceBook:"https://foo.facebook.com", Description:"Hello World"}%                                                                    
Age 값이 0이다. 이건 우리가 원한 결과가 아니다. 이경우 우리가 원하는 값은 nil이 되어야 할 것이다.

해결 방법은 간단하다. 포인터를 사용하면 된다. 포인터의 기본 값(zero value)는 nil 이니까. 코드를 수정했다.
type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         *int   `json:"age,omitempty"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string
}

inputData := `{
	"Description" : "Hello World",
	"blog" : "https://blog.example.com",
	"email" : "foo@example.com",
	"facebook" : "https://foo.facebook.com",
	"user_id" : 1001
 }`

out := UserInfo{}
json.Unmarshal([]byte(inputData), &out)
fmt.Printf("%#v\n", out)
fmt.Println("Age :", out.Age)

코드를 실행해보자.
main.UserInfo{UserID:1001, Email:"foo@example.com", Age:(*int)(nil), Blog:"https://blog.example.com", FaceBook:"https://foo.facebook.com", Description:"Hello World"}
Age : <nil>

필드제외하기

위 예제에서 Description 필드는 JSON 마샬에서 제거할 생각이다. json태그에 "-"값을 설정해서 마샬/언마샬 하지 않도록 설정 할 수 있다.
type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         int    `json:"omitempty"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string `json:"-"`   // Description을 마샬/언마샬에서 제외
}

Nested JSON의 마샬/언마샬

웹 애플리케이션 서버를 개발하면 요청/응답 형식을 모델링 하게 될 것이다. 나는 아래와 같이 응답 형식을 모델링 했다.
{
    "code" : 0, 
    "message" : "OK", 
    "response": {
      "userid": 1,
      "email": "foo@example.com", 
      "age": 38,
      "blog": "https://foo.blog.com/",
      "facebook": "https://foo.facebook.com"
    }
}
중첩된(nested) JSON이다. Go에서는 struct 의 struct를 사용하면 된다.
package main

import (
	"encoding/json"
	"fmt"
)

type Response struct {
	Code     int    `json:"code"`
	Message  string `json:"message"`
	UserInfo `json:"userinfo"`
}

type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         *int   `json:"age,omitempty"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string
}

func main() {
	age := 38
	responseData := UserInfo{
		UserID:      1001,
		Email:       "foo@example.com",
		Age:         &age,
		Blog:        "https://blog.example.com",
		FaceBook:    "https://foo.facebook.com",
		Description: "Hello World",
	}

	res := Response{
		Code:     0,
		Message:  "OK",
		UserInfo: responseData,
	}
	sndData, _ := json.Marshal(res)
	fmt.Println(string(sndData))
}

코드실행 결과다.
{
   "code" : 0,
   "message" : "OK",
   "userinfo" : {
      "Description" : "Hello World",
      "age" : 38,
      "blog" : "https://blog.example.com",
      "email" : "foo@example.com",
      "facebook" : "https://foo.facebook.com",
      "user_id" : 1001
   }
}

Interface의 마샬/언마샬

위의 Nested JSON 마샬/언마샬은 잘 작동하지만 문제가 있다. 웹 애플리케이션은 여러 데이터 모델을 응답하기 마련이다. 위의 코드는 UserInfo 만 마샬/언마샬 할 수 있다는 문제가 있다.
// 사용자 정보 응답을 위한 모델
type ResponseUser struct {
	Code     int    `json:"code"`
	Message  string `json:"message"`
	UserInfo        `json:"userinfo"`
}

// 사용자 활동 정보
type ResponseActivity struct {
	Code     int    `json:"code"`
	Message  string `json:"message"`
	Activity        `json:"activity"`
}

// 기타등등

각 응답마다 모델을 만들 수 있지만 지저분하다. 인터페이스를 이용하는게 좋겠다.
type Response struct {
	Code         int         `json:"code"`
	Message      string      `json:"message"`
	ResponseData interface{} `json:"responseData"`
}
응답은 애플리케이션 표준으로 정해질 것이다. Code와 Message는 필수로 포함을하고, 각 API 별 응답은 ResponseData 에 설정한다. 아래는 완전히 작동하는 코드다.
package main

import (
	"encoding/json"
	"fmt"
)

type Response struct {
	Code         int         `json:"code"`
	Message      string      `json:"message"`
	ResponseData interface{} `json:"responseData"`
}

type UserInfo struct {
	UserID      int    `json:"user_id"`
	Email       string `json:"email"`
	Age         *int   `json:"age,omitempty"`
	Blog        string `json:"blog"`
	FaceBook    string `json:"facebook"`
	Description string
}

type Activity struct {
	ActivityID int    `json:"activity_id"`
	Action     string `json:"action"`
}

func main() {
	userInfo := UserInfo{
		UserID: 1001,
		Email:  "foo@example.com",
	}

	res := Response{
		Code:         0,
		Message:      "OK",
		ResponseData: userInfo,
	}

	sndData, _ := json.Marshal(res)
	fmt.Println(string(sndData))

	activity := Activity{
		ActivityID: 5129,
		Action:     "buy",
	}

	res = Response{
		Code:         0,
		Message:      "OK",
		ResponseData: activity,
	}

	sndData, _ = json.Marshal(res)
	fmt.Println(string(sndData))
}

코드를 실행해보자.
{
   "code" : 0,
   "message" : "OK",
   "responseData" : {
      "Description" : "",
      "blog" : "",
      "email" : "foo@example.com",
      "facebook" : "",
      "user_id" : 1001
   }
}
{
   "code" : 0,
   "message" : "OK",
   "responseData" : {
      "action" : "buy",
      "activity_id" : 5129
   }
}

배열&슬라이스의 마샬/언마샬

위 예제코드에서 Activity(사용자 활동)는 리스트가 될 것이다. Activity 응답 코드를 페이지 네비게이션 가능하도록 수정 할 것이다. 아래와 같이 코드를 수정했다.
package main

import (
	"encoding/json"
	"fmt"
)

type Response struct {
	Code         int         `json:"code"`
	Message      string      `json:"message"`
	ResponseData interface{} `json:"responseData"`
}

type ActivityList struct {
	Offset int            `json:"offset"`
	Limit  int            `json:"limit"`
	Item   []ActivityItem `json:"item"`
}

type ActivityItem struct {
	ActivityID int    `json:"activity_id"`
	Action     string `json:"action"`
}

func main() {

	activity := ActivityList{
		Offset: 10,
		Limit:  10,
		Item: []ActivityItem{
			{
				ActivityID: 1003,
				Action:     "buy",
			},
			{
				ActivityID: 972,
				Action:     "comment",
			},
			{
				ActivityID: 970,
				Action:     "like",
			},
		},
	}
	res := Response{
		Code:         0,
		Message:      "OK",
		ResponseData: activity,
	}

	sndData, _ := json.Marshal(res)
	fmt.Println(string(sndData))
}

Code, Message 는 모든 애플리케이션이 공통으로 하고, ResponseData에 필요한 데이터를 넣게 했다.

인터페이스를 Go 스트럭처로

위 예제에서 Go 스트럭처를 JSON으로 마샬링 했다. 이 JSON을 언마샬링 해보자. 문제는 Response.ResponseData가 인터페이스(Interface) 라는 점이다. JSON을 Response 스트럭처로 언마샬링 할 경우 ResponseData는 map[string]interface 타입으로 언마샬링된다.
res := Response{
	Code:         0,
	Message:      "OK",
	ResponseData: activity,
}

sndData, _ := json.Marshal(res)
fmt.Println(string(sndData))

out := Response{}

json.Unmarshal(sndData, &out)
fmt.Println(reflect.TypeOf(out.ResponseData) // map[string]interface{}
결과적으로 map[string]interface{}를 스트럭처(ActivityItem 배열)로 변환해야 한다.

비효율적이지만 쉬운 방법은 map[string]interface{}를 JSON으로 마샬링하고 다시 스트럭처로 언마샬링 하면 된다.
... 계속

커스텀 타입에 대한 JSON 마샬

내장 타입이 아닌 타입들에 대해서, 커스텀 마샬 코드를 적용 할 수 있다. json.Marshaler 의 인터페이스를 개발하면 된다.
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

UserInfo 스트럭처에 시간(time)필드를 넣을려고 한다. 시간은 "Mon May 3"와 같은 별도의 포맷으로 설정하고 싶다.
package main

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

type JSONTime time.Time

type UserInfo struct {
	UserID      int       `json:"user_id"`
	Email       string    `json:"email"`
	Age         int       `json:"age,omitempty"`
	Blog        string    `json:"blog"`
	FaceBook    string    `json:"facebook"`
	Description string    `json:"-"`
	Stamp       time.Time `json:"stamp"`
}

func (d *UserInfo) MarshalJSON() ([]byte, error) {
	type Alias UserInfo
	return json.Marshal(&struct {
		*Alias
		Stamp string `json:"stamp"`
	}{
		Alias: (*Alias)(d),
		Stamp: d.Stamp.Format("Mon Jan _2"),
	})
}

func main() {
	d := UserInfo{
		UserID:      1001,
		Email:       "foo@example.com",
		Blog:        "https://blog.example.com",
		FaceBook:    "https://foo.facebook.com",
		Description: "Hello World",
		Stamp:       time.Now(),
	}
	out, _ := d.MarshalJSON()
	fmt.Println(string(out))
}

실행해보자.
{
   "blog" : "https://blog.example.com",
   "email" : "foo@example.com",
   "facebook" : "https://foo.facebook.com",
   "stamp" : "Mon May  3",
   "user_id" : 1001
}
Stamp를 string으로 하고 time.Now().Format("Mon Jan _2")와 같이 직접 코딩하는 것에 비해서 훨씬 깔끔하게 코드를 유지 할 수 있다.

아래와 같이 특정 필드에 대한 MarshalJSON 인터페이스를 구현 할 수도 있다.
type JSONTime time.Time

type UserInfo struct {
	UserID      int      `json:"user_id"`
	Email       string   `json:"email"`
	Age         int      `json:"age,omitempty"`
	Blog        string   `json:"blog"`
	FaceBook    string   `json:"facebook"`
	Description string   `json:"-"`
	Stamp       JSONTime `json:"stamp"`
}

func (t JSONTime) MarshalJSON() ([]byte, error) {
	stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("Mon Jan _2"))
	return []byte(stamp), nil
}

func main() {
	d := UserInfo{
		UserID:      1001,
		Email:       "foo@example.com",
		Blog:        "https://blog.example.com",
		FaceBook:    "https://foo.facebook.com",
		Description: "Hello World",
		Stamp:       JSONTime(time.Now()),
	}
	out, _ := json.Marshal(d)
}

UnmarshalJSON 인터페이스를 구현해서 커스텀 언마샬 코드를 설정 할 수 있다.

웹 애플리케이션에서의 JSON

Go 언어에서 JSON을 다루는 90% 이상은 웹 애플리케이션 서버 개발 영역에서일 것이다. 마지막으로 웹 애플리케이션에서 JSON을 다루는 일반적인 방법을 다루고 이 글을 마치려 한다.

API, MODEL

나는 API 문서화 도구로 swagger를 사용한다. swagger로 API 스펙을 정의 할 때, 가장 먼저 하는 것은 응답(response)과 요청(request)의 데이터 모델을 정의 하는 것이다. 이 데이터 모델은 웹 애플리케이션의 JSON 스트럭처로 정의한다.

예를 들어 유저 정보와 관련된 swagger 스키마는 아래와 같이 설정 할 것이다.
components:
  schemas:
    User:       # Schema name
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
      example:   # Object-level example
        id: 1
        name: Jessica Smith
        email: jessica.smith@example.com 

Go 스트럭처는 아래와 같이 정의 할 수 있을 것이다.
type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

HTTP 응답은 에러코드와 에러메시지와 같은 공통 필드와 각 API 별 모델이 함께 구성될 것이다. Nested JSON으로 구성될 것이고 go 스트럭처의 경우 interface 로 구성할 것이다. 아래와 같이 하면 대부분의 응답을 처리하는데 문제가 없을 것이다.
type Response struct {
	Code         int         `json:"code"`
	Message      string      `json:"message"`
	ResponseData interface{} `json:"responseData"`
}

Go 언어에서는 API를 처리할 핸들러를 구성할 것이다. 각 핸들러는 처리해야 하는 응답/요청 모델이 정해지기 마련이다. 요청 데이터의 경우 context로 읽을 수 있을 텐데, 별도의 bind 함수를 만들어서 처리한다. 아래 코드는 golang echo 프레임워크를 기준으로 하고 있는데, 다른 패키지들도 차이는 없을 것이다. Comment를 Update 하는 핸들러가 있다고 가정해보자. 모델은 model 패키지에 따로 저장을 하지만 지금은 설명을 위해서 하나의 패키지에 몰아 넣었다.
type Comment struct {
	ObjectType string `json:"objecttype"`
	ObjectID   int64  `json:"objectid"`
	UserID     int64  `json:"userid"`
	Title      string `json:"title" gorm:"size:255"`
	Text       string `json:"text" gorm:"size:65535"`
}

func (h *Handler) CreateComment(c echo.Context) error {
	req := CommentRequest{}
	comment, err := req.bind(c)
	if err != nil {
		return c.JSON(http.StatusUnprocessableEntity, Response{Code: "1000", Message: err.Error()})
	}
	// 데이터베이스에 저장

	// 응답
	return c.JSON(http.StatusOK, Response{Code: "200", Message: "OK"})
}

type CommentRequest struct {
	Comment
}

func (r *CommentRequest) bind(c echo.Context) (model.Comment, error) {
	if err := c.Bind(r); err != nil {

	}
	comment := Comment{
		ObjectID:   r.ObjectID,
		ObjectType: r.ObjectType,
		UserID:     r.UserID,
		Title:      r.Title,
		Text:       r.Text,
	}

	return comment, nil
}
echo.Context를 이용해서 모델에 직접 바인딩을 하고, 정리가 끝난 요청을 리턴한다.

응답은 아래와 같이 처리한다.
type Response struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type CommentResponse struct {
	Response
	ResponseData *model.Comment `json:"responseData"`
}

func (h *Handler) GetComment(c echo.Context) error {
	// 데이터베이스에서 comment를 읽어온다. 
	id := c.Parm("id")
	item := h.Store.GetCommentFromID(id)  
	return c.JSON(http.StatusOK, CommentResponse{
		Response{Code: "200", Message: "OK"},
		item,
	})
}