1. encoding/json 표준 라이브러리 개요

Go 언어는 JSON 데이터 형식을 다루기 위한 강력한 encoding/json 라이브러리를 제공합니다. 이 라이브러리를 사용하면 Go 데이터 유형을 JSON 형식으로 변환(직렬화)하거나 JSON 데이터를 Go 데이터 유형으로 변환(역직렬화)할 수 있습니다. 이 라이브러리는 인코딩, 디코딩, 스트리밍 IO 및 사용자 정의 JSON 파싱 로직 지원 등 많은 기능을 제공합니다.

이 라이브러리에서 가장 중요한 데이터 유형 및 함수는 다음과 같습니다.

  • MarshalMarshalIndent: Go 데이터 유형을 JSON 문자열로 직렬화하는 데 사용됩니다.
  • Unmarshal: JSON 문자열을 Go 데이터 유형으로 역직렬화하는 데 사용됩니다.
  • EncoderDecoder: JSON 데이터의 스트리밍 IO에 사용됩니다.
  • Valid: 주어진 문자열이 유효한 JSON 형식인지 확인하는 데 사용됩니다.

다음 장에서 이러한 함수 및 유형의 사용법을 구체적으로 학습하게 될 것입니다.

2. Go 데이터 구조를 JSON으로 직렬화하기

2.1. json.Marshal 사용하기

json.Marshal은 Go 데이터 유형을 JSON 문자열로 직렬화하는 함수입니다. 이 함수는 Go 언어의 데이터 유형을 입력으로 받아서 이를 JSON 형식으로 변환한 뒤 가능한 오류와 함께 바이트 슬라이스를 반환합니다.

다음은 간단한 예제로, Go 구조체를 JSON 문자열로 변환하는 방법을 보여줍니다:

package main

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

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    person := Person{"Alice", 30}
    jsonData, err := json.Marshal(person)
    if err != nil {
        log.Fatalf("JSON marshaling failed: %s", err)
    }
    fmt.Println(string(jsonData)) // 출력: {"name":"Alice","age":30}
}

구조체 외에도 json.Marshal 함수는 mapslice와 같은 다른 데이터 유형도 직렬화할 수 있습니다. 아래에 map[string]interface{}slice를 사용한 예제가 있습니다:

// map을 JSON으로 변환
myMap := map[string]interface{}{
    "name": "Bob",
    "age":  25,
}
jsonData, err := json.Marshal(myMap)
// ... 오류 처리 및 출력은 생략합니다 ...

// slice를 JSON으로 변환
mySlice := []string{"Apple", "Banana", "Cherry"}
jsonData, err := json.Marshal(mySlice)
// ... 오류 처리 및 출력은 생략합니다 ...

2.2. 구조체 태그

Go에서 구조체 태그는 구조체 필드의 메타데이터를 제공하여 JSON 직렬화의 동작을 제어하는 데 사용됩니다. 가장 일반적인 사용 사례는 필드 이름 바꾸기, 필드 무시, 조건부 직렬화 등이 있습니다.

예를 들어 json:"<name>" 태그를 사용하여 JSON 필드의 이름을 지정할 수 있습니다:

type Animal struct {
    SpeciesName string `json:"species"`
    Description string `json:"desc,omitempty"`
    Tag         string `json:"-"` // "-" 태그를 추가하면 이 필드는 직렬화되지 않습니다
}

위의 예에서 Tag 필드 앞에 있는 json:"-" 태그는 json.Marshal에게 이 필드를 무시하도록 지시합니다. Description 필드에 대한 omitempty 옵션은 이 필드가 비어 있을 경우(빈 문자열과 같은 제로 값을 가진 경우) 직렬화되지 않도록 지시합니다.

다음은 구조체 태그를 활용한 완전한 예제입니다:

package main

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

type Animal struct {
    SpeciesName string `json:"species"`
    Description string `json:"desc,omitempty"`
    Tag         string `json:"-"`
}

