튜토리얼: Go와 Gin으로 RESTful API 개발하기
튜토리얼: Go와 Gin으로 RESTful API 개발하기
이 튜토리얼은 Go와 Gin 웹 프레임워크(Gin)로 RESTful 웹 서비스 API를 작성하는 기본기를 다룹니다. Go와 그 도구에 대한 기본적인 친숙함이 있다면 가장 많은 걸 얻어갈 수 있어요. Go가 처음이라면 Tutorial: Get started with Go부터 훑어보는 걸 권장합니다.
Gin은 웹 서비스뿐 아니라 웹 애플리케이션을 만들 때 수반되는 많은 코딩 작업을 단순화해 줘요. 이 튜토리얼에서는 Gin으로 요청을 라우팅하고, 요청 세부 사항을 꺼내고, 응답용 JSON을 직렬화하는 법을 배웁니다. 결과물로 두 개의 엔드포인트를 가진 RESTful API 서버를 만들게 되는데, 예시 프로젝트는 빈티지 재즈 레코드에 관한 데이터 저장소랍니다.
출처: Go 공식 문서
이 튜토리얼은 다음 섹션들로 구성됩니다.
- API 엔드포인트 설계하기
- 코드 폴더 만들기
- 데이터 만들기
- 모든 항목을 반환하는 핸들러 작성하기
- 새 항목을 추가하는 핸들러 작성하기
- 특정 항목을 반환하는 핸들러 작성하기
준비 사항 (Prerequisites)
- Go. 최신 버전의 Go를 쓰는 걸 권장합니다. 설치 방법은 Installing Go를 참고하세요.
- 코드를 편집할 도구. 아무 텍스트 에디터나 잘 작동해요.
- 명령 터미널. Go는 Linux와 Mac의 어떤 터미널에서든 잘 작동하며, Windows에서는 PowerShell이나 cmd에서 잘 돌아갑니다.
- curl 도구. Linux와 Mac에는 보통 이미 설치되어 있어요. Windows에서는 Windows 10 Insider 빌드 17063 이상에 포함되어 있습니다. 그보다 이전 Windows 버전이라면 설치가 필요할 수 있어요. 자세한 내용은 Tar and Curl Come to Windows를 참고하세요.
API 엔드포인트 설계하기
빈티지 레코드를 판매하는 상점(vinyl)에 접근을 제공하는 API를 만들 거예요. 그러려면 클라이언트가 사용자용 앨범을 가져오고 추가할 수 있는 엔드포인트를 제공해야 합니다.
API를 개발할 때는 보통 엔드포인트 설계부터 시작해요. 엔드포인트가 이해하기 쉬우면 API 사용자도 성공할 가능성이 높아지거든요. 이 튜토리얼에서 만들 엔드포인트는 다음과 같아요.
| 엔드포인트 | 동작 |
|---|---|
/albums |
GET – 모든 앨범 목록을 JSON으로 반환 |
/albums |
POST – JSON으로 보낸 요청 데이터에서 새 앨범 추가 |
/albums/:id |
GET – ID로 앨범을 찾아 그 데이터를 JSON으로 반환 |
코드 폴더 만들기
시작하려면 작성할 코드를 위한 프로젝트를 만듭니다.
명령 프롬프트를 열고 홈 디렉터리로 이동하세요. Linux나 Mac에서는:
$ cd
Windows에서는:
C:\> cd %HOMEPATH%
명령 프롬프트에서 web-service-gin이라는 코드 디렉터리를 만들고 이동합니다.
$ mkdir web-service-gin
$ cd web-service-gin
의존성을 관리할 모듈을 만듭니다. go mod init 명령을 실행하면서 코드가 놓일 모듈의 경로를 인자로 줍니다.
$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin
이 명령은 go.mod 파일을 만들고, 앞으로 추가할 의존성이 추적을 위해 그곳에 나열됩니다. 모듈 이름을 모듈 경로로 짓는 법에 대해 더 알아보려면 Managing dependencies 문서를 참고하세요.
데이터 만들기
튜토리얼을 단순하게 유지하려고 데이터를 메모리에 저장할 거예요. 더 전형적인 API라면 데이터베이스와 상호작용하겠지만요. 메모리에 저장한다는 건 서버를 멈출 때마다 앨범 세트가 사라지고, 서버를 다시 시작하면 재생성된다는 뜻이에요.
코드 작성
텍스트 에디터로 web-service 디렉터리에 main.go 파일을 만듭니다. 여기에 Go 코드를 작성할 거예요. main.go 맨 위에 다음 패키지 선언을 붙여 넣습니다.
package main
라이브러리가 아닌 독립 실행 프로그램은 항상 package main에 있어요. 패키지 선언 아래에 앨범 struct 선언을 붙여 넣습니다. 앨범 데이터를 메모리에 저장하는 데 쓸 거예요.
json:"artist" 같은 struct 태그는 struct 내용이 JSON으로 직렬화될 때 필드 이름이 무엇이어야 하는지 지정합니다. 태그가 없으면 JSON이 struct의 대문자 필드 이름을 쓰게 되는데, JSON에서는 흔하지 않은 스타일이죠.
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
방금 추가한 struct 선언 아래에, 시작 데이터로 쓸 album struct 슬라이스를 붙여 넣습니다.
// albums slice to seed record album data.
var albums = []album{
{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}
모든 항목을 반환하는 핸들러 작성하기
클라이언트가 GET /albums에 요청을 보내면, 모든 앨범을 JSON으로 반환하고 싶을 거예요. 이를 위해 두 가지를 작성합니다.
- 응답을 준비하는 로직
- 요청 경로를 그 로직에 매핑하는 코드
참고로 이 순서는 런타임에서 실행되는 순서와 반대예요. 우리는 먼저 의존성을 추가하고, 그다음 그 의존성을 사용하는 코드를 추가하죠.
코드 작성
앞 섹션에서 추가한 struct 코드 아래에, 앨범 목록을 가져오는 다음 코드를 붙여 넣습니다. 이 getAlbums 함수는 album struct 슬라이스에서 JSON을 만들어 응답에 써 넣어요.
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
이 코드에서:
gin.Context파라미터를 받는getAlbums함수를 작성합니다. 이 함수에는 어떤 이름을 붙여도 됐다는 걸 기억하세요 — Gin도 Go도 특정 함수 이름 형식을 요구하지 않아요.gin.Context는 Gin에서 가장 중요한 부분입니다. 요청 세부 사항을 담고, JSON을 검증·직렬화하며, 그 외 여러 일을 해요. (이름이 비슷하지만 Go의 내장 context 패키지와는 달라요.)Context.IndentedJSON을 호출해 struct를 JSON으로 직렬화하고 응답에 추가합니다.- 함수의 첫 인자는 클라이언트에 보내고 싶은 HTTP 상태 코드예요. 여기서는 net/http 패키지의
StatusOK상수를 전달해 200 OK를 나타냅니다.
참고로 Context.IndentedJSON을 Context.JSON 호출로 바꾸면 더 간결한 JSON을 보낼 수 있어요. 실무에서는 들여쓰기된 형태가 디버깅할 때 훨씬 다루기 쉽고, 크기 차이는 보통 작습니다.
main.go 상단, albums 슬라이스 선언 바로 아래에 다음 코드를 붙여 넣어 핸들러 함수를 엔드포인트 경로에 할당합니다. 이렇게 하면 getAlbums가 /albums 엔드포인트 경로에 대한 요청을 처리하는 연결이 설정돼요.
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.Run("localhost:8080")
}
이 코드에서:
Default로 Gin 라우터를 초기화합니다.GET함수로 GET HTTP 메서드와/albums경로를 핸들러 함수에 연결합니다. 참고로getAlbums라는 함수 이름을 전달하고 있어요. 이는 함수의 결과를 전달하는 것과 달라요 — 결과를 전달하려면getAlbums()(괄호가 붙은)를 넘겨야 하죠.Run함수로 라우터를http.Server에 붙이고 서버를 시작합니다.
main.go 상단, 패키지 선언 바로 아래에 방금 작성한 코드를 지원하는 데 필요한 패키지를 import 합니다. 파일의 첫 줄들은 이렇게 보여야 해요.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
main.go를 저장합니다.
코드 실행
Gin 모듈을 의존성으로 추적하기 시작합니다. 커맨드 라인에서 go get으로 github.com/gin-gonic/gin 모듈을 당신 모듈의 의존성으로 추가하세요. 점(.) 인자는 "현재 디렉터리의 코드에 대한 의존성을 가져와라"는 뜻이에요.
$ go get .
go get: added github.com/gin-gonic/gin v1.7.2
Go는 이전 단계에서 추가한 import 선언을 충족시키기 위해 이 의존성을 해석해 내려받았습니다.
main.go가 있는 디렉터리의 커맨드 라인에서 코드를 실행합니다. 점(.) 인자는 "현재 디렉터리의 코드를 실행하라"는 뜻이에요.
$ go run .
코드가 실행되면 요청을 보낼 수 있는 HTTP 서버가 하나 돌고 있는 상태예요. 새 커맨드 라인 창에서 curl로 실행 중인 웹 서비스에 요청을 보내보세요.
$ curl http://localhost:8080/albums
명령은 서비스를 시드한 데이터를 보여줘야 합니다.
[
{
"id": "1",
"title": "Blue Train",
"artist": "John Coltrane",
"price": 56.99
},
{
"id": "2",
"title": "Jeru",
"artist": "Gerry Mulligan",
"price": 17.99
},
{
"id": "3",
"title": "Sarah Vaughan and Clifford Brown",
"artist": "Sarah Vaughan",
"price": 39.99
}
]
API 하나를 시작했어요! 다음 섹션에서는 항목을 추가하는 POST 요청을 처리하는 코드로 엔드포인트를 하나 더 만듭니다.
새 항목을 추가하는 핸들러 작성하기
클라이언트가 POST /albums에 요청을 보내면, 요청 본문에 담긴 앨범을 기존 앨범 데이터에 추가하고 싶을 거예요. 이를 위해 두 가지를 작성합니다.
- 새 앨범을 기존 목록에 추가하는 로직
- POST 요청을 그 로직으로 라우팅하는 약간의 코드
코드 작성
앨범 데이터를 앨범 목록에 추가하는 코드를 더합니다. import 문 뒤 아무 곳에나 다음 코드를 붙여 넣어도 돼요. (파일 끝이 이 코드를 넣기 좋은 위치지만, Go는 함수 선언 순서를 강제하지 않아요.)
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
이 코드에서:
Context.BindJSON으로 요청 본문을newAlbum에 바인딩합니다.- JSON에서 초기화한 album struct를
albums슬라이스에 append 합니다. - 응답에 201 상태 코드와 함께, 추가한 앨범을 나타내는 JSON을 더합니다.
main 함수가 다음처럼 router.POST 함수를 포함하도록 바꿉니다.
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
이 코드에서:
/albums경로의 POST 메서드를postAlbums함수에 연결합니다.- Gin에서는 핸들러를 HTTP 메서드-경로 조합과 연결할 수 있어요. 이렇게 하면 단일 경로로 보내진 요청을 클라이언트가 쓰는 메서드에 따라 따로 라우팅할 수 있죠.
코드 실행
이전 섹션에서 서버가 아직 돌고 있다면 멈춥니다. main.go가 있는 디렉터리의 커맨드 라인에서 코드를 실행해요.
$ go run .
다른 커맨드 라인 창에서 curl로 실행 중인 웹 서비스에 요청을 보냅니다.
$ curl http://localhost:8080/albums \
--include \
--header "Content-Type: application/json" \
--request "POST" \
--data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'
명령은 추가된 앨범의 헤더와 JSON을 보여줘야 해요.
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Jun 2021 00:34:12 GMT
Content-Length: 116
{
"id": "4",
"title": "The Modern Sound of Betty Carter",
"artist": "Betty Carter",
"price": 49.99
}
이전 섹션에서처럼 curl로 전체 앨범 목록을 가져와 새 앨범이 추가됐는지 확인할 수 있어요.
$ curl http://localhost:8080/albums \
--header "Content-Type: application/json" \
--request "GET"
명령은 앨범 목록을 보여줘야 합니다.
[
{
"id": "1",
"title": "Blue Train",
"artist": "John Coltrane",
"price": 56.99
},
{
"id": "2",
"title": "Jeru",
"artist": "Gerry Mulligan",
"price": 17.99
},
{
"id": "3",
"title": "Sarah Vaughan and Clifford Brown",
"artist": "Sarah Vaughan",
"price": 39.99
},
{
"id": "4",
"title": "The Modern Sound of Betty Carter",
"artist": "Betty Carter",
"price": 49.99
}
]
다음 섹션에서는 특정 항목에 대한 GET을 처리하는 코드를 추가합니다.
특정 항목을 반환하는 핸들러 작성하기
클라이언트가 GET /albums/[id]에 요청을 보내면, id 경로 파라미터와 일치하는 ID의 앨범을 반환하고 싶을 거예요. 이를 위해:
- 요청한 앨범을 가져오는 로직을 추가한다.
- 경로를 그 로직에 매핑한다.
코드 작성
이전 섹션에서 추가한 postAlbums 함수 아래에, 특정 앨범을 가져오는 다음 코드를 붙여 넣습니다. 이 getAlbumByID 함수는 요청 경로에서 ID를 추출한 뒤 일치하는 앨범을 찾아요.
// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop over the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
이 코드에서:
Context.Param으로 URL에서id경로 파라미터를 가져옵니다. 이 핸들러를 경로에 매핑할 때 경로에 파라미터용 플레이스홀더를 넣을 거예요.- 슬라이스의 album struct를 반복하면서, ID 필드 값이
id파라미터 값과 일치하는 걸 찾습니다. 찾으면 그 album struct를 JSON으로 직렬화하고 200 OK HTTP 코드로 응답으로 반환하죠. - 앞서 말했듯이 실무 서비스라면 이 조회에 데이터베이스 쿼리를 쓸 겁니다.
- 앨범을 찾지 못하면
http.StatusNotFound로 HTTP 404 오류를 반환합니다.
마지막으로 main이 경로가 이제 /albums/:id인 router.GET 호출을 새로 포함하도록 바꿉니다. 다음 예시처럼요.
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
이 코드에서:
/albums/:id경로를getAlbumByID함수에 연결합니다. Gin에서 경로의 항목 앞에 붙은 콜론(:)은 그 항목이 경로 파라미터라는 뜻이에요.
코드 실행
이전 섹션에서 서버가 아직 돌고 있다면 멈춥니다. main.go가 있는 디렉터리의 커맨드 라인에서 코드를 실행해 서버를 시작해요.
$ go run .
다른 커맨드 라인 창에서 curl로 실행 중인 웹 서비스에 요청을 보냅니다.
$ curl http://localhost:8080/albums/2
명령은 사용한 ID의 앨범 JSON을 보여줘야 해요. 앨범을 찾지 못하면 오류 메시지가 든 JSON을 받습니다.
{
"id": "2",
"title": "Jeru",
"artist": "Gerry Mulligan",
"price": 17.99
}
결론 (Conclusion)
축하합니다! Go와 Gin으로 간단한 RESTful 웹 서비스를 작성했습니다. 다음으로 추천하는 주제들:
- Go가 처음이라면 Effective Go와 How to write Go code에 설명된 좋은 실무 관례를 발견할 수 있을 거예요.
- The Go Tour는 Go 기초를 단계별로 훌륭하게 소개합니다.
- Gin에 대해 더 보려면 Gin Web Framework 패키지 문서 또는 Gin Web Framework 문서를 참고하세요.
완성된 코드 (Completed code)
이 섹션은 이 튜토리얼로 만든 애플리케이션의 코드를 담고 있어요.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
// albums slice to seed record album data.
var albums = []album{
{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop through the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}