고(Go) JSON 인코딩의 숨은 병기, omitempty 태그 완벽 분석! (깔끔한 JSON 만들기 꿀팁)
omitempty
, 너는 대체 누구냐? (기본 개념부터 확실하게!)
고(Go) 언어에서 구조체(struct) 필드에 붙이는 '태그(tag)'는 마치 옷에 붙이는 이름표처럼, 해당 필드에 특별한 정보를 추가해주는 역할을 합니다.
JSON 데이터를 다룰 때 자주 사용되는 태그 중 하나가 바로 omitempty
인데요.
이름에서부터 뭔가 '비어있으면 생략한다(omit if empty)'는 느낌이 오지 않나요?
네, 맞습니다! omitempty
태그는 JSON 인코더(Go 데이터를 JSON 문자열로 바꿔주는 녀석)에게 "이 필드의 값이 비어있으면, JSON 결과물에서 아예 빼버려!"라고 지시하는 역할을 합니다.
그렇다면 고(Go) 언어에서 '값이 비어있다'는 것은 어떤 의미일까요? 타입별로 조금씩 다른데요.
- 불리언(boolean) 타입:
false
일 때 - 숫자(numeric) 타입:
0
일 때 - 문자열(string) 타입:
""
(빈 문자열)일 때 - 포인터(pointer), 인터페이스(interface), 맵(map), 슬라이스(slice), 채널(channel) 타입:
nil
일 때 - 배열(array), 슬라이스(slice), 맵(map): 길이가
0
일 때 (요소가 하나도 없을 때)
예를 들어, 이런 구조체가 있다고 해봅시다.
type Example struct {
Name string `json:"name,omitempty"` // 이름 필드, 비어있으면 생략
Age int `json:"age,omitempty"` // 나이 필드, 0이면 생략
Email string `json:"email,omitempty"` // 이메일 필드, 빈 문자열이면 생략
}
만약 Example
구조체 변수의 Age
필드가 0
이고 Email
필드가 빈 문자열이라면, 이 구조체를 JSON으로 변환했을 때 age
와 email
필드는 결과물에 아예 포함되지 않게 됩니다.
꽤 똑똑하죠?.
omitempty
태그, 이렇게 사용해요! (기본 사용법)
자, 그럼 실제 코드를 통해 omitempty
태그를 어떻게 사용하는지 한번 살펴볼까요?.
강아지 정보를 담는 Dog
라는 구조체를 예로 들어보겠습니다.
package main
import (
"encoding/json"
"fmt"
)
type Dog struct {
Breed string `json:"breed"` // 견종 필드 (항상 포함)
WeightKg int `json:"weight_kg,omitempty"` // 몸무게 필드 (0이면 생략)
}
func main() {
// 몸무게 정보가 없는 강아지 객체를 만듭니다.
// WeightKg 필드는 int 타입의 제로 값인 0으로 자동 초기화됩니다.
d := Dog{
Breed: "pug", // 견종은 퍼그
}
// 이 강아지 객체를 JSON 문자열로 변환합니다.
b, _ := json.Marshal(d) // 에러 처리는 간단하게 생략했습니다.
// 변환된 JSON 문자열을 출력합니다.
fmt.Println(string(b)) // 결과: {"breed":"pug"}
}
위 코드에서 Dog
구조체의 WeightKg
필드에는 omitempty
태그가 붙어있습니다.
그래서 d
라는 강아지 객체를 만들 때 WeightKg
값을 따로 지정해주지 않으면, 정수 타입의 기본값인 0
이 들어가게 되는데요.
이 0
은 omitempty
태그에 의해 '비어있는 값'으로 간주되어, JSON으로 변환될 때 weight_kg
필드가 아예 사라진 것을 볼 수 있습니다.
만약 WeightKg
에 0
이 아닌 다른 값을 넣어주면, 당연히 JSON 결과물에 weight_kg
필드가 포함되어 나타나겠죠?.
중첩된 구조체와 omitempty
, 조금 더 까다로운 이야기
omitempty
태그는 기본적으로 훌륭하게 작동하지만, 구조체 안에 또 다른 구조체가 들어있는 경우(중첩 구조체)에는 조금 주의해야 할 점이 있습니다.
단순히 중첩된 구조체 필드에 omitempty
를 붙인다고 해서, 그 내부 필드들이 모두 비어있을 때 자동으로 외부 구조체 필드까지 생략되지는 않기 때문인데요.
예를 들어, 강아지의 크기 정보를 담는 Dimensions
구조체가 Dog
구조체 안에 포함된 경우를 생각해봅시다.
package main
import (
"encoding/json"
"fmt"
)
type Dimensions struct {
Height int `json:"height,omitempty"` // 높이 (0이면 생략)
Width int `json:"width,omitempty"` // 너비 (0이면 생략)
}
type Dog struct {
Breed string `json:"breed"`
WeightKg int `json:"weight_kg,omitempty"`
Size Dimensions `json:"size,omitempty"` // 크기 정보 (Dimensions 구조체)
}
func main() {
d := Dog{
Breed: "pug",
// Size 필드는 Dimensions 타입의 제로 값으로 초기화됩니다.
// 즉, d.Size.Height와 d.Size.Width는 모두 0입니다.
}
b, _ := json.Marshal(d)
fmt.Println(string(b)) // 결과: {"breed":"pug","size":{"height":0,"width":0}}
}
어라? Dog
구조체의 Size
필드에 omitempty
를 붙였는데도, Size
내부의 Height
와 Width
가 모두 0
임에도 불구하고 JSON 결과물에 size
필드가 그대로 남아있네요!
심지어 height
와 width
필드에도 omitempty
가 붙어 있어서 0
이면 생략될 줄 알았는데, size
라는 껍데기가 남아있으니 그 안에 억지로 0
으로 채워져서 나타난 모습입니다.
이런 상황에서 Size
필드 자체를 (내부 값들이 모두 비어있을 때) 통째로 생략하고 싶다면, Size
필드의 타입을 포인터(*Dimensions
)로 바꿔주어야 합니다.
package main
import (
"encoding/json"
"fmt"
)
type Dimensions struct {
Height int `json:"height,omitempty"`
Width int `json:"width,omitempty"`
}
type Dog struct {
Breed string `json:"breed"`
WeightKg int `json:"weight_kg,omitempty"`
Size *Dimensions `json:"size,omitempty"` // Size 필드를 포인터 타입으로 변경!
}
func main() {
d := Dog{
Breed: "pug",
// Size 필드를 nil (포인터의 제로 값)로 두면, omitempty에 의해 생략됩니다.
}
b, _ := json.Marshal(d)
fmt.Println(string(b)) // 결과: {"breed":"pug"}
}
이제 Dog
구조체의 Size
필드가 *Dimensions
(Dimensions 구조체에 대한 포인터) 타입이 되었습니다.
이렇게 하면, Size
필드의 값이 nil
(아무것도 가리키지 않는 상태)일 때 omitempty
태그가 작동하여 JSON 결과물에서 size
필드가 깔끔하게 사라지게 됩니다.
중첩 구조체를 다룰 때는 이 포인터 활용법을 꼭 기억해주세요!
omitempty
사용 시 특별히 고려해야 할 점들!
omitempty
태그는 매우 유용하지만, 몇 가지 특별한 상황에서는 개발자의 의도와 다르게 동작할 수도 있어서 주의가 필요합니다.
제로 값(Zero Value) vs 의도적으로 생략된 필드: 혼동 주의!
omitempty
를 사용하면 필드 값이 제로 값(숫자 0, 빈 문자열 등)일 때 JSON에서 해당 필드가 생략됩니다.
그런데 만약, '고객 수'처럼 값이 실제로 0
인 것이 의미가 있는 필드에 omitempty
를 사용하면 어떻게 될까요?
package main
import (
"encoding/json"
"fmt"
)
type Restaurant struct {
NumberOfCustomers int `json:"number_of_customers,omitempty"` // 고객 수 (0이면 생략)
Name string `json:"name"` // 식당 이름 (항상 포함)
}
func main() {
r := Restaurant{
Name: "Diner", // 식당 이름은 "Diner"
// NumberOfCustomers는 0으로 초기화됩니다.
}
b, _ := json.Marshal(r)
fmt.Println(string(b)) // 결과: {"name":"Diner"}
}
위 예제에서 NumberOfCustomers
필드는 0
이라는 값을 가지고 있지만, omitempty
때문에 JSON 결과물에서 아예 사라져 버렸습니다.
만약 JSON을 받는 쪽에서 "어? 고객 수 정보가 없네? 그럼 고객이 아직 없는 건가?"라고 오해할 수도 있겠죠?
이처럼 필드의 제로 값이 실제로 의미를 가지는 경우에는 해당 필드에 omitempty
태그를 사용하지 않는 것이 좋습니다.
항상 필드의 특성을 고려해서 omitempty
사용 여부를 결정해야 데이터의 완전성을 지킬 수 있습니다.
time.Time
타입의 필드를 다룰 때
고(Go) 언어의 time.Time
타입은 날짜와 시간 정보를 다루는 구조체입니다.
그런데 time.Time{}
(time.Time 타입의 제로 값)은 omitempty
에 의해 '비어있는 값'으로 간주되지 않습니다.
그래서 time.Time
타입의 필드가 제로 값일 때도 JSON에서 생략되도록 하려면, 위에서 중첩 구조체를 다뤘던 것처럼 해당 필드를 포인터(*time.Time
) 타입으로 선언해야 합니다.
package main
import (
"encoding/json"
"fmt"
"time"
)
type Event struct {
Name string `json:"name"`
Timestamp *time.Time `json:"timestamp,omitempty"` // 타임스탬프 필드를 포인터 타입으로!
}
func main() {
// Timestamp 필드를 nil로 두면 omitempty에 의해 생략됩니다.
e := Event{
Name: "Conference",
}
b, _ := json.Marshal(e)
fmt.Println(string(b)) // 결과: {"name":"Conference"}
// 만약 특정 시간을 지정하고 싶다면,
now := time.Now()
eWithTime := Event{
Name: "Meeting",
Timestamp: &now, // 시간 값의 주소를 할당합니다.
}
bWithTime, _ := json.Marshal(eWithTime)
// 결과 예시: {"name":"Meeting","timestamp":"2024-07-27T15:04:05.123456Z"} (시간 값에 따라 다름)
fmt.Println(string(bWithTime))
}
이렇게 Timestamp
필드를 *time.Time
으로 만들고, 값이 nil
일 경우에만 JSON에서 생략되도록 할 수 있습니다.
마무리하며: omitempty
로 JSON 마법사가 되어보세요!
지금까지 고(Go) 언어에서 JSON을 다룰 때 유용하게 사용되는 omitempty
태그에 대해 자세히 알아봤습니다.omitempty
는 비어있는 필드를 깔끔하게 생략하여 JSON 결과물을 더 보기 좋고 효율적으로 만들어주는 강력한 도구인데요.
하지만 어떤 값이 '비어있는 것'으로 간주되는지, 중첩 구조체나 제로 값과 어떻게 상호작용하는지를 정확히 이해하는 것이 중요합니다.
오늘 배운 내용을 잘 활용하셔서, 여러분의 고(Go) 애플리케이션에서 JSON 데이터를 더욱 효과적으로 다루실 수 있기를 바랍니다!
'Go' 카테고리의 다른 글
Go 언어에서 배열에 특정 값이 있는지 확인하는 꿀팁! (feat. 슬라이스 활용법) (1) | 2025.06.03 |
---|---|
Go 언어, 상속 대신 '조합'으로 더 유연하게! 구조체 상속 완벽 이해하기 (2) | 2025.06.03 |
고(Go) 언어의 'goto' 문, 과연 필요악일까? 제대로 알고 사용하기 위한 모든 것! (0) | 2025.05.30 |
고(Go) 개발자를 위한 필수템! 데이터베이스 마이그레이션, '구스(Goose)'로 쉽고 빠르게! (핵심 기능 총정리) (0) | 2025.05.30 |
고(Go) 언어 전역 변수, 양날의 검! 똑똑하게 활용하는 방법은? (핵심 정리) (0) | 2025.05.30 |