go-playground/validator를 echo 서버에서 이용하기

서버 개발을 하다 보면 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
  • email
  • 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

자세한 문서 를 참고하면 더 다양한 특징을 확인할 수 있다.

예시들도 제공된다.