[go/gin] 요청 Validation에 대해 알아보자.

박세훈·2022년 9월 6일
1

1. validator가 필요한 이유

클라이언트의 정보를 저장하는 API에서

요청 시,

이메일, 비밀번호,휴대폰 번호 등 다양한 데이터를 받는다.

요청받은 데이터를 데이터베이스에 저장되며,
사용자의 요청이 있을 때 다시 데이터를 반환해준다.

예를들어 사용자의 휴대폰번호를 데이터를 반환해줄 때, 

누구는 010-xxxx-xxxx 형태고,

누구는 010xxxxxxxx

누구는 +8210xxxxxxxx 형태면

미적으로 보기 좋지 않다.

이는 프로그램의 품질과 관계되며,

품질저하는 사용자의 신뢰를 낮추는 주요한 요인이다.

또다른 예로,

사용자의 보안을 위해 비밀번호 정책을 세운다고 해보자.

"사용자의 비밀번호에는 특수문자를 꼭 포함시키세요"하는 정책이 있을 때 (SQL Inject의 위험이 되는 특수문자는 제거해줘야 한다.)

데이터베이스에 비밀번호를 저장하기 전에, 특수문자를 포함했는지 검사를 해야 한다.

"클라이언트에서 검사를 하면 되지 않나요?" 라는 질문이 있을 수 있다.

서버 URL로 직접 호출하는 경우는 막을 수 없기 때문에, 완벽한 검사가 될 수는 없다.

그러므로 데이터베이스에 저장하기 전에 각 정책에 맞게 데이터를 규격화할 필요가 있다.

2. Golang (gin)에서 Validator 사용하기

다양한 라이브러리가 존재하지만,
gin framework에서 사용하는
go-playground/validator의 사용법을 알아보자. 

https://github.com/go-playground/validator

2.1. gin에서 사용법

~참고 : binding하는 방법~

gin 공식 문서에 나와있는 validation 방법이다.

구조체 안에 binding:"" 따옴표 안에 validation할 것을 적어주면 된다.

뭘 넣을 지는 상황에 따라 선택해서 넣어주면 되고, validator공식문서를 참고하면 되지만,

예시 몇 가지를 들어 이해를 돕고 싶다.

2.1.1. 필수로 입력받아야 하는 데이터

요청 시 없으면 에러를 발생시키는 태그는 required다.

type Login struct {
  User     string `form:"user" json:"user" binding:"required"`
  Password string `form:"password" json:"password" binding:"required"`
}

2.1.2. 최소 글자 길이 최대 글자 길이

min max라는 태그를 사용한다.

최소 최대 둘 다 적용하고 싶다면, 순서관계 없이 ,로 구분하여 작성하면 된다.

type Login struct {
  User     string `form:"user" json:"user" binding:"required,max=8"`
  Password string `form:"password" json:"password" binding:"required,min=8,max=20"`
}

2.1.3. 특정한 문자열만 받기.

oneof라는 태그를 사용하며,

공백으로 구분하여 문자열을 적어주면 된다.

type Login struct {
  Type string `form:"type" json:"type" binding:"required,oneof=user admin superadmin"`
}

이외에도 숫자만, 알파뱃숫자만, 소문자만 받는 등 다양한 경우의 Validation 기능이 존재하니,

자세하게 알기 위해서는 go-playground/validator를 참고하길 바란다.

만약 내가 Validation을 하고 싶은 게 공식문서에 없으면 어떡할까?

Custom Validation을 만들면 된다.

2.2. Validation 커스텀

휴대폰 Validator를 한다고 가정해보자.

핵심은 서버에

v.RegisterValidation을 해줘야 한다.

첫 번째 파라미터는 구조체 binding 안에서 작동할 문자열을 넣어주면 되고,
두 번째 파라미터에는 검사할 코드를 넣어주면 된다.

func main(){ 

    const phoneRegex = `^01([0|1|6|7|8|9])([0-9]{3,4})([0-9]{4})$`

    func ValidateRegex(regex, value string) bool {
        reg := regexp.MustCompile(regex)
        return reg.Match([]byte(value))
    }

    func Phone() validator.Func {
        return func(fl validator.FieldLevel) bool {
            if value, ok := fl.Field().Interface().(string); ok {
                return ValidateRegex(phoneRegex, value)
            }
            return true
        }
    }

    server := gin.Default()

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("customPhone", Phone())
    }


    type User struct {
        Phone string `json:"phone" binding:"required,customPhone"`
    }

    server.GET("/", func(c *gin.Context) {
        var json User
        if err := c.shouldBindJson(&json); err != nil {
            c.JSON(200, gin.H{"error": err.Error()})
            return
        }
         c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

}

원리는 customPhone을 적어놓은 필드에 값이 들어오면,
Phone()함수를 거친다.

필드의 값이 정규식 상황에 따라 true, false를 반환한다.

2.2. 프레임워크에 의존하지 않고 Validation 사용하기

validator 소스는 validate:""안에 넣어주면 된다.

validate를 생성하고, 구조체를 검사해주면 된다.

type User struct {
	phone string `validate:"required,min=5,max=10"`
}

validate := validator.New()
s := User{phone: "01022234213"}
if err := validate.Struct(s); err != nil {
    //에러처리
}

3.  마무리

이상.

profile
changing

0개의 댓글