Prologue
Go에서 오브젝트를 JSON으로 변환하려면, 해당 오브젝트를 기술하는 구조체가 선언되어 있어야 합니다. 예를 들자면 다음과 같습니다.
type Score struct {
Korean uint `json:"korean,omitempty"`
Math uint `json:"math,omitempty"`
English uint `json:"english,omitempty"`
}
type UserV1 struct {
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Age uint64 `json:"age"`
Score Score `json:"score,omitempty"`
}
위 구조체의 필드 자료형 옆에 적힌 omitempty
태그는 해당 필드의 데이터가 존재하지 않으면, Marshaling 수행 시, 해당 필드는 제외하고 Marshaling을 수행하게 하는 역할을 합니다.
그러나 실제로 위와 같이 구조체를 선언하여 사용하면 omitempty
가 선언되어 있음에도 필드의 이름이 출력됩니다. 예를 들면 이렇습니다.
{
"username": "cathy",
"name": "Cathy",
"email": "cathy@cathy.com",
"age": 26,
"score": {}
}
해당 데이터의 Score 필드가 입력되지 않은 상태임에도, {}
가 출력됩니다. 아예 "score"
필드가 보이지 않도록 하는 방법은 없을까요?
Testing
encoding/json 공식 문서에서는 omitempty
를 이렇게 설명하고 있습니다.
The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.
"omitempty" 옵션은 해당 필드의 값이 empty value일 때 변환 대상으로부터 생략되어야 함을 나타냅니다. empty value는 다음과 같이 정의할 수 있습니다.
false
,0
,nil pointer
,nil interface value
,empty array, slice, map, or string
우리는 구조체 필드를 숨기고 싶습니다. 하지만 empty value에는 구조체 필드에 관한 내용이 없네요. 그럼 어떻게 해야할까요? 우리는 nil pointer
에 주목해볼 필요가 있습니다. 바로 구조체 필드를 구조체 포인터 필드로 바꿔 사용하는 것입니다.
지금부터 일반 구조체 필드를 사용했을 때와 구조체 포인터 필드를 사용했을 때 어떻게 결괏값이 달라지는지 간단한 실험을 통해 확인해보겠습니다.
예제 데이터 모델
- UserV1 - Score 데이터를 Pointer로 받지 않은 경우
type UserV1 struct {
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Age uint64 `json:"age"`
Score Score `json:"score,omitempty"`
}
- UserV2 - Score 데이터를 Pointer로 받은 경우
type UserV2 struct {
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Age uint64 `json:"age"`
Score *Score `json:"score,omitempty"`
}
예제 데이터 변수
- Alice - UserV1 타입의 데이터이며, Score 데이터가 포함되어 있음
userV1MockupScore = UserV1{ UserName: "alice", Name: "Alice", Email: "alice@alice.com", Age: 28, Score: Score{ Korean: 88, Math: 78, English: 98, }, }
- Bob - UserV2 타입의 데이터이며, Score 데이터가 포함되어 있음
userV2MockUpScore = UserV2{ UserName: "bob", Name: "Bob", Email: "bob@bob.com", Age: 38, Score: &Score{ Korean: 10, Math: 20, English: 30, }, }
- Cathy - UserV1 타입의 데이터이며, Score 데이터가 포함되어 있지 않음
userV1MockupNonScore = UserV1{ UserName: "cathy", Name: "Cathy", Email: "cathy@cathy.com", Age: 26, }
- David - UserV2 타입의 데이터이며, Score 데이터가 포함되어 있지 않음
userV2MockUpNonScore = UserV2{ UserName: "david", Name: "David", Email: "david@david.com", Age: 32, }
테스트 진행
이제 각 데이터를 API로 호출하여 어떻게 응답이 오는지 확인해보겠습니다.
Alice
Alice는 UserV1 (포인터 X) 타입이며, Score 데이터가 존재하는 데이터입니다. 위와 같이 결과는 모든 데이터가 잘 채워져서 왔습니다.
Bob
Bob은 UserV2 (포인터 O) 타입이며, Score 데이터가 존재하는 데이터입니다. Alice와 마찬가지로 모든 데이터가 잘 채워져서 왔습니다.
Cathy
Cathy는 UserV1 (포인터 X) 타입이며, Score 데이터가 존재하지 않는 데이터입니다. Score 데이터가 없으면 JSON Body에 해당 데이터가 표기 되지 않기를 원하는 우리의 바람과 달리, 해당 데이터가 빈 데이터로 표현되어 전달되었음을 확인할 수 있습니다.
David
David는 UserV2 (포인터 O) 타입이며, Score 데이터가 존재하지 않는 데이터입니다. 우리가 바라던 대로 Score 데이터가 없을 때, JSON Body에 해당 데이터가 아예 존재하지 않음을 확인할 수 있습니다.
테스트 결과
Cathy와 David는 모두 Score 필드에 데이터가 없는 상태입니다. 차이점이라면 Cathy는 Score를 구조체 필드로 받고 있고, David는 Score를 구조체 포인터 필드로 받았다는 점입니다. 실험에 앞서서 공식 문서에서 설명했듯이, omitempty
옵션의 영향을 받으려면 nil pointer
여야 합니다. 따라서 Score를 구조체 포인터 형식으로 정의한 David만 omitempty
옵션이 적용된 것입니다.
결론
요약해보자면, 필드의 자료형이 구조체일 때, 해당 구조체가 omitempty
태그의 영향을 받아서 데이터가 없을 때 필드의 이름도 출력되지 않도록 하고 싶다면, 필드의 자료형을 구조체 포인터로 선언해주면 됩니다.
예제 코드
아래는 본 튜토리얼에서 사용한 전체 예제 코드입니다. 본 코드는 제가 직접 작성한 예제 코드입니다. 본 GitHub 주소에서도 확인하실 수 있습니다.
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
type Score struct {
Korean uint `json:"korean,omitempty"`
Math uint `json:"math,omitempty"`
English uint `json:"english,omitempty"`
}
type UserV1 struct {
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Age uint64 `json:"age"`
Score Score `json:"score,omitempty"`
}
type UserV2 struct {
UserName string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Age uint64 `json:"age"`
Score *Score `json:"score,omitempty"`
}
var (
userV1MockupScore = UserV1{
UserName: "alice",
Name: "Alice",
Email: "alice@alice.com",
Age: 28,
Score: Score{
Korean: 88,
Math: 78,
English: 98,
},
}
userV1MockupNonScore = UserV1{
UserName: "cathy",
Name: "Cathy",
Email: "cathy@cathy.com",
Age: 26,
}
userV2MockUpScore = UserV2{
UserName: "bob",
Name: "Bob",
Email: "bob@bob.com",
Age: 38,
Score: &Score{
Korean: 10,
Math: 20,
English: 30,
},
}
userV2MockUpNonScore = UserV2{
UserName: "david",
Name: "David",
Email: "david@david.com",
Age: 32,
}
)
func main() {
e := echo.New()
e.GET("/api/v1/user/:username", GetUser)
e.Logger.Fatal(e.Start(":30000"))
}
func GetUser(c echo.Context) error {
username := c.Param("username")
var userdata interface{}
switch username {
case "alice":
userdata = userV1MockupScore
case "bob":
userdata = userV2MockUpScore
case "cathy":
userdata = userV1MockupNonScore
case "david":
userdata = userV2MockUpNonScore
}
return c.JSONPretty(http.StatusOK, userdata, " ")
}
마무리
여기까지 따라오시느라 고생이 많으셨습니다. 만약 이 튜토리얼이 도움이 되셨다면 글 좌측 하단의 하트❤를 눌러주시면 감사하겠습니다.
혹시라도 튜토리얼에 이상이 있거나, 이해가 가지 않으시는 부분, 또는 추가적으로 궁금하신 내용이 있다면 주저 마시고 댓글💬을 남겨주세요! 빠른 시간 안에 답변을 드리겠습니다 😊
참고
'IT > Go' 카테고리의 다른 글
[Go/Golang] bufio.Scanner를 이용할 때 주의 사항 (0) | 2021.05.22 |
---|---|
[Go/Golang] 정수형 자료를 입력 받을 때, Scanner 사용하기 (0) | 2021.05.13 |
[Go/Golang] Zap과 시간 기반 파일 로테이션 로깅 수행하기 (0) | 2021.03.29 |
[Go/Golang] Go Application에 Prometheus Exporter 연동하기 (2) | 2021.03.16 |
[Go/Golang] Map 자료형을 Struct로 변환하기(mapstructure) (0) | 2021.03.15 |