Programming Language/Golang(Go)

<할 일 목록> 웹앱 작성해보기(2) - 직렬화와 역직렬화

dr.meteor 2019. 10. 11. 01:43

*본문은 <Go 디스커버리>(염재현 저, 한빛미디어 2016)의 5장, 6장을 공부한 뒤 정리한 내용입니다.

 

1. 개념

1) 정의

직렬화(Serialization): 객체의 상태를 보관이나 전송 가능한 상태로 변환하는 것

역직렬화(Deserialization): 보관되거나 전송받은 것을 다시 객체로 복원하는 것

 

2) 용도

- 보조 기억장치에 저장/불러오기 기능을 구현하고자 할 때

- 네트워크를 통해 다른 사람에게 메시지를 보내려고 할 때

- RPC(Remote Procedure Call) - 원격 프로시저 호출 같이 다른 기계에게 네트워크를 통해 메시지를 보내서 원하는 일을 시킬 때에도 같은 방법을 활용할 수 있다.

 

 

2. JSON(JavaScript Object Notation)

pass

 

3. 본격적인 직렬화와 역직렬화

1) JSON 직렬화와 역직렬화

아래는 직렬화를 하는 코드에 해당한다.

func Example_marshalJSON() {
  t := Task {
    "Laundry",
    DONE,
    NewDeadline(time.Date(2015, time.August, 16, 15, 43, 0, 0, time.UTC)),
  }
  
  b, err := json.Marshal(t)//return byte slice & err
  
  if err != nil {
    log.Println(err)
    return
  }
  
  fmt.Println(string(b))//Output:{"Title":"Laundry","Status":2,"Deadline":"2015-08-16T15:43:00Z"}

여기서 주목할 점은 구조체 내부의 세 필드들이 대문자로 시작하게 바뀌었단 점이다. JSON 패키지는 대문자로 시작하는 필드들만 JSON으로 직렬화하므로, 직렬화하고 싶지 않은 필드들은 소문자로 시작하게 만들면 된다. 그리고 대문자로 시작하는 필드들은 같은 패키지뿐만 아니라 다른 패키지에서도 보인다.

 

아래는 역직렬화 코드이다.

func Example_unmarshalJSON() {
  b := []byte(`{"Title":"Buy Milk","Status":2,"Deadline":"2015-08-16T15:43:00Z"}`)
  t := Task{}
  err := json.Unmarshal(b, &t) //바이트 자료를 역직렬화하여 구조체 t 안에 넣을 것임을 의미한다고 추론 가능
  
  if err != nil {
    log.Println(err)
    return
  }
  
  fmt.Println(t.Title) //Buy Milk
  fmt.Println(t.Status) //2
  fmt.Println(t.Deadline.UTC()) //생략
}

json.Unmarshal을 이용하여 구조체 안의 값을 채워줄 수 있다. 이땐 포인터를 이용해야 수정된 것이 반영되므로 t가 아닌 &t를 써주었다.

 

 

2) JSON 태그

구조체의 필드 이름은 Title이지만, JSON에서 필드 이름은 title로 하고 싶을 수도 있다. 또는 숫자가 0이거나 문자열이 빈값일 경우엔 굳이 필드를 나열하고 싶지 않을 수도 있다. 이와 같이 기본 직렬화 필드대로 출력하고 싶지 않을 경우, 아래 코드와 같이 구조체의 필드에 json 태그를 붙이면 이를 JSON 라이브러리가 읽고 처리해준다.

type MyStruct struct {
  Title		string	`json:"title"` //Title 대신 title을 JSON 필드로 사용
  Internal	string	`json:"-"` //JSON 필드에 나타나지 않고 무시된다
  Value		int64	`json:",omitempty"` //두 " 사이에 ,가 있다는 것 주의. 0인 경우엔 JSON 결과를 내지 않음
  ID		int64	`json:",string"` //JSON에선 string 타입으로
}

마지막 ID부분에서 왜 위와 같이 작성했는지 알아보자. JSON으로 직렬화하고 고언어에서 해당 JSON을 읽을 것 같으면 그냥 int64형으로 두어도 괜찮다. 그러나 웹 어플리케이션을 작성하는 경우 등 자바스크립트가 JSON을 읽게 되면 문제가 발생한다. 자스에서 숫자형은 8바이트 실수형이기 때문에 정수값이 53비트를 넘어가면 정확도가 떨어진다(낮은 자리 숫자들이 0으로 바뀌어 버리는 문제가 생긴다). 따라서 64비트 정수를 JSON으로 주고받을 때는 'json:",string"'을 해주는 습관을 기르는 것도 좋다, 이렇게 하면 JSON 결과는 "123"과 같은 10진수로 표현된 문자열이 되지만 GO언어에서 역직렬화해서 읽으면 정수형으로 변환된다.

 

 

3) JSON 직렬화 사용자 정의

