JSON(JavaScript Object Notation)는 속성-값 쌍의 형식을 가지는 개방형 표준 파일 포맷으로 데이터 교환에 사용 하고 있다. JSON은 언어 독립적인 데이터 형식이지만 JavaScript 프로그래밍 언어에서 직접 생성하고 읽을 수 있기 때문에, 웹 애플리케이션에서 사용하기에 적합하다. 이런 이유로 특히 웹 애플리케이션에서 널리 사용한다.
Marshalling
마샬링은 메모리에 저장된 객체를 다른 시스템어 전송하기에 적합한 데이터 형식으로 변환하는 과정이다. 예를 들어 C 언어의 구조체를 파이선 애플리케이션으로 보내는 경우 파이선에서 읽을 수 있도록 변환해야 한다. 이를 마샬링이라고 한다.
마샬링된 데이터는 다시 시스템이 쉽게 처리 할 수 있는 객체로 변환해야 하는데, 이를 언마샬링(unmarshalling)이라고 한다.
Json과 마샬링
마샬링은 데이터를 다른 시스템이 사용 할 수 있도록 변환하는과정이다. 정보의 상호운용성이 목적이라고 할 수 있다. 따라서 마샬링은 널리 사용하며, 사용하기 쉽고, 범용적인 포맷을 사용해야 할 것이다. 특히 인터넷 애플리케이션이라면 오픈 표준 포맷을 사용해야 한다. 가장 널리 사용하는 포맷이 "XML"과 "JSON"이고, 특히 요즘에는 JSON을 널리 사용한다.
이 문서에서는 Go의 데이터 스트럭처를 JSON으로 마샬링 & 언마샬링 하는 것을 살펴볼 것이다.
Go Struct를 JSON으로 마샬링
사용자 정보를 인터넷을 통해서 제공하는 Go 애플리케이션을 만들려고 한다. 이 애플리케이션의 사양은 아래와 같다.
HTTP(S) 1.1 로 제공 한다.
REST API 로 제공한다.
웹 브라우저와 안드로이드 앱 모두에서 데이터를 사용 할 수 있도록 하기 위해서 JSON을 포맷으로 사용하기로 했다.
사용자 정보에는 "사용자 고유 식별번호", "이메일주소", "나이", "블로그 페이지", "페이스북 페이지"를 포함한다.
유저 정보를 저장하고 출력하는 기능을 가진다.
사용자 정보는 아래와 같은 스트럭처로 정의 했다.
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 데이터를 보기좋게 출력해주는 유틸리티 프로그램이다.
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를 리턴한다.
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))
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
}
위의 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, Message 는 모든 애플리케이션이 공통으로 하고, ResponseData에 필요한 데이터를 넣게 했다.
인터페이스를 Go 스트럭처로
위 예제에서 Go 스트럭처를 JSON으로 마샬링 했다. 이 JSON을 언마샬링 해보자. 문제는 Response.ResponseData가 인터페이스(Interface) 라는 점이다. JSON을 Response 스트럭처로 언마샬링 할 경우 ResponseData는 map[string]interface 타입으로 언마샬링된다.
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 패키지에 따로 저장을 하지만 지금은 설명을 위해서 하나의 패키지에 몰아 넣었다.
Contents
JSON
Marshalling
Json과 마샬링
Go Struct를 JSON으로 마샬링
JSON Unmarshaling
JSON Tag
omitempty
필드제외하기
Nested JSON의 마샬/언마샬
Interface의 마샬/언마샬
배열&슬라이스의 마샬/언마샬
인터페이스를 Go 스트럭처로
커스텀 타입에 대한 JSON 마샬
웹 애플리케이션에서의 JSON
API, MODEL
Recent Posts
Archive Posts
Tags