go/ fiber로 gin의 shouldBindJSON 비슷하게 만들어보기.

박세훈·2022년 11월 25일
0
post-thumbnail

들어가며

Fiber vs Gin

Fiber는 속도가 굉장히 빠르다고 하여, 끌려서 사용하는 중이다.
gin과 비교했을 때 불편한 점이 있었다.

Gin의 ShouldBindJson, ShouldBindQuery ShouldBind 시리즈는 참 편리한데, Fiber에서는 비슷한 게 없다.

ShouldBind시리즈는
파싱과 동시에 Validation까지 할 수 있는 인터페이스다.

Fiber에서
ShouldBind를 비슷하게 만들어보자.

목표 : Parse + Validation 한 번에 할 수 있는 인터페이스 만들기.

Gin, ShouldBind

ShouldBind 의 동작원리

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를 거친다.

header , form ,xml.. 등 다양한 binding시리즈가 존재한다.

만들어보자

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의 코드는 정말 깔끔하게 잘 만들어져있다.
보고 뜯고 맛보고 즐길 필요가 있어보인다.

profile
changing

0개의 댓글