서버 개발을 하다 보면 validation은 필수다. request로 꼭 사전 정의된 값만 전송되는 것은 아니기에, 적절한 validation을 통해 bad request의 경우 에러를 반환할 수 있어야 한다.
go-playground/validator를 이용하면 tag를 이용해 쉽게 validation을 할 수 있다. custom tag도 지원하므로 도메인 니즈에 맞는 tag를 추가하는 것도 쉽다.
go-playground/validator에서 지원하는 태그들의 예시를 보면, 상당히 폭넓게 지원됨을 알 수 있다.
- isbn
- cidr
- ip
- mac
- url
- numeric
- number
- boolean
- base64
- hsl
- jwt
- rgb
- uuid
이 밖에도 많은 태그들이 지원된다.
비교문도 지원되는데
- eq: equlas
- gt: 큼
- lte: 같거나 작음
상당히 편리하다.
사용방법
태그 기반으로 간단히 사용할 수 있다.
type MyStruct struct {
Email string `validate:"required,email"`
}
err := validate.Struct(MyStruct)
태그로 넘기면 validate 메서드를 이용해 validation을 할 수 있다.
Custom Validation 추가
도메인 로직 상 기본 제공되는 validator 말고 다른 validator가 필요할 수도 있다. 이런 경우, custom tag를 이용하면 된다.
예를들어 숫자or문자:숫자
형태로 된 redis key가 있다고 하자.
아래 validator는 {숫자 또는 알파벳대소문자}:{숫자} 형태로 된 redis key를 validation한다.
// CustomValidator Custom validator tag 추가 함수
func CustomValidator(v *validator.Validate) {
v.RegisterValidation("redis_key", func(fl validator.FieldLevel) bool {
serviceID := fl.Field().String()
matched, err := regexp.MatchString("^[0-9a-zA-Z]:{1}[0-9]+$", serviceID)
// 정규식이 잘못된 경우,
if err != nil {
// 도메인에 맞는 적절한 error handling
return false
}
// serviceID가 정규식에 match되면 true, 아니면 false를 반환
return matched
})
}
사용시에는 아래처럼 간단히 사용하면 된다.
type RedisUser struct {
Key string `validate:"redis_key"`
}
echo와 사용
echo는 고성능의 가벼운 웹 프레임워크로 go 커뮤니티에서 꽤 일반적으로 사용된다.
echo에서 validator를 사용하는 방법을 정리해봤다.
사실 공식 문서 - Request에서 충분히 설명하고 있다.
import(
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
// RequestValidator request validator Struct
type RequestValidator struct {
validator *validator.Validate
}
// NewPrequestValidator 생성자
func NewRequestValidator(validator *validator.Validate) *RequestValidator {
return &RequestValidator{validator: validator}
}
// Validate validate 메서드
func (rv *RequestValidator) Validate(i interface{}) error {
if err := rv.validator.Struct(i); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return nil
}
// main.go
func main() {
e := echo.New()
e.Validator = &RequestValidator{validator: validator.New()}
e.Logger.Fatal(e.Start(":8080"))
}
uber-go/fx를 이용한다면
DI를 활용하면 유지보수가 용이해지고 재사용성이나 확장이 쉬워지며 테스트 또한 편리해진다. go에서는 google의 wire와 facebook의 inject, uber의 fx가 삼대장이라 할 수 있는데, uber의 fx의 경우 라이프사이클 등 다양한 기능을 제공하며 매우 친절한 문서 도 제공하고 있어 추천한다.
만약 uber-go/fx를 이용해 DI(Dependency Injection)을 하고 있다면 아래처럼 사용해도 좋을 것 같다.
validator.go
package config
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"go.uber.org/fx"
)
// RequestValidator request validator Struct
type RequestValidator struct {
validator *validator.Validate
}
// NewPrequestValidator 생성자
func NewRequestValidator(validator *validator.Validate) *RequestValidator {
return &RequestValidator{validator: validator}
}
// Validate validate 메서드
func (rv *RequestValidator) Validate(i interface{}) error {
if err := rv.validator.Struct(i); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return nil
}
// ValidatorModule validator 모듈
var ValidatorModule = fx.Module(
"config/validator",
fx.Provide(NewRequestValidator),
fx.Provide(validator.New),
)
main.go
func CreateHTTPServer(lc fx.Lifecycle, e *echo.Echo, validator *RequestValidator) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
// hooks는 blocking으로 동작하므로 separate goroutine으로 실행 필요
// https://github.com/uber-go/fx/issues/627#issuecomment-399235227
go func() {
e.Validator = validator
e.Logger.Fatal(e.Start(":8080"))
}()
return nil
},
OnStop: func(ctx context.Context) error {
return e.Shutdown(ctx)
},
})
}
func main() {
fx.New(echo.New, ValidatorModule, fx.Invoke(CreateHTTPServer)).Run()
}
Wrap-up
자세한 문서 를 참고하면 더 다양한 특징을 확인할 수 있다.
예시들도 제공된다.