Golang 필드 검사 validator 라이브러리 알아보기👀

타미·2021년 7월 27일
1

Hello Golang

목록 보기
6/7
post-thumbnail
post-custom-banner

어떻게 validate할까?

웹 애플리케이션을 만들다보면 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가지가 있는데

go-validator/valiator

go-playground/validator

아래 라이브러리가 훨씬 활발해서 아묻따 아래걸로 공부한다.


코드는 여기에서 볼 수 있습니다.

기본적인 사용법

validator 객체를 사용하여 검증한다.

v := validator.New()
err := v.Struct(validPerson)
  • validaotr 객체를 생성하여 사용한다.
    • struct 정보가 캐싱되기 때문에 검증할 때마다 객체를 생성하지말고 하나의 객체를 사용하는 게 좋다.
  • v.Struct를 하면 그때 검증된다.
  • err == nil : 😊 valid
  • err != nil : 😩 invalid

tag로 무엇을 검증할지 적어준다.

type Request struct {
	Email string `validate:"required,email"`
	Age  int    `validate:"required,min=0"`
}

필드마다 원하는 검증이 있을 것이다.

  • required: 꼭 값이 들어가야 함
  • email: email 타입이어야 함
  • min: 최소 0이상이어야 함

이런 의미를 tag에 담아두면 validator는 그 tag를 보고 value를 검사한다.
기본적으로 다양한 tag를 제공하고 자신만의 Custom Tag도 생성할 수 있다.


Custom Tag 만들기

valiator에 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"
})
  • validator 생성
  • custom tag와 그에 대한 검증 로직 추가
  • validator.FeildLevel에 있는 값을 통해 변수값을 확인할 수 있다.
    • 변수가 string 타입일 경우, 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이 들어간 경우 에러가 발생하는 것을 볼 수 있다.


Error

Validator의 Error

Validator는 Valiator에서 만든 에러를 사용한다.

v.Struct했을 때 return되는 값의 타입은 일반적으로 사용되는 built-in error이고,
실제로 전달되는 값은 valiator에서 만든 error이다.


built-in error를 return한다.


디버깅해보면 validator에서 만든 에러를 주고 있다.

err := v.Struct(p)
validationErrors := err.(validator.ValidationErrors) // 타입 변환하여 사용

Struct에서 받은 에러를 위와 같이 변환해서 사용한다.

Valiate할 때 모든 tag를 검사한다. (잘못된 값을 발견해도 끝까지 검사한다.)

type Request struct {
	Email string `validate:"required"`
	Age  int    `validate:"required"`
}
r := Request{}

위와 같은 상황이 있을 때 2개의 에러가 발생한다는 의미~


Valiator의 에러는 내부적으로 배열로 만들어져있어서,
검사했을 때 1개이상의 모든 에러를 전달한다.

Custom Error

실제로 사용하다보면 꼭 필요한 게 Custom Error이다.
사용자한테 Key: 'Person.Contact' Error:Field validation for 'Contact' failed on the 'contact' tag 이런 row한 에러 메시지를 보내줄수는 없는 노릇!!

translator 생성

Custom 에러를 만들려면 우선 translator라는 놈을 가져와야한다.

en := en.New()
uni = ut.New(en, en)

trans, _ := uni.GetTranslator("en") // Accept-Lang 한국어 가능

en_translations.RegisterDefaultTranslations(v, trans)
  • 커스텀 메시지를 작성하면 여러 언어가 사용되니까, 그에 필요한 언어 translator를 만드는 것 같다. 한국어버전은 없고 en 영어를 쓰면 한국어도 잘 변환된다.
  • translator와 valiator를 연결해준다.

Custom Error 등록

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 에러를 등록한다.

translate해야 커스텀 에러 메시지를 볼 수 있다.

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하지 않을 경우 기본 에러가 나오기 때문에 주의하자


Struct Level에서의 valiate

valiator를 사용하다보면 tag만으로는 검증에 한계가 있다.
struct 내의 다른 필드를 확인해야 검증할 수 있을 때가 있다.

예를 들어

type Person struct {
	ContactType  string `validate:"oneof=email phone"`
	ContactValue string `validate:"required"`
}

이메일 타입으로 지정하면 value가 이메일 형식이고,
번호 타입으로 지정하면 value가 전화번호 형식이어야 하는 경우가 있다.
(oneof: email or phone이어야 한다는 의미)

struct에 valiate 함수 등록

v = validator.New()
v.RegisterStructValidation(StructLevelValidation, Person{})

요런 식으로 struct마다 함수를 등록할 수 있다.

validate 함수

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에서 원하는 타입으로 변환
  • 검증 로직 생성
  • 에러 케이스 작성
    • sl.ReportError
  • 참고
    • v.Var({{value}}, {{tag}}
      • v.Struct가 Struct를 검사했다면, v.Var는 변수 하나만 검사해준다.
    • e164 tag는 기본 제공되는 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
})

이메일 타입 + 전화번호를 작성하면 의도한대로 에러가 발생한다.

profile
IT's 호기심 천국
post-custom-banner

0개의 댓글