티스토리 뷰

golang

Golang Gin Gonic - 1. Basic REST API

사용자 fistful 2021. 9. 14. 17:17
반응형

간단한 API 서버를 제외하고는 웹서비스의 전형적인 HTTP API server 실무에서 개발한 적이 없다. 유튜브에서 Golang 대표적인 web framework Gin 이용한 좋은 강좌를 만나 이를 하나씩 따라하려고 한다. 

 

번째는 간단한 REST API 작성이다. Handler 테스트하는 방법을 곁들였다.

 

- 전체 Playlist: Rest API in Golang using Gin Gonic: https://bit.ly/3hsZKbv

- 이번 포스팅 YouTube. Create basic rest API using Gin Gonic framework in Golang: https://youtu.be/xJ8ryXFobGA

- Test 위해 참고한 링크: https://kpat.io/2019/06/testing-with-gin/

 

- 구현해본 GitHub repo: https://github.com/nicewook/gin-gonic-study

- 이번 블로그 포스팅 소스코드: https://github.com/nicewook/gin-gonic-study/tree/main/basic-api-1

 

준비사항 - Gin 패키지 설치

 

아래와 같이 설치하면 된다

$ go get -u github.com/gin-gonic/gin

 

Gin 으로 API server 만들기

Gin 이용해 API 서버를 만드는 것은 간단하다.

 

1. gin.Default() 이용해 `*gin.Engine` 생성한다

2. endpoint 마다 이를 처리할 hander 지정한다

    1) GET "" 아무것도 없을 때에는 helloHandler()

    2) GET "/name" URL 뒤에 임의의 값을 포함하여 들어올 경우 helloUserHandler()

    3) POST /add 들어올 경우 helloAccountHandler()

///basic-api-1/main.go
func main() {
  newServer().Run()
}
func newServer() *gin.Engine {
  r := gin.Default()
  r.GET("", helloHandler)
  r.GET("/:name", helloUserHandler)
  r.POST("/add", helloAccountHandler)
  return r
}

각각의 hander 동작역시 간단하다

 

1. helloHandler() JSON으로 회신해준다. 코멘트 처리한 코드를 이용하면 단순한 string 회신할 수도 있다.

2. helloUserHandler() endpoint 뒤에 붙어오는 파라미터를 받아서 이용하는 방법을 보여준다

3. helloAccountHandler() POST 들어온 JSON 데이터를 그대로 되돌려 보내준다.

    - Account struct에서 binding:"required" 태그를 주의해보자. 이걸 빠뜨리고 request 하면 StatusBadRequest 회신하게 된다.

    - validation 해주는 함수가 gin.Context.ShoudBind() 이다.

    - 그렇지 않으면 받은 데이터를 그대로 돌려준다.

type Account struct {
  Id   int    `json:"id" binding:"required"`
  Name string `json:"name" binding:"required"`
}
func helloHandler(c *gin.Context) {
  // c.String(http.StatusOK, "hello world!")
  c.JSON(http.StatusOK, gin.H{
    "responseData": "hello world",
  })
}

func helloUserHandler(c *gin.Context) {
  name := c.Param("name")
  c.JSON(http.StatusOK, gin.H{
    "greetings": fmt.Sprintf("hello %v", name),
  })
}

func helloAccountHandler(c *gin.Context) {
  var data Account
  if err := c.ShouldBind(&data); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{
      "error": fmt.Sprintf("err: %v", err),
    })
    return
  }
  c.JSON(http.StatusOK, gin.H{
    "dataReceived": data,
  })
}

Test 이용하여 구현을 확인해보자

 

sub test 위해 함께 test server 만들어준다

  // basic-api-1/main_test.go
  ts := httptest.NewServer(newServer())
  defer ts.Close()

