Golang Gin Gonic - 2. Bind data
간단한 API 서버를 제외하고는 웹서비스의 전형적인 HTTP API server를 실무에서 개발한 적이 없다. 유튜브에서 Golang의 대표적인 web framework인 Gin을 이용한 좋은 강좌를 만나 이를 하나씩 따라하려고 한다.
두 번째는 HTTP Client가 server로 보내는 data를 Gin Gonic에서 어떻게 받을 수 있는지 보겠다.
Playlist: Rest API in Golang using Gin Gonic: https://bit.ly/3hsZKbv
YouTube. How to bind data from request in Golang using Gin Gonic: https://youtu.be/OoNeWiJ1Ebk
구현해본 GitHub repo: https://github.com/nicewook/gin-gonic-study
이번 블로그 포스팅 소스코드: https://github.com/nicewook/gin-gonic-study/tree/main/bind-data-2
Server의 구성
전체 코드는 GitHub을 참고하자.
Gin을 이용해서 네 개의 endpoint를 구현하였다
1) GET /user 로 오면 Query data를 받는다
2) POST /user 로 오면 body의 JSON data를 받는다
3) PUT으로 3개의 URI data를 받는다
4) PUT으로 1개의 URI data와 JSON data를 받는다.
func newServer() *gin.Engine {
r := gin.Default()
r.GET("/user", getUserQueryHandle)
r.POST("/user", postUserJSONHandle)
r.PUT("/user/:id/:name/:email", putUserURIHandle)
r.PUT("/user/:id", putUserURIJSONHandle)
return r
}
func main() {
newServer().Run()
}
각각의 핸들러와 그 테스트 코드
바인딩할 구조체를 만들어보았다. 구조체 태그에 form, uri, json을 모두 넣어두었다.
각각 form은 Query, uri는 URI, json은 JSON을 의미한다
type User struct {
ID int `form:"id" uri:"id" json:"id"`
Name string `form:"name" uri:"name" json:"name"`
Email string `form:"email" uri:"email" json:"email"`
}
getUserQueryHandle
Query로 들어오는 data를 ShouldBindQuery 메서드로 바인딩한 다음, 바인딩한 user 구조체를 그대로 response 해주는 핸들러이다
func getUserQueryHandle(c *gin.Context) {
var user User
if err := c.ShouldBindQuery(&user); err != nil {
log.Println("err: ", err)
c.AbortWithStatus(http.StatusBadRequest)
}
log.Printf("user: %+v", user)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"data": user,
})
}
테스트 코드는 아래와 같다.
GET request의 enpoint인 /user 뒤에 물음표를 달고 key=value 포맷으로 넣어주면 된다.
t.Run("GET Query OK", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/user?id=1&name=hsjeong&email=a@gmail.com", ts.URL))
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code %v, got %v", http.StatusOK, resp.StatusCode)
}
printResponseBody(resp)
})
postUserJSONHandle
POST reqeust의 body속 JSON을 ShouldBindJSON 메서드를 이용해 바인딩 해준다. 나머지 코드는 모두 같다.
func postUserJSONHandle(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
log.Println("err: ", err)
c.AbortWithStatus(http.StatusBadRequest)
}
log.Printf("user: %+v", user)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"data": user,
})
}
테스트 코드는 POST request에 JSON을 담아서 보낸다.
t.Run("POST JSON OK", func(t *testing.T) {
account := User{
ID: 1,
Name: "Hyunseok, Jeong",
Email: "a@gmail.com",
}
b, _ := json.Marshal(account)
buff := bytes.NewBuffer(b)
resp, err := http.Post(fmt.Sprintf("%s/user", ts.URL), "application/json", buff)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code %v, got %v", http.StatusOK, resp.StatusCode)
}
printResponseBody(resp)
})
putUserURIHandle
이번에는 PUT request로 URI가 세 개 왔을때의 처리이다.
func putUserURIHandle(c *gin.Context) {
var user User
if err := c.ShouldBindUri(&user); err != nil {
log.Println("err: ", err)
c.AbortWithStatus(http.StatusBadRequest)
}
log.Printf("user: %+v", user)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"data": user,
})
}
Gin에서 endpoint를 3개 명시하였기에 3개가 아니면 에러가 난다.
t.Run("PUT URI OK", func(t *testing.T) {
client := &http.Client{}
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/user/1/hyunseokJeong/a@gmail.com", ts.URL), nil)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code %v, got %v", http.StatusOK, resp.StatusCode)
}
printResponseBody(resp)
})
putUserURIJSONHandle
가장 재미있다 할 수 있는 URI와 JSON의 조합이다. URI가 하나 있을때에 이 핸들러가 처리한다.
먼저 URI를 바인딩 한 다음에 JSON을 바인딩 한다. 만약 JSON에 ID 필드까지 포함되면 overwrite 될 것이다.
func putUserURIJSONHandle(c *gin.Context) {
var user User
if err := c.ShouldBindUri(&user); err != nil {
log.Println("err: ", err)
c.AbortWithStatus(http.StatusBadRequest)
}
log.Printf("user: %+v", user)
if err := c.ShouldBindJSON(&user); err != nil {
log.Println("err: ", err)
c.AbortWithStatus(http.StatusBadRequest)
}
log.Printf("user: %+v", user)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"data": user,
})
}
테스트 코드는 URI를 통하여 ID를 보내고, Name과 Email은 JSON을 통해 보내고 있다.
t.Run("PUT URI JSON", func(t *testing.T) {
account := User{
Name: "Hyunseok, Jeong",
Email: "a@gmail.com",
}
b, _ := json.Marshal(account)
buff := bytes.NewBuffer(b)
client := &http.Client{}
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/user/1", ts.URL), buff)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code %v, got %v", http.StatusOK, resp.StatusCode)
}
printResponseBody(resp)
})
참고: 테스트 결과 with richgo
테스트 결과가 알아보기 힘들어서 구글링으로 알아낸 패키지인데 한결 보기 쉬운 것 같다.
richgo 패키지: https://github.com/kyoh86/richgo
go get -u github.com/kyoh86/richgo 로 설치한 다음,
~/.zshrc에 alias 를 rgo='richgo test ./…'로 추가하여 테스트를 쉽게 해보았다.
테스트가 모두 잘 패스하였으며, Test 코드에서 response 받은 body를 출력하게 하여 data를 확인할 수 있게 하였다.
$ rgo -v
START| BindDataAPI
| [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
| [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
| - using env: export GIN_MODE=release
| - using code: gin.SetMode(gin.ReleaseMode)
| [GIN-debug] GET /user --> git-gonic-study/bind-data-2.getUserQueryHandle (3 handlers)
| [GIN-debug] POST /user --> git-gonic-study/bind-data-2.postUserJSONHandle (3 handlers)
| [GIN-debug] PUT /user/:id/:name/:email --> git-gonic-study/bind-data-2.putUserURIHandle (3 handlers)
| [GIN-debug] PUT /user/:id --> git-gonic-study/bind-data-2.putUserURIJSONHandle (3 handlers)
START| BindDataAPI/GET_Query_OK
| 2021/09/17 13:58:58 user: {ID:1 Name:hsjeong Email:a@gmail.com}
| [GIN] 2021/09/17 - 13:58:58 | 200 | 136.5µs | 127.0.0.1 | GET "/user?id=1&name=hsjeong&email=a@gmail.com"
| === CONT TestBindDataAPI
| main_test.go:24: StatusCode: 200, responseBody: {"data":{"id":1,"name":"hsjeong","email":"a@gmail.com"},"status":"ok"}
START| BindDataAPI/POST_JSON_OK
| 2021/09/17 13:58:58 user: {ID:1 Name:Hyunseok, Jeong Email:a@gmail.com}
| [GIN] 2021/09/17 - 13:58:58 | 200 | 44.9µs | 127.0.0.1 | POST "/user"
| === CONT TestBindDataAPI
| main_test.go:24: StatusCode: 200, responseBody: {"data":{"id":1,"name":"Hyunseok, Jeong","email":"a@gmail.com"},"status":"ok"}
START| BindDataAPI/PUT_URI_OK
| 2021/09/17 13:58:58 user: {ID:1 Name:hyunseokJeong Email:a@gmail.com}
| [GIN] 2021/09/17 - 13:58:58 | 200 | 28.6µs | 127.0.0.1 | PUT "/user/1/hyunseokJeong/a@gmail.com"
| === CONT TestBindDataAPI
| main_test.go:24: StatusCode: 200, responseBody: {"data":{"id":1,"name":"hyunseokJeong","email":"a@gmail.com"},"status":"ok"}
START| BindDataAPI/PUT_URI_JSON
| 2021/09/17 13:58:58 user: {ID:1 Name: Email:}
| 2021/09/17 13:58:58 user: {ID:0 Name:Hyunseok, Jeong Email:a@gmail.com}
| [GIN] 2021/09/17 - 13:58:58 | 200 | 85.2µs | 127.0.0.1 | PUT "/user/1"
| === CONT TestBindDataAPI
| main_test.go:24: StatusCode: 200, responseBody: {"data":{"id":0,"name":"Hyunseok, Jeong","email":"a@gmail.com"},"status":"ok"}
PASS | BindDataAPI (0.00s)
PASS | BindDataAPI/GET_Query_OK (0.00s)
PASS | BindDataAPI/POST_JSON_OK (0.00s)
PASS | BindDataAPI/PUT_URI_OK (0.00s)
PASS | BindDataAPI/PUT_URI_JSON (0.00s)
PASS | git-gonic-study/bind-data-2