개발/Go

[Gin] GORM Custom Type 활용법 - JSON 필드, Enum, Time 처리까지 한 번에 정리!

devhooney 2025. 6. 25. 08:32
728x90

Go + GORM으로 백엔드 개발을 하다 보면 다음과 같은 고민, 한 번쯤 해봤을 것이다.

 

- Struct 안에 JSON 배열을 저장하고 싶은데 어떻게 하지?
- Enum 타입처럼 제한된 값만 저장하고 싶은데?
- time.Time 포맷이 DB랑 안 맞아서 깨져서 나온다?

이럴 때 유용하게 쓰이는 게 GORM의 Custom Type 기능이다. 이 글에서는 JSON 필드, Enum 처리, 커스텀 Time 포맷을 예제로 하나씩 정리해보려 한다.

 

 


 

 

 

728x90

 

 

 

1. JSON 필드 매핑하기 — Slice나 Map을 JSON으로 저장하기
Go에서는 []string, map[string]string 같은 필드를 DB의 JSON 컬럼에 저장하려면 Scan()과 Value() 메서드를 직접 구현해줘야 한다.

✅ 예제: []string을 JSON으로 저장하는 커스텀 타입

type StringArray []string

func (a *StringArray) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return fmt.Errorf("failed to scan JSON")
	}
	return json.Unmarshal(bytes, a)
}

func (a StringArray) Value() (driver.Value, error) {
	return json.Marshal(a)
}

 

✅ 모델에 적용

type User struct {
	ID      uint        `gorm:"primaryKey"`
	Name    string
	Hobbies StringArray `gorm:"type:json"`
}

 

- PostgreSQL이라면 type:jsonb
- MySQL이라면 type:json 을 지정해주면 된다.

 

 

 


 

2. Enum 타입 처리 — 제한된 값만 허용하기
Go에는 Enum이 없기 때문에 보통 string 타입을 쓰게 되는데, Custom Type으로 구현하면 코드의 의도를 명확히 할 수 있다.

 

 

✅ 예제: UserRole Enum 정의

type UserRole string

const (
	Admin  UserRole = "ADMIN"
	Member UserRole = "MEMBER"
)

func (r *UserRole) Scan(value interface{}) error {
	*r = UserRole(value.(string))
	return nil
}

func (r UserRole) Value() (driver.Value, error) {
	return string(r), nil
}

 

✅ 모델에 적용

type User struct {
	ID   uint     `gorm:"primaryKey"`
	Name string
	Role UserRole `gorm:"type:varchar(20)"`
}

 

참고: DB에서 ENUM 타입으로 정의하면 더 안전하게 관리할 수 있다.

 

 


 

3. Custom Time 포맷 처리 — time.Time 포맷 맞추기
기본 time.Time은 RFC3339 형식인데, DB나 프론트에서 yyyy-MM-dd HH:mm:ss 포맷을 요구할 때가 많다. 이럴 때도 Custom Type으로 포맷을 컨트롤할 수 있다.

✅ 예제: CustomTime 타입 정의

type CustomTime struct {
	time.Time
}

const layout = "2006-01-02 15:04:05"

func (ct *CustomTime) Scan(value interface{}) error {
	switch v := value.(type) {
	case time.Time:
		ct.Time = v
	case []byte:
		t, err := time.Parse(layout, string(v))
		if err != nil {
			return err
		}
		ct.Time = t
	}
	return nil
}

func (ct CustomTime) Value() (driver.Value, error) {
	return ct.Format(layout), nil
}

func (ct CustomTime) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("\"%s\"", ct.Format(layout))), nil
}

func (ct *CustomTime) UnmarshalJSON(b []byte) error {
	str := strings.Trim(string(b), "\"")
	t, err := time.Parse(layout, str)
	if err != nil {
		return err
	}
	ct.Time = t
	return nil
}

 

 

✅ 모델에 적용

type Event struct {
	ID        uint       `gorm:"primaryKey"`
	Title     string
	StartTime CustomTime `gorm:"column:start_time"`
}

 

 

 

 

728x90