본문 바로가기
Golang

Gin / time 타입 parsing 오류 (parsing time "~~~" as "~~~": cannot parse "~~~" as "~~~")

by Limm_jk 2021. 9. 25.

진토닉이 땡기는 프레임워크

평화롭게 Gin을 가지고 놀던 어느 날, 시간 Field를 Request로 받아서 작업해야 할 일이 생겨서 관련하여 공부해보았습니다.

 

대충 format 지정해주고 넘기면 알아서 찰떡같이 parsing 해주리라 생각했던 것과는 달리, 꽤나 골치가 아팠어서 블로그에 정리해봅니다.


위에 이야기를 한 것과 같이 시간 정보를 parsing 해주기 위한 format을 어떻게 지정해주는지 알아봤습니다. 그리고 금세 나오는 정보는 아래와 같은 코드였습니다.

type Person struct {
	Birthday time.Time `json:"birthday" time_format:"2006-01-02 15:14:13"`
}

아하! 이런 식으로 bind할 structure에 time_format을 지정해주면 되는구나!

 

그래서 다음과 같이 지정해주고 요청을 보냈을 때 다음과 같은 에러 메세지를 만날 수 있었습니다.

parsing time "\"2016-01-02 15:04:05\"" as "\"2006-01-02T15:04:05Z07:00\"": cannot parse " 15:04:05\"" as "T"

대충 읽어봤을 때, 위에 time_format이 제대로 적용되지 않았음을 알 수 있었습니다.

 

이러한 문제를 조금 더 찾아봤을 때, 해당 문제가 많은 Gin 초심자들을 괴롭히고 있는 문제이며 gin의 저장소 issue에도 자주 올라오는 문제임을 깨닫게 됩니다.

 

문제점

먼저 위에서 사용했던 time_format부터 다시 보겠습니다.

time_format tag is only used in form binding, not json;
출처 : stack overflow

그렇습니다. 위에서 이야기했던 time_format은 form형식으로 binding할 때 사용하는 tag이기 때문에 위에서 Json형식을 binding할 때 당연히 동작하지 않았을 것입니다.

 

그렇다면 json으로 binding할 때는 왜 문제가 생기며, 어떻게 format을 지정해줘야 하는 것일까요?

 

이 문제는 gin의 issue를 찾아보다가 문제의 이유와 해답을 찾을 수 있었습니다.

Has nothing to do with gin but what the golang JSON Decoder expects for time values. It must be in the format of RFC3339 in order to be successfully decoded when using the JSON binder for ShouldBindJSON, or BindJSON.
출처 : github - gin-gonic/gin Issue

gin에서 일어나는 문제가 아닌 Golang의 JSON Decoder가 당연히 시간 값은 RFC3339의 형식으로 들어올 것이라고 생각하고 있기 때문에 일어나는 문제였습니다.

 

그렇기 때문에 unmarshaling 과정에서 옳지 않은 값이 들어오고 binding에 실패하게 되는 것입니다.

 

해결책

그렇다면 내부적으로 동작을 그렇게 해버리는데 어떻게 해결을 할 수 있을까요?

함께 제안된 해결책은 time type을 사용하는 것이 아닌 자체적으로 type을 지정하여 해결하는 방법을 제시합니다.

import (
	"encoding/json"
	"time"
)

type MyTime time.Time
const MyTimeFormat = "2021-09-25 22:09:05"
const MyLocation = "Asia/Seoul"

var _ json.Unmarshaler = &MyTime{}

func (mt *MyTime) UnmarshalJSON(bs []byte) error {
	var s string

	err := json.Unmarshal(bs, &s)
	if err != nil {
		return err
	}

	location, err := time.LoadLocation(MyLocation)
	if err != nil{
		return err
	}

	t, err := time.ParseInLocation(MyTimeFormat, s, location)
	if err != nil {
		return err
	}

	*mt = MyTime(t)

	return nil
}

golang에서 RFC3339로 알아서 변환하는 time 자료형이 아닌 MyTime이라는 자료형을 따로 정의해줬습니다.

time.Time의 구조를 가져와서 정의해주었기 때문에 거의 비슷하게 사용할 수 있습니다.

 

여기에서 저희가 문제를 겪었던 UnmarshalJSON을 재정의하여 사용하겠습니다.

UnmarshalJSON을 수행하는 과정에서 골치를 아프게 만든 RFC3339로 변환하는 과정을 대신하여 저희가 선호하는 MyTimeFormat을 통하여 parsing을 수행하도록 t, err := time.ParseInLocation(MyTimeFormat, s, location)을 통하여 수행해주었습니다.

 

그리고 이 MyTime을 사용하는 structure를 다음과 같이 사용해 줄 수 있습니다.

type Request struct {
	ID		int64			`json:"id" binding:"required"`
	Title		string			`json:"title" binding:"required"`
	Date		MyTime			`json:"date" binding:"required"`
}

이런 식으로 MyTime을 넣고 bind를 수행 시, MyTimeFormat의 형식으로 값을 받을 수 있습니다.

 

그런데 bind를 하기 위하여 사용하였지만, 우리의 비즈니스 로직에서 원하는 Type은 time.Time 일 수 있습니다.

이 경우 아래와 같이 간단하게 캐스팅을 해주면 사용할 수 있습니다. 우리는 위에서 time.Time의 구조를 기반으로 사용자 정의 타입을 만들었기 때문입니다.

date := time.Time(mytime)

굉장히 비슷하면서도 묘하게 다른 고랭의 세계입니다.

 

부족한 점이나 틀린 점이 있다면 편하게 알려주시면 감사하겠습니다!

 

오늘도 읽어주셔서 감사합니다.

좋은 하루 보내셔요 :)

댓글