Gin 기초 #3 요청 바인딩과 검증
지난 글에서 라우팅을 다뤘습니다. 이번 글은 들어오는 요청 데이터를 Go 구조체로 받는 바인딩과, 그 값이 올바른지 확인하는 검증을 정리합니다. POST/PUT 요청을 제대로 처리하려면 반드시 거쳐야 하는 단계입니다.
- #1 시작과 첫 서버
- #2 라우팅과 핸들러
- #3 요청 바인딩과 검증 ← 이번 글
- #4 응답 처리 — JSON, 상태 코드, 에러
- #5 미들웨어
- #6 데이터베이스 연동 (GORM)
- #7 프로젝트 구조와 미니 REST API
바인딩이란? #
클라이언트가 보낸 JSON 본문이나 폼 데이터를, 미리 정의한 Go 구조체에 채워 넣는 작업을 바인딩이라고 합니다. 필드를 하나씩 꺼내 변환하는 대신, 구조체 하나로 한 번에 받습니다.
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}json:"name" 태그는 JSON의 어떤 키를 어떤 필드에 매핑할지 지정합니다. JSON 키가 name이면 Name 필드에 들어갑니다.
JSON 본문 바인딩 — ShouldBindJSON #
c.ShouldBindJSON이 요청 본문을 읽어 구조체에 채웁니다.
func createUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"received": req})
}curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"커티스","email":"a@b.com","age":30}'
# {"received":{"name":"커티스","email":"a@b.com","age":30}}구조체의 포인터를 넘긴 점에 주목하세요. 바인딩은 구조체의 값을 채워야 하므로 &req로 주소를 전달합니다. 본문 형식이 잘못됐거나 타입이 맞지 않으면 에러가 돌아오고, 위 코드는 400으로 응답한 뒤 return으로 처리를 끝냅니다.
검증 — binding 태그 #
바인딩만으로는 “값이 들어왔는지”, “형식이 맞는지"까지 확인하지 못합니다. Gin은 구조체 태그로 검증 규칙을 선언하는 기능을 내장하고 있습니다. binding 태그에 규칙을 적습니다.
type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=130"`
}required— 값이 반드시 있어야 함email— 이메일 형식이어야 함gte=0,lte=130— 0 이상 130 이하
이제 규칙에 어긋나는 요청은 ShouldBindJSON 단계에서 에러가 됩니다.
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"","email":"not-an-email","age":200}'
# 400, 검증 에러 메시지자주 쓰는 규칙을 몇 가지 더 정리합니다.
| 태그 | 의미 |
|---|---|
required | 값이 비어 있으면 안 됨 |
email | 이메일 형식 |
min=3 / max=20 | 문자열 길이 또는 숫자 크기 범위 |
gte=0 / lte=100 | 이상 / 이하 |
oneof=admin user | 나열된 값 중 하나 |
len=10 | 정확한 길이 |
검증 엔진은 go-playground/validator이며, 더 많은 규칙은 해당 문서에서 확인할 수 있습니다.
ShouldBind vs Bind #
Gin의 바인딩 함수는 두 계열로 나뉩니다.
ShouldBindJSON계열 — 에러를 돌려주기만 합니다. 응답은 직접 작성합니다.BindJSON계열 — 에러가 나면 자동으로 400과 함께 요청을 중단(abort)합니다.
// 직접 에러 처리 — 권장
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 자동 400 — 응답 형식을 통제하기 어려움
if err := c.BindJSON(&req); err != nil {
return // Gin이 이미 400 응답을 보냄
}API의 에러 응답 형식을 일관되게 통제하려면 ShouldBind 계열을 쓰는 편이 낫습니다. 이 시리즈는 ShouldBind를 기본으로 씁니다.
쿼리와 경로도 바인딩 #
본문뿐 아니라 쿼리 스트링과 경로 파라미터도 구조체로 받을 수 있습니다. 파라미터가 많을 때 핸들러가 깔끔해집니다.
type ListQuery struct {
Page int `form:"page,default=1" binding:"gte=1"`
Size int `form:"size,default=20" binding:"gte=1,lte=100"`
Sort string `form:"sort"`
}
func listUsers(c *gin.Context) {
var q ListQuery
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"query": q})
}쿼리는 form 태그로 키를 지정하고, default=로 기본값을 줄 수 있습니다. 경로 파라미터는 uri 태그와 c.ShouldBindUri를 씁니다.
type UserURI struct {
ID int `uri:"id" binding:"required"`
}
func getUser(c *gin.Context) {
var u UserURI
if err := c.ShouldBindUri(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"userID": u.ID})
}폼 데이터 바인딩 #
application/x-www-form-urlencoded나 multipart/form-data 폼도 같은 방식으로 받습니다. form 태그를 쓰고 c.ShouldBind를 호출하면 Gin이 Content-Type을 보고 알맞게 처리합니다.
type LoginForm struct {
Username string `form:"username" binding:"required"`
Password string `form:"password" binding:"required"`
}
func login(c *gin.Context) {
var form LoginForm
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": form.Username})
}검증 에러를 다듬어 응답하기 #
err.Error()를 그대로 내보내면 메시지가 장황하고 사용자가 읽기 어렵습니다. 어떤 필드가 왜 실패했는지 정리해서 돌려주면 클라이언트가 처리하기 좋습니다.
import "github.com/go-playground/validator/v10"
if err := c.ShouldBindJSON(&req); err != nil {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := make(map[string]string, len(ve))
for _, fe := range ve {
out[fe.Field()] = fe.Tag() // 예: "Email": "email"
}
c.JSON(http.StatusBadRequest, gin.H{"errors": out})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "잘못된 요청"})
return
}에러 메시지를 더 정교하게 다듬는 작업은 중급 시리즈의 에러 처리 편에서 이어가겠습니다.
마무리 #
이번 글에서 정리한 내용입니다.
- 바인딩은 요청 데이터를 Go 구조체에 채우는 작업,
json/form/uri태그로 매핑 - 본문은
c.ShouldBindJSON, 쿼리는c.ShouldBindQuery, 경로는c.ShouldBindUri - 구조체 포인터(
&req)를 넘겨야 값이 채워짐 binding태그로 검증 규칙 선언,required/email/gte/oneof등ShouldBind계열은 에러만 반환,Bind계열은 자동 400, 통제력은ShouldBind가 큼- 검증 에러는
validator.ValidationErrors로 필드별로 정리해 응답 가능
다음 글(#4 응답 처리)에서는 JSON 외의 다양한 응답 형식과 상태 코드, 그리고 일관된 에러 응답을 만드는 법을 정리하겠습니다.