웹 애플리케이션을 만들다보면 Request 필드 검증이 필수이다.
처음에는 별 생각없이 함수를 생성해서 진행했는데,
검증할 struct가 늘어날수록 검증 방식이 복잡해질 수록 코드가 굉장히 더러워졌다!!!!
// Bad Case
type Request struct {
Age int
Name string
}
func IsValidRequest(r Request) bool {
if r.Age < 0 {
return false
}
if len(r.Name) == 0 {
return false
}
return true
}
위의 방식보다 Go Validator 라이브러리를 이용하는 게 더 좋은 방식이라고 한다~~
Go Validator 라이브러리는 크게 2가지가 있는데
아래 라이브러리가 훨씬 활발해서 아묻따 아래걸로 공부한다.
코드는 여기에서 볼 수 있습니다.
v := validator.New()
err := v.Struct(validPerson)
type Request struct {
Email string `validate:"required,email"`
Age int `validate:"required,min=0"`
}
필드마다 원하는 검증이 있을 것이다.
이런 의미를 tag에 담아두면 validator는 그 tag를 보고 value를 검사한다.
기본적으로 다양한 tag를 제공하고 자신만의 Custom Tag도 생성할 수 있다.
나만의 Tag를 만들고 싶다면 validator에 등록을 시켜줘야한다.
type Person struct {
Name string `validate:"required"`
Age int `validate:"required"`
Contact string `validate:"contact"` // custom tag: only email or phone
}
어떤 사람이 이메일이나 전화번호로만 연락을 받아야 해서 두가지 값만 받고 싶다고 해보자.
v := validator.New()
v.RegisterValidation("contact", func(fl validator.FieldLevel) bool {
// fl.Field().Interface().(string)
return fl.Field().String() == "email" || fl.Field().String() == "phone"
})
fl.Field().String()
fl.Field().Interface().(MyType)
t.Run("(정상) 연락 수단은 이메일이나 전화번호이다.", func(t *testing.T) {
validPerson := Person{"Minion", 26, "email"}
err := v.Struct(validPerson)
assert.Nil(t, err)
})
t.Run("(예외) 연락 수단이 카카오톡이다.", func(t *testing.T) {
validPerson := Person{"Minion", 26, "kakaotalk"}
err := v.Struct(validPerson)
assert.NotNil(t, err)
// Key: 'Person.Contact' Error:Field validation for 'Contact' failed on the 'contact' tag
})
contact에 email이 들어간 경우 에러가 발생하지 않지만,
의도하지 않은 kakaotalk이 들어간 경우 에러가 발생하는 것을 볼 수 있다.
v.Struct
했을 때 return되는 값의 타입은 일반적으로 사용되는 built-in error이고,
실제로 전달되는 값은 valiator에서 만든 error이다.
built-in error를 return한다.
디버깅해보면 validator에서 만든 에러를 주고 있다.
err := v.Struct(p)
validationErrors := err.(validator.ValidationErrors) // 타입 변환하여 사용
Struct에서 받은 에러를 위와 같이 변환해서 사용한다.
type Request struct {
Email string `validate:"required"`
Age int `validate:"required"`
}
r := Request{}
위와 같은 상황이 있을 때 2개의 에러가 발생한다는 의미~
Valiator의 에러는 내부적으로 배열로 만들어져있어서,
검사했을 때 1개이상의 모든 에러를 전달한다.
실제로 사용하다보면 꼭 필요한 게 Custom Error이다.
사용자한테 Key: 'Person.Contact' Error:Field validation for 'Contact' failed on the 'contact' tag
이런 row한 에러 메시지를 보내줄수는 없는 노릇!!
Custom 에러를 만들려면 우선 translator라는 놈을 가져와야한다.
en := en.New()
uni = ut.New(en, en)
trans, _ := uni.GetTranslator("en") // Accept-Lang 한국어 가능
en_translations.RegisterDefaultTranslations(v, trans)
RegisterCustomError(trans)
func RegisterCustomError(trans ut.Translator) {
v.RegisterTranslation("required", trans, func(ut ut.Translator) error {
return ut.Add("required", "[My Custom Error Msg] {0} must have a value!", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
}
위와 같은 방식으로 custom 에러를 등록한다.
invalidStudent := Student{}
err := v.Struct(invalidStudent)
/*
translate 안했을 때
Key: 'Student.Name' Error:Field validation for 'Name' failed on the 'required' tag
Key: 'Student.Age' Error:Field validation for 'Age' failed on the 'required' tag
*/
fmt.Println(err.Error())
/*
translate 했을 때
[My Custom Error Msg] Name must have a value!
[My Custom Error Msg] Age must have a value!
*/
if err != nil {
errs := err.(validator.ValidationErrors)
for _, e := range errs {
fmt.Println(e.Translate(trans))
}
}
Custom Error 등록 함수를 호출한 후에, v.Struct
로 검사하면 에러가 나오는데
translate하지 않을 경우 기본 에러가 나오기 때문에 주의하자
valiator를 사용하다보면 tag만으로는 검증에 한계가 있다.
struct 내의 다른 필드를 확인해야 검증할 수 있을 때가 있다.
예를 들어
type Person struct {
ContactType string `validate:"oneof=email phone"`
ContactValue string `validate:"required"`
}
이메일 타입으로 지정하면 value가 이메일 형식이고,
번호 타입으로 지정하면 value가 전화번호 형식이어야 하는 경우가 있다.
(oneof
: email or phone이어야 한다는 의미)
v = validator.New()
v.RegisterStructValidation(StructLevelValidation, Person{})
요런 식으로 struct마다 함수를 등록할 수 있다.
func StructLevelValidation(sl validator.StructLevel) {
user := sl.Current().Interface().(Person)
if user.ContactType == "email" &&
(v.Var(user.ContactType, "email") != nil) {
sl.ReportError(user.ContactValue, "content_value", "content_type", "email", "")
// tag -> custom message로 연동된다.
}
if user.ContactType == "phone" &&
(v.Var(user.ContactType, "e164") != nil) {
sl.ReportError(user.ContactValue, "content_value", "content_type", "e164", "")
}
}
sl.ReportError
v.Var({{value}}, {{tag}}
t.Run("[invalid] type: email, value: phone", func(t *testing.T) {
invalid := Person{ContactType: "email", ContactValue: "0210102444"}
err := v.Struct(invalid)
assert.NotNil(t, err)
// Key: 'Person.content_value' Error:Field validation for 'content_value' failed on the 'email' tag
})
이메일 타입 + 전화번호를 작성하면 의도한대로 에러가 발생한다.