func main() {
    animal := Animal{
        SpeciesName: "African Elephant",
        Description: "A large mammal with a trunk and tusks.",
        Tag:         "endangered", // 이 필드는 JSON으로 직렬화되지 않습니다
    }
    jsonData, err := json.Marshal(animal)
    if err != nil {
        log.Fatalf("JSON marshaling failed: %s", err)
    }
    fmt.Println(string(jsonData)) // 출력: {"species":"African Elephant","desc":"A large mammal with a trunk and tusks."}
}

이렇게 함으로써 JSON 표현을 유연하게 다루면서 명확한 데이터 구조를 보장할 수 있습니다.

3. JSON을 Go 데이터 구조로 역직렬화하기

3.1 json.Unmarshal 사용하기

json.Unmarshal 함수를 사용하면 JSON 문자열을 구조체(structs), 맵(map) 등 Go 데이터 구조로 파싱할 수 있습니다. json.Unmarshal을 사용하려면 먼저 JSON 데이터와 일치하는 Go 데이터 구조를 정의해야 합니다.

다음과 같은 JSON 데이터가 있다고 가정해봅시다.

{
    "name": "Alice",
    "age": 25,
    "emails": ["[email protected]", "[email protected]"]
}

이 데이터를 Go 구조체로 파싱하려면 일치하는 구조체를 정의해야 합니다.

type User struct {
    Name   string   `json:"name"`
    Age    int      `json:"age"`
    Emails []string `json:"emails"`
}

이제 역직렬화를 위해 json.Unmarshal을 사용할 수 있습니다.

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonData := `{
        "name": "Alice",
        "age": 25,
        "emails": ["[email protected]", "[email protected]"]
    }`

    var user User
    err := json.Unmarshal([]byte(jsonData), &user)
    if err != nil {
        fmt.Println("JSON 역직렬화 오류:", err)
        return
    }

    fmt.Printf("사용자: %+v\n", user)
}

위 예제에서는 json:"name"과 같은 태그를 사용하여 json.Unmarshal 함수에 JSON 필드를 구조체 필드에 매핑하는 방법을 알려줍니다.

3.2 동적 파싱

가끔 필요한 JSON 구조가 미리 알려지지 않거나 JSON 데이터의 구조가 동적으로 변할 수도 있습니다. 그런 경우에는 interface{}json.RawMessage를 사용할 수 있습니다.

interface{}를 사용하면 JSON 구조를 미리 알지 못해도 파싱할 수 있습니다.

func main() {
    jsonData := `{
        "name": "Alice",
        "details": {
            "age": 25,
            "job": "Engineer"
        }
    }`

    var result map[string]interface{}
    json.Unmarshal([]byte(jsonData), &result)

    fmt.Println(result)

    // 타입 어서션을 사용하여 사용하기 전에 타입 일치를 보장합니다
    name := result["name"].(string)
    fmt.Println("이름:", name)
    details := result["details"].(map[string]interface{})
    age := details["age"].(float64) // 참고: interface{}의 숫자는 float64로 취급됩니다
    fmt.Println("나이:", age)
}

json.RawMessage를 사용하면 원본 JSON을 보존하면서 그 일부분만 선택적으로 파싱할 수 있습니다.

type UserDynamic struct {
    Name    string          `json:"name"`
    Details json.RawMessage `json:"details"`
}

func main() {
    jsonData := `{
        "name": "Alice",
        "details": {
            "age": 25,
            "job": "Engineer"
        }
    }`

    var user UserDynamic
    json.Unmarshal([]byte(jsonData), &user)

    var details map[string]interface{}
    json.Unmarshal(user.Details, &details)

    fmt.Println("이름:", user.Name)
    fmt.Println("나이:", details["age"])
    fmt.Println("직업:", details["job"])
}

이 접근 방식은 특정 필드가 다른 유형의 데이터를 포함할 수 있는 JSON 구조를 처리하는 데 유용하며 유연한 데이터 처리를 가능하게 합니다.

4 중첩 구조 및 배열 다루기

4.1 중첩된 JSON 객체

