티스토리 뷰
간단한 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
'golang' 카테고리의 다른 글
Golang: TLS version and Cipher suites (0) | 2021.09.27 |
---|---|
Golang Gin Gonic - 2. Bind data (0) | 2021.09.17 |
Golang: AES-GSM에서 additional data의 활용 (0) | 2021.09.08 |
Golang: AES 암호화 알고리즘의 이용 (0) | 2021.09.07 |
Golang: 사용자의 비밀번호를 저장하고 인증하는 서버 만들기 (0) | 2021.08.18 |
- Total
- Today
- Yesterday
- Bug
- API
- 잡학툰
- strange
- folklore
- Shortcut
- pool
- go
- bun
- 독서후기
- OpenAI
- github
- intellij
- 2023
- golang
- 영화
- JIRA
- 클린 애자일
- agile
- 체호프
- solid
- ChatGPT
- 독서
- notion
- Gin
- websocket
- 제이펍
- 인텔리제이
- postgres
- 노션
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |