Fiber는 속도가 굉장히 빠르다고 하여, 끌려서 사용하는 중이다.
gin과 비교했을 때 불편한 점이 있었다.
Gin의 ShouldBindJson, ShouldBindQuery ShouldBind 시리즈는 참 편리한데, Fiber에서는 비슷한 게 없다.
ShouldBind시리즈는
파싱과 동시에 Validation까지 할 수 있는 인터페이스다.
Fiber에서
ShouldBind를 비슷하게 만들어보자.
목표 : Parse + Validation 한 번에 할 수 있는 인터페이스 만들기.
ShouldBindJson은 어떻게 되어 있을까?
func (c *Context) ShouldBindJSON(obj any) error {
return c.ShouldBindWith(obj, binding.JSON)
}
ShouldBindQuery도 마찬가지로 ShoudBindWith을 거친다.
func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query)
}
둘의 차이점은 무엇일까?
ShouldBindWith의 두 번째 매개변수가 다르다.
매개변수는 각각 다른 구조체를 띄고 있다.
var (
JSON = jsonBinding{}
Query = queryBinding{}
...생략
)
그럼 shouldBInd는 어떻게 동작할까?
Binding이라는 추상화된 interface 가 존재한다.
// binding/bind.go
type Binding interface {
Name() string
Bind(*http.Request, any) error
}
두 번째 인자로 넘겨받은 struct에 정의된 Bind 함수를 실행시킨다.
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
JSON일 경우 jsonBinding{}.Bind(c.Request, obj)
일테고
Query일 경우 queryBinding{}.Bind(c.Request, obj)
일 것이다.
// JSON /binding/json.go
type jsonBinding struct{}
func (jsonBinding) Name() string {
return "json"
}
func (jsonBinding) Bind(req *http.Request, obj any) error {
if req == nil || req.Body == nil {
return errors.New("invalid request")
}
return decodeJSON(req.Body, obj)
}
// QUERY // JSON /binding/query.go
type queryBinding struct{}
func (queryBinding) Name() string {
return "query"
}
func (queryBinding) Bind(req *http.Request, obj any) error {
values := req.URL.Query()
if err := mapForm(obj, values); err != nil {
return err
}
return validate(obj)
}
Gin의 인터페이스인
ShouldBindJson의 경우 jsonBinding으로 묶인 Bind를 거친다.
ShoudBindQuery의 경우 queryBinding으로 묶인 Bind를 거친다.
gin과 같은 방식으로 제작할 거다.
추상화를 먼저 해준다.
Validator는 go-playerground의 validator를 따로 추상화 했다.
여기서는 다루지 않고
중점을 Bind하는 것에 맞춰주길 바란다.
type Binding interface {
Bind(c *fiber.Ctx, out interface{}) []*validatorx.ErrorResponse
}
var (
JSON = jsonBinding{}
Query = queryBinding{}
)
// 아래 부분은 개인적으로 프로젝트에서 사용하고 있는 것들에 의존하기 때문에 볼 필요 없음.
var Validator validatorx.Validator = validatorx.NewValidatorx().Init()
func validate(out interface{}) []*validatorx.ErrorResponse {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(out)
}
func newErrorFiberParse(err error) []*validatorx.ErrorResponse {
var error []*validatorx.ErrorResponse
e := err.(*json.UnmarshalTypeError)
message := fmt.Sprintf("%s to %s", e.Value, e.Type.Name())
tagNameIndex := strings.LastIndex(e.Field, ".")
error = append(error, &validatorx.ErrorResponse{
FailedField: e.Field,
Tag: message,
FailedFieldTagName: e.Field[tagNameIndex+1:],
Value: message,
})
return error
}
// JSON bind/json.go
type jsonBinding struct{}
func (jsonBinding) Bind(c *fiber.Ctx, out interface{}) []*validatorx.ErrorResponse {
// 파싱
err := c.BodyParser(out)
if err != nil {
errData := newErrorFiberParse(err)
return errData
}
// 벨리데이션
return validate(out)
}
// Query bind/query.go
type queryBinding struct{}
func (queryBinding) Bind(c *fiber.Ctx, out interface{}) []*validatorx.ErrorResponse {
// 파싱
err := c.QueryParser(out)
if err != nil {
errData := newErrorFiberParse(err)
return errData
}
// 벨리데이션
return validate(out)
}
//utils/binding.go
func BindingJson(c *fiber.Ctx, v validatorx.Validator, out interface{}) []*validatorx.ErrorResponse {
return bindWith(c, v, out, &bind.JSON)
}
func BindingQuery(c *fiber.Ctx, v validatorx.Validator, out interface{}) []*validatorx.ErrorResponse {
return bindWith(c, v, out, &bind.Query)
}
func bindWith(c *fiber.Ctx, v validatorx.Validator, out interface{}, b bind.Binding) []*validatorx.ErrorResponse {
return b.Bind(c, v, out)
}
사용방법
reqBody := new(transport.AcademyCreateRequestBody)
if err := utils.BindingJson(c, reqBody); err != nil {
return c.Status(http.StatusBadRequest).JSON(ex.NewInvalidInputError(err))
}
go/gin의 코드는 정말 깔끔하게 잘 만들어져있다.
보고 뜯고 맛보고 즐길 필요가 있어보인다.