Go 표준 라이브러리의 encoding/json과 encoding/xml 패키지는 Go 값과 JSON·XML 문서 사이의 변환을 지원합니다.
두 패키지는 구조체를 처리할 때 reflection을 사용하며, 기본적으로 외부에서 접근할 수 있는 exported field만 변환합니다. 필드 이름이나 생략 조건은 struct tag로 지정할 수 있습니다.
JSON과 XML은 비슷한 방식으로 사용할 수 있지만 표현 구조와 struct tag 규칙은 서로 다릅니다. JSON은 객체의 key와 구조체 필드를 대응시키고, XML은 element, attribute, 문자 데이터와 구조체 필드를 대응시킵니다.
외부에서 받은 JSON이나 XML을 바로 디코딩할 때는 문법 검사만으로 충분하지 않습니다. 입력 크기를 제한하고, 필요한 필드와 허용할 필드 정책을 애플리케이션에서 정해야 합니다.
핵심 개념
| 구분 | JSON | XML |
|---|---|---|
| 패키지 | encoding/json |
encoding/xml |
| Go 값을 문서로 변환 | json.Marshal |
xml.Marshal |
| 문서를 Go 값으로 변환 | json.Unmarshal |
xml.Unmarshal |
| stream 읽기 | json.Decoder |
xml.Decoder |
| stream 쓰기 | json.Encoder |
xml.Encoder |
| field tag | json:"name,omitempty" |
xml:"name,omitempty" |
| 기본 구조 | object, array, string, number 등 | element, attribute, character data 등 |
| 알 수 없는 필드 | 기본적으로 무시, DisallowUnknownFields 사용 가능 |
구조체와 대응되지 않는 element·attribute는 일반적으로 무시 |
Marshal은 Go 값을 []byte로 변환하고, Unmarshal은 완전한 []byte 데이터를 Go 값에 저장합니다.
Encoder와 Decoder는 io.Writer와 io.Reader를 사용하므로 파일, HTTP body, 표준 입출력처럼 stream을 처리할 때 적합합니다.
디렉터리 구조는 다음과 같이 구성합니다.
sources/
└── json-xml/
└── main.gosources 디렉터리에서 실행합니다.
cd sources
go run ./json-xmlJSON 기본 변환
다음 구조체를 JSON으로 변환해 보겠습니다.
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Tags []string `json:"tags,omitempty"`
Internal string `json:"-"`
note string
}
각 필드의 의미는 다음과 같습니다.
| 선언 | 의미 |
|---|---|
json:"id" |
JSON key를 id로 지정 |
json:"tags,omitempty" |
값이 비어 있으면 tags 생략 |
json:"-" |
JSON 변환 대상에서 항상 제외 |
note string |
소문자로 시작하는 unexported field이므로 변환되지 않음 |
실행 예제:
package main
import (
"encoding/json"
"fmt"
"log"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Tags []string `json:"tags,omitempty"`
Internal string `json:"-"`
note string
}
func main() {
product := Product{
ID: 1001,
Name: "키보드",
Price: 89000,
Tags: []string{"입력장치", "USB"},
Internal: "관리용 메모",
note: "외부에 공개하지 않음",
}
data, err := json.Marshal(product)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
출력:
{"id":1001,"name":"키보드","price":89000,"tags":["입력장치","USB"]}
Internal은 json:"-"가 지정되어 제외되고, note는 exported field가 아니므로 제외됩니다.
사람이 읽기 편한 형태가 필요하면 json.MarshalIndent를 사용할 수 있습니다.
data, err := json.MarshalIndent(product, "", " ")
출력:
{
"id": 1001,
"name": "키보드",
"price": 89000,
"tags": [
"입력장치",
"USB"
]
}
JSON을 구조체로 변환하기
JSON의 구조를 알고 있다면 map[string]any보다 명확한 구조체로 디코딩하는 것이 좋습니다.
package main
import (
"encoding/json"
"fmt"
"log"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
Tags []string `json:"tags"`
}
func main() {
input := []byte(`{
"id": 1001,
"name": "키보드",
"price": 89000,
"tags": ["입력장치", "USB"]
}`)
var product Product
if err := json.Unmarshal(input, &product); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d\n", product.ID)
fmt.Printf("이름: %s\n", product.Name)
fmt.Printf("가격: %d\n", product.Price)
fmt.Printf("태그: %v\n", product.Tags)
}
출력:
ID: 1001
이름: 키보드
가격: 89000
태그: [입력장치 USB]
Unmarshal의 두 번째 인수에는 결과를 저장할 수 있도록 포인터를 전달해야 합니다.
json.Unmarshal(input, &product)
&product가 아니라 product를 전달하면 Unmarshal이 값을 변경할 수 없으므로 오류가 발생합니다.
struct tag와 omitempty
omitempty는 필드 값이 비어 있다고 판단될 때 해당 필드를 출력에서 생략합니다.
JSON v1 패키지에서 일반적으로 비어 있다고 판단되는 값은 다음과 같습니다.
false- 숫자
0 - 빈 문자열
"" - 길이가 0인 array, slice, map
nilpointer 또는 interface
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Nickname string `json:"nickname,omitempty"`
Roles []string `json:"roles,omitempty"`
Active bool `json:"active,omitempty"`
}
func main() {
user := User{
ID: 1,
Name: "홍길동",
}
data, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
출력:
{"id":1,"name":"홍길동"}
Nickname은 빈 문자열이고, Roles는 nil, Active는 false이므로 생략됩니다.
값이 없다는 상태와 0을 구분하기
omitempty를 사용하면서 0이나 false를 실제 값으로 보내야 한다면 pointer를 사용할 수 있습니다.
type UpdateRequest struct {
Stock *int `json:"stock,omitempty"`
Active *bool `json:"active,omitempty"`
}
zero := 0
inactive := false
request := UpdateRequest{
Stock: &zero,
Active: &inactive,
}
pointer 자체가 nil이 아니므로 다음과 같이 출력됩니다.
{"stock":0,"active":false}
이 방식은 부분 수정 API에서 다음 상태를 구분할 때 유용합니다.
- 필드를 보내지 않음: pointer가
nil - 재고를 0으로 변경:
Stock이&zero - 활성 상태를 false로 변경:
Active가&inactive
알 수 없는 JSON 필드 처리
json.Unmarshal과 기본 json.Decoder는 구조체에 없는 JSON 필드를 기본적으로 무시합니다.
예를 들어 구조체에 name만 있어도 입력의 admin은 오류가 되지 않습니다.
package main
import (
"encoding/json"
"fmt"
"log"
)
type UserRequest struct {
Name string `json:"name"`
}
func main() {
input := []byte(`{"name":"홍길동","admin":true}`)
var request UserRequest
if err := json.Unmarshal(input, &request); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", request)
}
출력:
{Name:홍길동}
API 요청처럼 허용되지 않은 필드를 오류로 처리하려면 Decoder.DisallowUnknownFields를 사용합니다.
package main
import (
"encoding/json"
"fmt"
"strings"
)
type UserRequest struct {
Name string `json:"name"`
}
func main() {
input := `{"name":"홍길동","admin":true}`
decoder := json.NewDecoder(strings.NewReader(input))
decoder.DisallowUnknownFields()
var request UserRequest
if err := decoder.Decode(&request); err != nil {
fmt.Println("디코딩 실패:", err)
return
}
fmt.Printf("%+v\n", request)
}
출력:
디코딩 실패: json: unknown field "admin"
DisallowUnknownFields는 오타가 있는 요청이나 서버가 지원하지 않는 필드를 조기에 발견하는 데 도움이 됩니다.
DisallowUnknownFields는 JSON 객체를 구조체로 디코딩할 때 의미가 있습니다.map[string]any는 임의의 key를 받기 위한 타입이므로 알 수 없는 필드라는 개념을 적용할 수 없습니다.
JSON 숫자와 any
JSON 숫자를 any 또는 map[string]any로 디코딩하면 기본적으로 float64가 됩니다.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
input := []byte(`{"id":1001,"price":89000}`)
var value map[string]any
if err := json.Unmarshal(input, &value); err != nil {
log.Fatal(err)
}
fmt.Printf("id 값: %v, 타입: %T\n", value["id"], value["id"])
fmt.Printf("price 값: %v, 타입: %T\n", value["price"], value["price"])
}
출력:
id 값: 1001, 타입: float64
price 값: 89000, 타입: float64
구조를 알고 있다면 숫자 타입을 명확히 선언한 구조체로 디코딩하는 것이 가장 단순합니다.
type Product struct {
ID int64 `json:"id"`
Price int64 `json:"price"`
}
UseNumber로 숫자 표현 보존하기
입력 구조를 미리 알 수 없어 any를 사용해야 한다면 Decoder.UseNumber를 사용할 수 있습니다.
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
)
func main() {
input := `{"id":9007199254740993,"price":89000}`
decoder := json.NewDecoder(strings.NewReader(input))
decoder.UseNumber()
var value map[string]any
if err := decoder.Decode(&value); err != nil {
log.Fatal(err)
}
idNumber, ok := value["id"].(json.Number)
if !ok {
log.Fatal("id가 json.Number가 아닙니다")
}
id, err := idNumber.Int64()
if err != nil {
log.Fatal(err)
}
fmt.Printf("원본 표현: %s\n", idNumber.String())
fmt.Printf("int64 값: %d\n", id)
fmt.Printf("저장 타입: %T\n", value["id"])
}
출력:
원본 표현: 9007199254740993
int64 값: 9007199254740993
저장 타입: json.Number
json.Number는 숫자를 문자열 형태로 보관하고 Int64, Float64 메서드로 필요한 타입으로 변환합니다. 변환할 때 범위와 형식 오류를 확인할 수 있습니다.
Marshal·Unmarshal과 Encoder·Decoder
두 방식의 차이는 입력과 출력 형태에 있습니다.
| 방식 | 입력 또는 출력 | 적합한 상황 |
|---|---|---|
json.Marshal |
Go 값 → []byte |
메모리에서 완성된 JSON 데이터가 필요할 때 |
json.Unmarshal |
[]byte → Go 값 |
완성된 JSON byte slice를 한 번에 읽을 때 |
json.Encoder |
Go 값 → io.Writer |
파일, HTTP 응답, 표준 출력에 직접 쓸 때 |
json.Decoder |
io.Reader → Go 값 |
HTTP body, 파일, 연결 stream에서 읽을 때 |
Encoder 예제
package main
import (
"encoding/json"
"log"
"os"
)
type Event struct {
Type string `json:"type"`
ID int `json:"id"`
}
func main() {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
event := Event{
Type: "created",
ID: 1001,
}
if err := encoder.Encode(event); err != nil {
log.Fatal(err)
}
}
Encoder.Encode는 JSON 값 뒤에 줄바꿈을 추가합니다. 여러 JSON 값을 연속해서 stream으로 쓸 때 편리합니다.
여러 JSON 값을 stream으로 읽기
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"strings"
)
type Event struct {
Type string `json:"type"`
ID int `json:"id"`
}
func main() {
input := `
{"type":"created","id":1001}
{"type":"updated","id":1002}
{"type":"deleted","id":1003}
`
decoder := json.NewDecoder(strings.NewReader(input))
for {
var event Event
err := decoder.Decode(&event)
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %d\n", event.Type, event.ID)
}
}
출력:
created: 1001
updated: 1002
deleted: 1003
하나의 JSON 값만 허용하기
HTTP 요청 body에서 Decode를 한 번만 호출하면 첫 번째 JSON 값 뒤에 추가 JSON 값이 있어도 놓칠 수 있습니다.
예를 들어 다음 입력에는 JSON 객체가 두 개 있습니다.
{"name":"홍길동"} {"name":"김철수"}
하나의 요청 body에 JSON 값 하나만 허용하려면 첫 번째 디코딩 후 두 번째 Decode가 io.EOF인지 확인합니다.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
)
type UserRequest struct {
Name string `json:"name"`
}
func decodeSingleJSON(input string, destination any) error {
decoder := json.NewDecoder(strings.NewReader(input))
decoder.DisallowUnknownFields()
if err := decoder.Decode(destination); err != nil {
return fmt.Errorf("JSON 디코딩 실패: %w", err)
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
if err == nil {
return fmt.Errorf("하나의 JSON 값만 허용됩니다")
}
return fmt.Errorf("추가 데이터 확인 실패: %w", err)
}
return nil
}
func main() {
input := `{"name":"홍길동"} {"name":"김철수"}`
var request UserRequest
if err := decodeSingleJSON(input, &request); err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", request)
}
출력:
하나의 JSON 값만 허용됩니다
입력 크기 제한
Decoder는 io.Reader에서 데이터를 계속 읽을 수 있습니다. 신뢰할 수 없는 입력에 크기 제한이 없으면 과도한 메모리나 CPU를 사용할 가능성이 있습니다.
일반적인 io.Reader에는 io.LimitReader를 적용할 수 있습니다.
const maxInputSize = 1 << 20 // 1 MiB
limitedReader := io.LimitReader(reader, maxInputSize+1)
다만 LimitReader만 사용하면 입력이 잘렸는지 명확히 구분해야 합니다. 최대 크기보다 1 byte 더 읽어 실제 초과 여부를 확인하는 방식이 안전합니다.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
const maxInputSize = 64
type Request struct {
Message string `json:"message"`
}
func decodeLimitedJSON(reader io.Reader, destination any) error {
limited := io.LimitReader(reader, maxInputSize+1)
data, err := io.ReadAll(limited)
if err != nil {
return fmt.Errorf("입력 읽기 실패: %w", err)
}
if len(data) > maxInputSize {
return fmt.Errorf("입력은 최대 %d byte까지 허용됩니다", maxInputSize)
}
if err := json.Unmarshal(data, destination); err != nil {
return fmt.Errorf("JSON 디코딩 실패: %w", err)
}
return nil
}
func main() {
input := []byte(`{"message":"허용된 크기의 메시지"}`)
var request Request
if err := decodeLimitedJSON(bytes.NewReader(input), &request); err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", request)
}
HTTP 서버에서는 http.MaxBytesReader를 사용해 요청 body 크기를 제한할 수 있습니다.
func handler(w http.ResponseWriter, r *http.Request) {
const maxBodySize = 1 << 20 // 1 MiB
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
defer r.Body.Close()
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var request CreateProductRequest
if err := decoder.Decode(&request); err != nil {
http.Error(w, "잘못된 요청입니다", http.StatusBadRequest)
return
}
}
크기 제한은 애플리케이션의 실제 요청 크기와 운영 환경을 기준으로 정해야 합니다.
XML 기본 변환
XML에서는 xml.Name과 XML 전용 struct tag를 사용해 root element, 하위 element, attribute를 지정할 수 있습니다.
package main
import (
"encoding/xml"
"fmt"
"log"
)
type Product struct {
XMLName xml.Name `xml:"product"`
ID int `xml:"id,attr"`
Name string `xml:"name"`
Price int `xml:"price"`
Tags []string `xml:"tags>tag"`
Internal string `xml:"-"`
}
func main() {
product := Product{
ID: 1001,
Name: "키보드",
Price: 89000,
Tags: []string{"입력장치", "USB"},
Internal: "관리용 메모",
}
data, err := xml.MarshalIndent(product, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(xml.Header + string(data))
}
출력:
<?xml version="1.0" encoding="UTF-8"?>
<product id="1001">
<name>키보드</name>
<price>89000</price>
<tags>
<tag>입력장치</tag>
<tag>USB</tag>
</tags>
</product>
XML tag의 의미는 다음과 같습니다.
| tag | 의미 |
|---|---|
xml:"product" |
root element 이름 지정 |
xml:"id,attr" |
id를 element가 아닌 attribute로 표현 |
xml:"tags>tag" |
<tags> 아래에 반복되는 <tag> element로 표현 |
xml:"-" |
XML 변환 대상에서 제외 |
xml.MarshalIndent는 XML 선언을 자동으로 추가하지 않습니다. XML 선언이 필요하면 xml.Header를 앞에 붙일 수 있습니다.
XML을 구조체로 변환하기
package main
import (
"encoding/xml"
"fmt"
"log"
)
type Product struct {
XMLName xml.Name `xml:"product"`
ID int `xml:"id,attr"`
Name string `xml:"name"`
Price int `xml:"price"`
Tags []string `xml:"tags>tag"`
}
func main() {
input := []byte(`
<product id="1001">
<name>키보드</name>
<price>89000</price>
<tags>
<tag>입력장치</tag>
<tag>USB</tag>
</tags>
</product>
`)
var product Product
if err := xml.Unmarshal(input, &product); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d\n", product.ID)
fmt.Printf("이름: %s\n", product.Name)
fmt.Printf("가격: %d\n", product.Price)
fmt.Printf("태그: %v\n", product.Tags)
}
출력:
ID: 1001
이름: 키보드
가격: 89000
태그: [입력장치 USB]
XMLName에 xml:"product"를 지정하면 입력의 root element가 <product>인지 확인하는 역할도 합니다. 다른 이름이 들어오면 오류가 발생합니다.
XML struct tag 사용법
encoding/xml은 element와 attribute 외에도 문자 데이터, 중첩 경로 등을 struct tag로 지정할 수 있습니다.
type Article struct {
XMLName xml.Name `xml:"article"`
ID string `xml:"id,attr"`
Title string `xml:"title"`
Author string `xml:"metadata>author"`
Content string `xml:",chardata"`
}
주요 option은 다음과 같습니다.
| option | 의미 |
|---|---|
,attr |
XML attribute로 처리 |
,chardata |
element 내부의 문자 데이터 저장 |
,cdata |
marshal할 때 CDATA section으로 작성 |
,innerxml |
내부 XML 원문 저장 또는 그대로 출력 |
,comment |
XML comment로 처리 |
a>b>c |
중첩된 element 경로 지정 |
- |
변환에서 제외 |
innerxml은 XML 내용을 구조화하지 않고 원문으로 보관하므로, 신뢰할 수 없는 입력을 다시 출력하거나 다른 문서에 삽입할 때는 사용 목적을 명확히 해야 합니다.
XML의 알 수 없는 element 처리
xml.Unmarshal과 xml.Decoder는 구조체 필드에 대응되지 않는 element와 attribute를 일반적으로 건너뜁니다.
package main
import (
"encoding/xml"
"fmt"
"log"
)
type User struct {
XMLName xml.Name `xml:"user"`
Name string `xml:"name"`
}
func main() {
input := []byte(`
<user admin="true">
<name>홍길동</name>
<role>administrator</role>
</user>
`)
var user User
if err := xml.Unmarshal(input, &user); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", user)
}
출력에는 Name만 저장됩니다.
{XMLName:{Space: Local:user} Name:홍길동}
encoding/xml에는 JSON의 DisallowUnknownFields와 같은 구조체 전용 option이 없습니다. 알 수 없는 element를 금지해야 한다면 다음 방법을 고려해야 합니다.
Decoder.Token으로 element를 직접 검사UnmarshalXML메서드를 구현해 허용 element를 검증- 디코딩 후 별도의 schema 또는 비즈니스 검증 수행
Decoder.Strict는 알 수 없는 element를 금지하는 설정이 아닙니다. 잘못 닫힌 element나 일부 비표준 XML 문법을 얼마나 엄격하게 처리할지 결정하는 설정입니다.
XML stream 처리
xml.Decoder는 io.Reader에서 XML token이나 element를 순차적으로 읽을 수 있습니다.
package main
import (
"encoding/xml"
"fmt"
"io"
"log"
"strings"
)
type Product struct {
ID int `xml:"id,attr"`
Name string `xml:"name"`
}
func main() {
input := `
<products>
<product id="1001"><name>키보드</name></product>
<product id="1002"><name>마우스</name></product>
</products>
`
decoder := xml.NewDecoder(strings.NewReader(input))
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
start, ok := token.(xml.StartElement)
if !ok || start.Name.Local != "product" {
continue
}
var product Product
if err := decoder.DecodeElement(&product, &start); err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s\n", product.ID, product.Name)
}
}
출력:
1001: 키보드
1002: 마우스
문서 전체를 하나의 큰 구조체에 저장하지 않고 필요한 element를 순차적으로 처리할 수 있으므로 큰 XML 문서에 유용합니다. 다만 입력 크기와 element 개수 같은 애플리케이션 수준의 제한은 별도로 적용해야 합니다.
JSON과 XML을 함께 지원하는 구조체
하나의 구조체에 JSON tag와 XML tag를 함께 지정할 수 있습니다.
type Product struct {
XMLName xml.Name `json:"-" xml:"product"`
ID int `json:"id" xml:"id,attr"`
Name string `json:"name" xml:"name"`
Price int `json:"price" xml:"price"`
Tags []string `json:"tags,omitempty" xml:"tags>tag,omitempty"`
}
XMLName은 XML 변환에 필요하지만 JSON에는 필요하지 않으므로 json:"-"로 제외합니다.
package main
import (
"encoding/json"
"encoding/xml"
"fmt"
"log"
)
type Product struct {
XMLName xml.Name `json:"-" xml:"product"`
ID int `json:"id" xml:"id,attr"`
Name string `json:"name" xml:"name"`
Price int `json:"price" xml:"price"`
Tags []string `json:"tags,omitempty" xml:"tags>tag,omitempty"`
}
func main() {
product := Product{
ID: 1001,
Name: "키보드",
Price: 89000,
Tags: []string{"입력장치", "USB"},
}
jsonData, err := json.MarshalIndent(product, "", " ")
if err != nil {
log.Fatal(err)
}
xmlData, err := xml.MarshalIndent(product, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println("JSON")
fmt.Println(string(jsonData))
fmt.Println("\nXML")
fmt.Println(xml.Header + string(xmlData))
}
외부 형식이 다르더라도 내부 도메인 구조가 완전히 같다면 하나의 구조체를 공유할 수 있습니다. 하지만 JSON과 XML의 표현 차이가 커지면 전송 전용 구조체를 각각 분리하는 편이 더 명확할 수 있습니다.
전체 예제
아래 예제는 다음 내용을 한 번에 확인합니다.
- 구조체의 JSON·XML 변환
- JSON unknown field 거부
UseNumber사용- JSON stream 디코딩
- XML stream 디코딩
프로젝트를 생성합니다.
mkdir -p sources/json-xml
cd sources/json-xml
go mod init example.com/json-xml
go mod edit -go=1.26.1
main.go:
package main
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"log"
"strings"
)
type Product struct {
XMLName xml.Name `json:"-" xml:"product"`
ID int64 `json:"id" xml:"id,attr"`
Name string `json:"name" xml:"name"`
Price int64 `json:"price" xml:"price"`
Tags []string `json:"tags,omitempty" xml:"tags>tag,omitempty"`
Internal string `json:"-" xml:"-"`
}
type Event struct {
Type string `json:"type"`
ID int64 `json:"id"`
}
func main() {
product := Product{
ID: 1001,
Name: "키보드",
Price: 89000,
Tags: []string{"입력장치", "USB"},
Internal: "외부에 출력하지 않는 값",
}
printJSON(product)
printXML(product)
decodeStrictJSON()
decodeJSONNumber()
decodeJSONStream()
decodeXMLStream()
}
func printJSON(product Product) {
data, err := json.MarshalIndent(product, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println("=== JSON Marshal ===")
fmt.Println(string(data))
}
func printXML(product Product) {
data, err := xml.MarshalIndent(product, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println("\n=== XML Marshal ===")
fmt.Println(xml.Header + string(data))
}
func decodeStrictJSON() {
input := `{"id":1001,"name":"키보드","price":89000,"unknown":true}`
decoder := json.NewDecoder(strings.NewReader(input))
decoder.DisallowUnknownFields()
var product Product
err := decoder.Decode(&product)
fmt.Println("\n=== JSON Unknown Field ===")
if err != nil {
fmt.Println("오류:", err)
return
}
fmt.Printf("%+v\n", product)
}
func decodeJSONNumber() {
input := `{"id":9007199254740993}`
decoder := json.NewDecoder(strings.NewReader(input))
decoder.UseNumber()
var value map[string]any
if err := decoder.Decode(&value); err != nil {
log.Fatal(err)
}
number, ok := value["id"].(json.Number)
if !ok {
log.Fatal("id가 json.Number가 아닙니다")
}
id, err := number.Int64()
if err != nil {
log.Fatal(err)
}
fmt.Println("\n=== JSON Number ===")
fmt.Printf("표현: %s, int64: %d, 타입: %T\n", number, id, value["id"])
}
func decodeJSONStream() {
input := `
{"type":"created","id":1001}
{"type":"updated","id":1002}
`
decoder := json.NewDecoder(strings.NewReader(input))
fmt.Println("\n=== JSON Stream ===")
for {
var event Event
err := decoder.Decode(&event)
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %d\n", event.Type, event.ID)
}
}
func decodeXMLStream() {
input := `
<products>
<product id="1001"><name>키보드</name><price>89000</price></product>
<product id="1002"><name>마우스</name><price>45000</price></product>
</products>
`
decoder := xml.NewDecoder(strings.NewReader(input))
fmt.Println("\n=== XML Stream ===")
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
start, ok := token.(xml.StartElement)
if !ok || start.Name.Local != "product" {
continue
}
var product Product
if err := decoder.DecodeElement(&product, &start); err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s, %d원\n", product.ID, product.Name, product.Price)
}
}
실행합니다.
cd sources
go run ./json-xml
출력 예:
=== JSON Marshal ===
{
"id": 1001,
"name": "키보드",
"price": 89000,
"tags": [
"입력장치",
"USB"
]
}
=== XML Marshal ===
<?xml version="1.0" encoding="UTF-8"?>
<product id="1001">
<name>키보드</name>
<price>89000</price>
<tags>
<tag>입력장치</tag>
<tag>USB</tag>
</tags>
</product>
=== JSON Unknown Field ===
오류: json: unknown field "unknown"
=== JSON Number ===
표현: 9007199254740993, int64: 9007199254740993, 타입: json.Number
=== JSON Stream ===
created: 1001
updated: 1002
=== XML Stream ===
1001: 키보드, 89000원
1002: 마우스, 45000원
적용 기준
JSON이나 XML을 처리할 때는 다음 기준으로 선택할 수 있습니다.
| 상황 | 권장 방식 |
|---|---|
| 입력 구조를 알고 있음 | 구체적인 구조체로 decode |
| 동적인 JSON 구조 | map[string]any, 필요하면 UseNumber 적용 |
완전한 []byte 데이터 |
Marshal·Unmarshal |
| HTTP body, 파일, 표준 입력 | Encoder·Decoder |
| JSON API에서 오타 필드 거부 | DisallowUnknownFields |
| 요청 body 처리 | 크기 제한과 단일 값 여부 확인 |
| 큰 XML 문서의 일부 element 처리 | Decoder.Token·DecodeElement |
| JSON과 XML 표현 구조가 크게 다름 | 전송 전용 구조체 분리 |
구조체로 디코딩하면 필드 타입이 명확해지고, 숫자 변환과 type assertion을 줄일 수 있습니다. 외부 요청을 받을 때는 단순히 디코딩 성공 여부만 확인하지 말고 필수값, 값 범위, 문자열 길이 같은 비즈니스 검증도 추가해야 합니다.
정리
encoding/json과 encoding/xml은 Go 표준 라이브러리에서 JSON과 XML을 처리하는 기본 패키지입니다.
두 패키지는 exported field를 중심으로 구조체를 변환하며, struct tag를 사용해 외부 필드 이름과 출력 방법을 지정합니다.
JSON 구조를 알고 있다면 map[string]any보다 명확한 구조체로 디코딩하는 것이 좋습니다. any로 JSON 숫자를 받으면 기본적으로 float64가 되므로 숫자의 정확한 표현이 필요하면 구조체의 정수 타입이나 Decoder.UseNumber를 사용해야 합니다.
완전한 byte slice에는 Marshal과 Unmarshal이 간단하고, HTTP body나 파일 같은 stream에는 Encoder와 Decoder가 적합합니다.
신뢰할 수 없는 입력을 처리할 때는 다음 사항을 함께 적용하는 것이 중요합니다.
- 입력 크기 제한
- JSON unknown field 정책
- 하나의 문서만 허용할지 여부
- 필수값과 값 범위 검증
- XML에서 허용할 element와 attribute 검증
디코딩은 외부 문서를 Go 값으로 옮기는 과정일 뿐이며, 해당 값이 애플리케이션에서 유효하고 안전한지는 별도의 검증 단계에서 확인해야 합니다.
참고 자료
- Go
encoding/json공식 문서: https://pkg.go.dev/encoding/json - Go
encoding/xml공식 문서: https://pkg.go.dev/encoding/xml - Go 공식 블로그, JSON and Go: https://go.dev/blog/json
- Go
net/http.MaxBytesReader공식 문서: https://pkg.go.dev/net/http#MaxBytesReader - Go
io.LimitReader공식 문서: https://pkg.go.dev/io#LimitReader
'프로그래밍 > Go' 카테고리의 다른 글
| Go 채널과 context 취소 신호 이해하기 (0) | 2026.07.30 |
|---|---|
| Go 공유 상태 관리: atomic, Mutex, owner goroutine (0) | 2026.07.27 |
| Go Worker Pool, WaitGroup, Rate Limiting (0) | 2026.07.27 |
| Go Timer와 Ticker (0) | 2026.07.24 |
| Go panic과 recover (0) | 2026.07.23 |
댓글