일반적인 JSON 데이터는 종종 평평하지 않고 중첩된 구조를 포함합니다. Go에서는 중첩된 구조를 정의하여 이러한 상황을 처리할 수 있습니다.

다음과 같은 중첩된 JSON이 있다고 가정해봅시다.

{
    "name": "Bob",
    "contact": {
        "email": "[email protected]",
        "address": "123 Main St"
    }
}

우리는 다음과 같이 Go 구조체를 정의할 수 있습니다.

type ContactInfo struct {
    Email   string `json:"email"`
    Address string `json:"address"`
}

type UserWithContact struct {
    Name    string      `json:"name"`
    Contact ContactInfo `json:"contact"`
}

역직렬화 동작은 중첩되지 않은 구조와 유사합니다.

func main() {
    jsonData := `{
        "name": "Bob",
        "contact": {
            "email": "[email protected]",
            "address": "123 Main St"
        }
    }`

    var user UserWithContact
    err := json.Unmarshal([]byte(jsonData), &user)
    if err != nil {
        fmt.Println("JSON 역직렬화 오류:", err)
    }

    fmt.Printf("%+v\n", user)
}

4.2 JSON 배열

JSON에서 배열은 흔한 데이터 구조입니다. Go에서는 이러한 배열을 slice로 표현합니다.

다음과 같은 JSON 배열을 고려해보세요:

[
    {"name": "Dave", "age": 34},
    {"name": "Eve", "age": 28}
]

Go에서는 해당하는 구조체 및 slice를 다음과 같이 정의합니다:

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonData := `[
        {"name": "Dave", "age": 34},
        {"name": "Eve", "age": 28}
    ]`

    var people []Person
    json.Unmarshal([]byte(jsonData), &people)
    
    for _, person := range people {
        fmt.Printf("%+v\n", person)
    }
}

이렇게 하면 JSON 배열의 각 요소를 Go 구조체의 slice로 역직렬화하여 나중에 처리하고 접근할 수 있습니다.

5 오류 처리

JSON 데이터를 처리할 때, 직렬화(구조화된 데이터를 JSON 형식으로 변환)든 역직렬화(JSON을 구조화된 데이터로 변환)든 오류가 발생할 수 있습니다. 다음으로는 일반적인 오류와 그 처리 방법에 대해 설명하겠습니다.

5.1 직렬화 오류 처리

직렬화 오류는 일반적으로 구조체나 다른 데이터 유형을 JSON 문자열로 변환하는 과정 중에 발생합니다. 예를 들어, JSON으로 표현할 수 없는 필드가 포함된 구조체를 직렬화하려고 시도하면(Data 필드에 채널 유형이나 JSON으로 표현할 수 없는 함수와 같은) json.Marshal이 오류를 반환합니다.

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

type User struct {
    Name string
    Age  int
    // 여기에 직렬화할 수 없는 필드가 있다고 가정
    // Data chan struct{} // 채널은 JSON으로 표현할 수 없음
}

func main() {
    u := User{
        Name: "Alice",
        Age:  30,
        // Data: make(chan struct{}),
    }

    bytes, err := json.Marshal(u)
    if err != nil {
        log.Fatalf("JSON 직렬화 실패: %v", err)
    }
    
    fmt.Println(string(bytes))
}

위 예제에서는 일부러 Data 필드를 주석 처리했습니다. 주석을 해제하면 직렬화가 실패하고 프로그램은 오류를 기록하고 실행을 중단합니다. 이러한 오류를 처리하는 일반적인 방법은 오류를 확인하고 오류 처리 전략을 구현하는 것입니다(예: 오류 기록, 기본 데이터 반환 등).

5.2 역직렬화 오류 처리

역직렬화 오류는 JSON 문자열을 다시 Go 구조체나 다른 데이터 유형으로 변환하는 과정 중에 발생할 수 있습니다. 예를 들어, JSON 문자열 형식이 잘못되거나 대상 유형과 호환되지 않는 경우 json.Unmarshal이 오류를 반환합니다.

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