받는 쪽에서 Status를 문자열로 받아야 하거나, 그렇게 하고 싶은 경우 아래와 같이 커스텀 코드를 작성할 수 있다. int형과 구분되는 status형을 만둘어주었기 때문에 가능하다.

//MarshalJSON implements the json.Marshaler interface.
func (s status) MarshalJSON() ([]byte, error) {
	switch s {
	case UNKNOWN:
		return []byte(`"UNKNOWN"`), nil
	case TODO:
		return []byte(`"TODO"`), nil
	case DONE:
		return []byte(`"DONE"`), nil
	default:
		return nil, errors.New("status.MarshalJSON: unknown value")
	}
}
//UnmarshalJSON implements the json.Unmarshaler interface.
func (s *status) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"UNKNOWN"`:
		*s = UNKNOWN
	case `"TODO"`:
		*s = TODO
	case `"DONE"`:
		*s = DONE
	default:
		return errors.New("status.UnmarshalJSON: unknown value")
	}
	return nil
}

참고: UnmarshalJSON 함수에서 `"UNKNOWN"`과 같이 역따옴표로 한 번 더 둘러싼 것은 넘어오는 데이터가 따옴표까지 포함된 문자열이기 때문이다. 또 주의할 점은 역따옴표 = 백 쿼트 = ` 이고, 이는 작은따옴표인 ' 와는 다르다는 점이다. macOS에서는 ~이 들어있는 키보드 자판키와 option키를 한번에 같이 누르면 입력할 수 있다.

 

이번에는 기한을 "Deadline":"2015-08-16T15:43:00Z"와 같은 문자열 형태가 아닌 유닉스 시간, 즉 정수형으로 표현하는 코드를 작성해보자. time.Time에 있는 메서드 Unix()를 호출해서 얻을 수 있고, 이는 53비트의 범위를 넘지 않는다. 이와 달리 UnixNano()는 유닉스 시간을 나노초 단위로 나타낸 것으로 Unix()의 10의 9승배가 되므로 53비트 범위를 넘어서 자스에서 이 JSON을 읽을 때 오차가 생긴다(데이터 열화가 일어나서 낮은 자리 숫자들이 사라진다). 열화 발생 방지를 원할 땐 `json:",string"` 태그를 붙여야 한다!

func (d Deadline) MarshalJSON ([]byte, error) {
	return strconv.AppendInt(nil, d.Unix(), 10), nil
}

func (d *Deadline) UnmarshalJSON(data []byte) error {
	unix, err := strconv.ParseInt(string(data), 10, 64)
	if err != nil {
		return err
	}
	d.Time = time.Unix(unix, 0)
	return nil
}

Task 구조체는 다음과 같이 태그를 달아 변경한다. 키 이름은 소문자로 시작하는 것이 일반적이다.

type Task struct {
  Title		string		`json:"title,omitempty"`
  Status	status		`json:"status,omitempty"`
  Deadline	*Deadline	`json:"deadline,omitempty"`
  Priority	int		`json:"priority,omitempty"`
}

 

4) 구조체가 아닌 자료형 처리

반드시 구조체를 사용하여 JSON 라이브러리를 이용할 필요는 없다. 배열을 (역)직렬화 하는데도 JSON 라이브러리를 쓸 수 있다. 다만 여러 필드가 있는 자스의 객체를 처리할 땐 구조체가 적절하다. 구조체 외에도 을 이용하여 자스 객체를 처리할 수 있다.

func Example_mapMarshalJSON() {
	b, _ := json.Marshal(map[string]string{
		"Name":"John",
		"Age":"16",
	})
	fmt.Println(string(b)) //{"Age":"16","Name":"John"}
}

맵은 순서가 없기 때문에 이름과 나이 중 어느 것이 먼저 나오는지에 대해선 정해진 순서가 없지만, JSON 라이브러리의 구현에서는 키를 정렬하여 출력하기 때문에 항상 Age가 Name보다 먼저 나온다. 주의할 점은 제이슨에 이용하는 맵은 키가 문자열형이어야 한다는 점이다. 위 코드에선 값도 string형으로 했는데, 아무 자료형이나 담고 싶다면 interface{}형을 담으면 된다. 그러면 "16"대신 16을 담을 수 있다. 더 나아가 interface{}형은 맵도 담을 수 있으므로 map[string]interface{} 자료형은 JSON 오브젝트 안에 또 JSON 오브젝트가 있는 형태들을 포함하여 어떤 JSON 오브젝트도 담을 수 있다. interface{} 자료형은 JSON으로 표현된 모든 데이터를 역직렬화할 수 있다. JSON 역직렬화 중 interface{}자료형을 만나면 map[string]interface{} 형태로 역직렬화를 하기 때문이다(아직 잘 이해되지 않는 부분...)

 

 

4) JSON 필드 조작