개의 request 간단하다. 각각 ts.URL GET request 보내고 회신이 오는 것을 확인한다.

  t.Run("hello GET", func(t *testing.T) {
    t.Log(ts.URL)
    resp, err := http.Get(fmt.Sprintf("%s", 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)
  })
  t.Run("hello name GET", func(t *testing.T) {
    t.Log(ts.URL)
    resp, err := http.Get(fmt.Sprintf("%s/hsjeong", 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)
  })

$ go test . -v 로 테스트를 실행하였을 때에 테스트의 결과 부분이다, 각각 회신이 제대로 것을 있다.

=== RUN   TestBasicAPI/hello_GET
    main_test.go:30: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 200 |       124.8µs |       127.0.0.1 | GET      "/"
=== CONT  TestBasicAPI
    main_test.go:26: {"responseData":"hello world"}
=== RUN   TestBasicAPI/hello_name_GET
    main_test.go:44: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 200 |         7.6µs |       127.0.0.1 | GET      "/hsjeong"
=== CONT  TestBasicAPI
    main_test.go:26: {"greetings":"hello hsjeong"}

이번에는 POST request 보자. 하나는 잘못된 JSON 보낸 것이고, 다른 하나는 제대로 보낸 것이다.

  t.Run("json POST BadReqeust", func(t *testing.T) {
    t.Log(ts.URL)
    account := struct {
      Id int
    }{
      1,
    }
    b, _ := json.Marshal(account)
    buff := bytes.NewBuffer(b)
    resp, err := http.Post(fmt.Sprintf("%s/add", ts.URL), "application/json", buff)
    if err != nil {
      t.Fatalf("Expected no error, got %v", err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusBadRequest {
      t.Logf("Expected status code %v, got %v", http.StatusBadRequest, resp.StatusCode)
    }
    printResponseBody(resp)
  })
  t.Run("json POST", func(t *testing.T) {
    t.Log(ts.URL)
    account := Account{10, "Alex"}
    b, _ := json.Marshal(account)
    buff := bytes.NewBuffer(b)
    resp, err := http.Post(fmt.Sprintf("%s/add", 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)
  })

각각  http.StatusBadRequest http.StatusOK 기대했고 그에 맞게 회신이 것을 있다.

=== RUN   TestBasicAPI/json_POST_BadReqeust
    main_test.go:58: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 400 |       171.2µs |       127.0.0.1 | POST     "/add"
=== CONT  TestBasicAPI
    main_test.go:26: {"error":"err: Key: 'Account.Name' Error:Field validation for 'Name' failed on the 'required' tag"}
=== RUN   TestBasicAPI/json_POST
    main_test.go:79: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 200 |        25.1µs |       127.0.0.1 | POST     "/add"
=== CONT  TestBasicAPI
    main_test.go:26: {"dataReceived":{"id":10,"name":"Alex"}}

전체 test 결과는 아래와 같다.

$ go test . -v
=== RUN   TestBasicAPI
[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    /                         --> git-gonic-study/basic-api-1.helloHandler (3 handlers)
[GIN-debug] GET    /:name                    --> git-gonic-study/basic-api-1.helloUserHandler (3 handlers)
[GIN-debug] POST   /add                      --> git-gonic-study/basic-api-1.helloAccountHandler (3 handlers)
=== RUN   TestBasicAPI/hello_GET
    main_test.go:30: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 200 |       124.8µs |       127.0.0.1 | GET      "/"
=== CONT  TestBasicAPI
    main_test.go:26: {"responseData":"hello world"}
=== RUN   TestBasicAPI/hello_name_GET
    main_test.go:44: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 200 |         7.6µs |       127.0.0.1 | GET      "/hsjeong"
=== CONT  TestBasicAPI
    main_test.go:26: {"greetings":"hello hsjeong"}
=== RUN   TestBasicAPI/json_POST_BadReqeust
    main_test.go:58: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 400 |       171.2µs |       127.0.0.1 | POST     "/add"
=== CONT  TestBasicAPI
    main_test.go:26: {"error":"err: Key: 'Account.Name' Error:Field validation for 'Name' failed on the 'required' tag"}
=== RUN   TestBasicAPI/json_POST
    main_test.go:79: http://127.0.0.1:7796
[GIN] 2021/09/14 - 17:00:37 | 200 |        25.1µs |       127.0.0.1 | POST     "/add"
=== CONT  TestBasicAPI
    main_test.go:26: {"dataReceived":{"id":10,"name":"Alex"}}
--- PASS: TestBasicAPI (0.00s)
    --- PASS: TestBasicAPI/hello_GET (0.00s)
    --- PASS: TestBasicAPI/hello_name_GET (0.00s)
    --- PASS: TestBasicAPI/json_POST_BadReqeust (0.00s)
    --- PASS: TestBasicAPI/json_POST (0.00s)
PASS
ok      git-gonic-study/basic-api-1     0.551s
반응형
댓글
댓글쓰기 폼