func main() {
    var data = []byte(`{"name":"Alice","age":"unknown"}`) // "age"는 정수여야 하지만 여기에 문자열이 제공됨
    var u User

    err := json.Unmarshal(data, &u)
    if err != nil {
        log.Fatalf("JSON 역직렬화 실패: %v", err)
    }
    
    fmt.Printf("%+v\n", u)
}

이 코드 예제에서는 일부러 age 필드에 잘못된 데이터 유형(예: 예상되는 정수 대신 문자열)을 제공하여 json.Unmarshal이 오류를 발생시키도록 했습니다. 따라서 이 상황을 적절하게 처리해주어야 합니다. 일반적으로는 오류 메시지를 기록하고 상황에 따라 빈 객체, 기본 값 또는 오류 메시지를 반환합니다.

6 고급 기능과 성능 최적화

6.1 사용자 지정 마샬 및 언마샬

기본적으로 Go 언어의 encoding/json 패키지는 반사(reflection)를 통해 JSON을 직렬화(serialize)하고 역직렬화(deserialize)합니다. 그러나 json.Marshalerjson.Unmarshaler 인터페이스를 구현하여 이러한 프로세스를 사용자 정의할 수 있습니다.

import (
    "encoding/json"
    "fmt"
)

type Color struct {
    Red   uint8
    Green uint8
    Blue  uint8
}

func (c Color) MarshalJSON() ([]byte, error) {
    hex := fmt.Sprintf("\"#%02x%02x%02x\"", c.Red, c.Green, c.Blue)
    return []byte(hex), nil
}

func (c *Color) UnmarshalJSON(data []byte) error {
    _, err := fmt.Sscanf(string(data), "\"#%02x%02x%02x\"", &c.Red, &c.Green, &c.Blue)
    return err
}

func main() {
    c := Color{Red: 255, Green: 99, Blue: 71}
    
    jsonColor, _ := json.Marshal(c)
    fmt.Println(string(jsonColor))

    var newColor Color
    json.Unmarshal(jsonColor, &newColor)
    fmt.Println(newColor)
}

여기서 Color 타입을 정의하고 MarshalJSONUnmarshalJSON 메서드를 구현하여 RGB 색상을 16진수 문자열로 변환하고 다시 RGB 색상으로 변환합니다.

6.2 인코더와 디코더

대량의 JSON 데이터를 처리할 때 json.Marshaljson.Unmarshal을 직접 사용하면 과도한 메모리 사용이나 비효율적인 입출력 작업을 유발할 수 있습니다. 따라서 Go 언어의 encoding/json 패키지는 스트리밍 방식으로 JSON 데이터를 처리할 수 있는 EncoderDecoder 타입을 제공합니다.

6.2.1 json.Encoder 사용하기

json.Encoder는 io.Writer 인터페이스를 구현하는 모든 객체에 JSON 데이터를 직접 기록할 수 있으므로 JSON 데이터를 파일, 네트워크 연결 등에 직접 인코딩할 수 있습니다.

import (
    "encoding/json"
    "os"
)

func main() {
    users := []User{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
    }
    
    file, _ := os.Create("users.json")
    defer file.Close()
    
    encoder := json.NewEncoder(file)
    if err := encoder.Encode(users); err != nil {
        log.Fatalf("Encoding error: %v", err)
    }
}

6.2.2 json.Decoder 사용하기

json.Decoder는 io.Reader 인터페이스를 구현하는 모든 객체에서 JSON 데이터를 직접 읽어와 JSON 객체 및 배열을 찾아 구문 분석할 수 있습니다.

import (
    "encoding/json"
    "os"
)

func main() {
    file, _ := os.Open("users.json")
    defer file.Close()
    
    var users []User
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&users); err != nil {
        log.Fatalf("Decoding error: %v", err)
    }
    
    for _, u := range users {
        fmt.Printf("%+v\n", u)
    }
}

인코더와 디코더를 사용하여 데이터를 처리하면 읽으면서 JSON 처리를 수행하여 메모리 사용량을 줄이고 처리 효율성을 높일 수 있으며, 특히 네트워크 전송이나 대규모 파일 처리에 유용합니다.