Golang에서 Structure Validation하기

외부에서 Data Object를 받아오는 경우, 특히 신뢰할 수 없는 출처로부터 받아오는 경우에는 해당 Object에 대한 Validation작업이 필수적으로 요구된다. RESTful API를 구현한 Backend Server가 대표적인 예인데, 클라이언트에서 넘겨준 Object는 무조건 신뢰할 수 없는 정보로 간주하고 Validation을 해야 안전한 서버를 구현할 수 있다. 이를 위해, Golang에서도 struct 에 담긴 Data를 검증하는 다양한 방법이 존재한다.


가장 Naive 하게 구현할 수 있는 방법은 해당 struct에 IsValid() 함수를 구현하는 방법이다. 예를 들어, 아래 형태의 struct를 사용한다고 가정 해 보자.

    type Person struct {
        Name string
        Age int
    }

이 경우, 아래와 같은 Validation Function을 만들어서 검증할 수 있다.

    func (p *Person) IsValid() error {
        if (len(p.name) <= 0 || len(p.name) > 30) {
            return errors.New("이름 길이가 너무 길거나 너무 짧습니다.")
        }

        if (age < 0 || age > 150) {
            return errors.New("나이가 너무 많거나 적습니다.")
        }

        return nil
    }

작은 규모의 프로젝트일 때는 이렇게 하나하나 검증을 해줄 수 있지만, 프로젝트가 커지고 struct가 복잡해질 수록 이 방법은 감당하기 어려워 질 것이다.


이보다 조금 더 나은 Approach를 제공하는것이 go-ozzo/ozzo-validation 라이브러리다. 이 라이브러리에서는 위의 방법을 그들이 제공하는 기본함수들을 통해서 조금 더 쉽게 Validation을 할 수 있도록 돕는다.

아래는 ozzo-validation의 Readme 에 적혀있는 예제 코드이다.

    package main

import (
	"fmt"
	"regexp"

	"github.com/go-ozzo/ozzo-validation"
	"github.com/go-ozzo/ozzo-validation/is"
)

type Address struct {
	Street string
	City   string
	State  string
	Zip    string
}

func (a Address) Validate() error {
	return validation.ValidateStruct(&a,
		// Street cannot be empty, and the length must between 5 and 50
		validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
		// City cannot be empty, and the length must between 5 and 50
		validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
		// State cannot be empty, and must be a string consisting of two letters in upper case
		validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
		// State cannot be empty, and must be a string consisting of five digits
		validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
	)
}

func main() {
	a := Address{
		Street: "123",
		City:   "Unknown",
		State:  "Virginia",
		Zip:    "12345",
	}

	err := a.Validate()
	fmt.Println(err)
	// Output:
	// Street: the length must be between 5 and 50; State: must be in a valid format.
}

이와 같은 라이브러리를 사용하여 검증하는 것은 첫번째 IsValid() 함수를 사용하는 것과 비슷하나, Validation 관리가 용이하며 기본적인 기능은 바퀴를 다시 만들지 않아도 된다는 점 등의 강점을 가진다.


또한 Golang의 struct tag를 활용한 방식도 널리 쓰인다. struct tag란 struct의 field마다 일종의 annotation을 달 수 있는 방식인데, 이 struct tag는 Reflection을 통해 Runtime에 tag정보를 불러올 수 있다는 장점이 있다. 이 방식은 struct를 json Marshal/Unmarshal 할 때 json에서의 필드명 등을 정의할 때 쉽게 볼 수 있다.

아래는 struct tag방식을 활용하여 Validation을 도와주는 go-playground/validator.v9 패키지의 struct 예시이다.

// User contains user information
type User struct {
	FirstName      string     `json:"fname"`
	LastName       string     `json:"lname"`
	Age            uint8      `validate:"gte=0,lte=130"`
	Email          string     `validate:"required,email"`
	FavouriteColor string     `validate:"hexcolor|rgb|rgba"`
	Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}

이렇게 필드 레벨로 Validation 해야하는 정보를 적어놓은 후, 패키지의 Validator로 Validation을 진행할 수 있다.

아래는 같은 라이브러리의 예제 코드 중 벨리데이션 하는 부분이다.

    func main() {

	validate = validator.New()

	// register validation for 'User'
	// NOTE: only have to register a non-pointer type for 'User', validator
	// interanlly dereferences during it's type checks.
	validate.RegisterStructValidation(UserStructLevelValidation, User{})

	user := &User{
		FirstName:      "",
		LastName:       "",
		Age:            45,
		Email:          "Badger.Smith@gmail.com",
		FavouriteColor: "#000",
		Addresses:      []*Address{address},
	}

	// returns InvalidValidationError for bad validation input, nil or ValidationErrors ( []FieldError )
	err := validate.Struct(user)
	if err != nil {
        ...

struct tag를 활용한 Validation의 경우 java의 Annotation을 사용하듯 손쉽게 Validation을 진행할 수 있다는 장점이 있다. 이와 비슷한 방식의 Validation 라이브러리로는 asaskevich/govalidator, go-validator/validator 등이 있다.

하지만 이 방법은 꽤 Magical하다. Magical이란 프로그래밍 플로우를 따라가면서 보기에 명시적이지 않은 부분이 있음을 의미한다. 이 방식의 경우 struct tag에 적은 내용이 어떻게 Validate 되는지가 라이브러리 레벨로 묶여 있어 해당사항을 알기 어렵다. 차라리 go generate등을 통해서 struct를 Validation해줄 함수를 만드는 라이브러리가 있었다면 더 Go스럽지않았을까

struct를 Validation하는 것을 소홀히 하다가는 잘못된 input으로 인해 큰 피해를 입을 수 있음을 명심하고 위의 방법들로 Validation을 진행하는 것이 좋을 것 같다.